Use SQL generated columns for area, aspectratio, basename, bitrate.
This commit is contained in:
parent
d819b23263
commit
57f1b80442
9 changed files with 102 additions and 56 deletions
|
@ -41,7 +41,7 @@ ffmpeg = _load_ffmpeg()
|
|||
|
||||
# Database #########################################################################################
|
||||
|
||||
DATABASE_VERSION = 21
|
||||
DATABASE_VERSION = 22
|
||||
|
||||
DB_INIT = f'''
|
||||
CREATE TABLE IF NOT EXISTS albums(
|
||||
|
@ -71,15 +71,11 @@ CREATE INDEX IF NOT EXISTS index_bookmarks_author_id on bookmarks(author_id);
|
|||
CREATE TABLE IF NOT EXISTS photos(
|
||||
id INT PRIMARY KEY NOT NULL,
|
||||
filepath TEXT COLLATE NOCASE,
|
||||
basename TEXT COLLATE NOCASE,
|
||||
override_filename TEXT COLLATE NOCASE,
|
||||
extension TEXT COLLATE NOCASE,
|
||||
mtime INT,
|
||||
sha256 TEXT,
|
||||
width INT,
|
||||
height INT,
|
||||
ratio REAL,
|
||||
area INT,
|
||||
duration INT,
|
||||
bytes INT,
|
||||
created INT,
|
||||
|
@ -87,12 +83,26 @@ CREATE TABLE IF NOT EXISTS photos(
|
|||
tagged_at INT,
|
||||
author_id INT,
|
||||
searchhidden INT,
|
||||
-- GENERATED COLUMNS
|
||||
area INT GENERATED ALWAYS AS (width * height) VIRTUAL,
|
||||
aspectratio REAL GENERATED ALWAYS AS (1.0 * width / height) VIRTUAL,
|
||||
-- Thank you ungalcrys
|
||||
-- https://stackoverflow.com/a/38330814/5430534
|
||||
basename TEXT GENERATED ALWAYS AS (
|
||||
COALESCE(
|
||||
override_filename,
|
||||
replace(filepath, rtrim(filepath, replace(replace(filepath, '\\', '/'), '/', '')), '')
|
||||
)
|
||||
) STORED COLLATE NOCASE,
|
||||
extension TEXT GENERATED ALWAYS AS (
|
||||
replace(basename, rtrim(basename, replace(basename, '.', '')), '')
|
||||
) VIRTUAL COLLATE NOCASE,
|
||||
bitrate REAL GENERATED ALWAYS AS ((bytes / 128) / duration) VIRTUAL,
|
||||
FOREIGN KEY(author_id) REFERENCES users(id)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS index_photos_id on photos(id);
|
||||
CREATE INDEX IF NOT EXISTS index_photos_filepath on photos(filepath COLLATE NOCASE);
|
||||
CREATE INDEX IF NOT EXISTS index_photos_override_filename on
|
||||
photos(override_filename COLLATE NOCASE);
|
||||
CREATE INDEX IF NOT EXISTS index_photos_basename on photos(basename COLLATE NOCASE);
|
||||
CREATE INDEX IF NOT EXISTS index_photos_created on photos(created);
|
||||
CREATE INDEX IF NOT EXISTS index_photos_extension on photos(extension);
|
||||
CREATE INDEX IF NOT EXISTS index_photos_author_id on photos(author_id);
|
||||
|
@ -194,7 +204,7 @@ ALLOWED_ORDERBY_COLUMNS = {
|
|||
'extension',
|
||||
'height',
|
||||
'random',
|
||||
'ratio',
|
||||
'aspectratio',
|
||||
'tagged_at',
|
||||
'width',
|
||||
}
|
||||
|
|
|
@ -850,11 +850,11 @@ class Photo(ObjectBase):
|
|||
|
||||
self.real_path = db_row['filepath']
|
||||
self.real_path = pathclass.Path(self.real_path)
|
||||
self.basename = db_row['basename']
|
||||
|
||||
self.id = db_row['id']
|
||||
self.created_unix = db_row['created']
|
||||
self._author_id = self.normalize_author_id(db_row['author_id'])
|
||||
self.override_filename = db_row['override_filename']
|
||||
self.extension = self.real_path.extension.no_dot
|
||||
self.mtime = db_row['mtime']
|
||||
self.sha256 = db_row['sha256']
|
||||
|
@ -864,12 +864,13 @@ class Photo(ObjectBase):
|
|||
else:
|
||||
self.dot_extension = '.' + self.extension
|
||||
|
||||
self.area = db_row['area']
|
||||
self.bytes = db_row['bytes']
|
||||
self.duration = db_row['duration']
|
||||
self.width = db_row['width']
|
||||
self.height = db_row['height']
|
||||
self.ratio = db_row['ratio']
|
||||
self.area = db_row['area']
|
||||
self.aspectratio = db_row['aspectratio']
|
||||
self.bitrate = db_row['bitrate']
|
||||
|
||||
self.thumbnail = self.normalize_thumbnail(db_row['thumbnail'])
|
||||
self.tagged_at_unix = db_row['tagged_at']
|
||||
|
@ -1004,17 +1005,6 @@ class Photo(ObjectBase):
|
|||
|
||||
return soup
|
||||
|
||||
@property
|
||||
def basename(self) -> str:
|
||||
return self.override_filename or self.real_path.basename
|
||||
|
||||
@property
|
||||
def bitrate(self) -> typing.Optional[float]:
|
||||
if self.duration and self.bytes is not None:
|
||||
return (self.bytes / 128) / self.duration
|
||||
else:
|
||||
return None
|
||||
|
||||
@property
|
||||
def bytes_string(self) -> str:
|
||||
if self.bytes is not None:
|
||||
|
@ -1181,11 +1171,11 @@ class Photo(ObjectBase):
|
|||
j = {
|
||||
'type': 'photo',
|
||||
'id': self.id,
|
||||
'aspectratio': self.aspectratio,
|
||||
'author': self.author.jsonify() if self._author_id else None,
|
||||
'extension': self.extension,
|
||||
'width': self.width,
|
||||
'height': self.height,
|
||||
'ratio': self.ratio,
|
||||
'area': self.area,
|
||||
'bytes': self.bytes,
|
||||
'duration_string': self.duration_string,
|
||||
|
@ -1281,8 +1271,6 @@ class Photo(ObjectBase):
|
|||
self.bytes = None
|
||||
self.width = None
|
||||
self.height = None
|
||||
self.area = None
|
||||
self.ratio = None
|
||||
self.duration = None
|
||||
|
||||
if self.real_path.is_file:
|
||||
|
@ -1302,10 +1290,6 @@ class Photo(ObjectBase):
|
|||
elif self.simple_mimetype == 'audio':
|
||||
self._reload_audio_metadata()
|
||||
|
||||
if self.width and self.height:
|
||||
self.area = self.width * self.height
|
||||
self.ratio = round(self.width / self.height, 2)
|
||||
|
||||
hash_kwargs = hash_kwargs or {}
|
||||
sha256 = spinal.hash_file(self.real_path, hash_class=hashlib.sha256, **hash_kwargs)
|
||||
self.sha256 = sha256.hexdigest()
|
||||
|
@ -1316,8 +1300,6 @@ class Photo(ObjectBase):
|
|||
'sha256': self.sha256,
|
||||
'width': self.width,
|
||||
'height': self.height,
|
||||
'area': self.area,
|
||||
'ratio': self.ratio,
|
||||
'duration': self.duration,
|
||||
'bytes': self.bytes,
|
||||
}
|
||||
|
@ -1352,8 +1334,6 @@ class Photo(ObjectBase):
|
|||
data = {
|
||||
'id': self.id,
|
||||
'filepath': new_filepath.absolute_path,
|
||||
'basename': new_filepath.basename,
|
||||
'extension': new_filepath.extension.no_dot,
|
||||
}
|
||||
self.photodb.update(table=Photo, pairs=data, where_key='id')
|
||||
self.real_path = new_filepath
|
||||
|
@ -1456,8 +1436,6 @@ class Photo(ObjectBase):
|
|||
data = {
|
||||
'id': self.id,
|
||||
'filepath': new_path.absolute_path,
|
||||
'basename': new_path.basename,
|
||||
'extension': new_path.extension.no_dot,
|
||||
}
|
||||
self.photodb.update(table=Photo, pairs=data, where_key='id')
|
||||
self.real_path = new_path
|
||||
|
@ -1494,7 +1472,6 @@ class Photo(ObjectBase):
|
|||
'override_filename': new_filename,
|
||||
}
|
||||
self.photodb.update(table=Photo, pairs=data, where_key='id')
|
||||
self.override_filename = new_filename
|
||||
|
||||
self.__reinit__()
|
||||
|
||||
|
|
|
@ -357,9 +357,7 @@ class PDBPhotoMixin:
|
|||
data = {
|
||||
'id': photo_id,
|
||||
'filepath': filepath.absolute_path,
|
||||
'basename': filepath.basename,
|
||||
'override_filename': None,
|
||||
'extension': filepath.extension.no_dot,
|
||||
'created': helpers.now().timestamp(),
|
||||
'tagged_at': None,
|
||||
'author_id': author_id,
|
||||
|
@ -370,14 +368,12 @@ class PDBPhotoMixin:
|
|||
'bytes': None,
|
||||
'width': None,
|
||||
'height': None,
|
||||
'area': None,
|
||||
'ratio': None,
|
||||
'duration': None,
|
||||
'thumbnail': None,
|
||||
}
|
||||
self.insert(table=objects.Photo, pairs=data)
|
||||
|
||||
photo = self.get_cached_instance(objects.Photo, data)
|
||||
photo = self.get_photo(photo_id)
|
||||
|
||||
if do_metadata:
|
||||
hash_kwargs = hash_kwargs or {}
|
||||
|
@ -417,11 +413,12 @@ class PDBPhotoMixin:
|
|||
self,
|
||||
*,
|
||||
area=None,
|
||||
aspectratio=None,
|
||||
width=None,
|
||||
height=None,
|
||||
ratio=None,
|
||||
bytes=None,
|
||||
duration=None,
|
||||
bitrate=None,
|
||||
|
||||
author=None,
|
||||
created=None,
|
||||
|
@ -450,7 +447,7 @@ class PDBPhotoMixin:
|
|||
):
|
||||
'''
|
||||
PHOTO PROPERTIES
|
||||
area, width, height, ratio, bytes, duration:
|
||||
area, aspectratio, width, height, bytes, duration, bitrate:
|
||||
A dotdot_range string representing min and max. Or just a number
|
||||
for lower bound.
|
||||
|
||||
|
@ -531,7 +528,7 @@ class PDBPhotoMixin:
|
|||
How many *successful* results to skip before we start yielding.
|
||||
|
||||
orderby:
|
||||
A list of strings like ['ratio DESC', 'created ASC'] to sort
|
||||
A list of strings like ['aspectratio DESC', 'created ASC'] to sort
|
||||
and subsort the results.
|
||||
Descending is assumed if not provided.
|
||||
|
||||
|
@ -562,9 +559,10 @@ class PDBPhotoMixin:
|
|||
searchhelpers.minmax('created', created, minimums, maximums, warning_bag=warning_bag)
|
||||
searchhelpers.minmax('width', width, minimums, maximums, warning_bag=warning_bag)
|
||||
searchhelpers.minmax('height', height, minimums, maximums, warning_bag=warning_bag)
|
||||
searchhelpers.minmax('ratio', ratio, minimums, maximums, warning_bag=warning_bag)
|
||||
searchhelpers.minmax('aspectratio', aspectratio, minimums, maximums, warning_bag=warning_bag)
|
||||
searchhelpers.minmax('bytes', bytes, minimums, maximums, warning_bag=warning_bag)
|
||||
searchhelpers.minmax('duration', duration, minimums, maximums, warning_bag=warning_bag)
|
||||
searchhelpers.minmax('bitrate', bitrate, minimums, maximums, warning_bag=warning_bag)
|
||||
|
||||
author = searchhelpers.normalize_author(author, photodb=self, warning_bag=warning_bag)
|
||||
extension = searchhelpers.normalize_extension(extension)
|
||||
|
@ -652,7 +650,8 @@ class PDBPhotoMixin:
|
|||
'area': area,
|
||||
'width': width,
|
||||
'height': height,
|
||||
'ratio': ratio,
|
||||
'aspectratio': aspectratio,
|
||||
'bitrate': bitrate,
|
||||
'bytes': bytes,
|
||||
'duration': duration,
|
||||
'author': list(author) or None,
|
||||
|
|
|
@ -347,10 +347,6 @@ def normalize_orderby(orderby, warning_bag=None):
|
|||
column_friendly = column
|
||||
column_expanded = {
|
||||
'random': 'RANDOM()',
|
||||
'area': '(width * height)',
|
||||
'basename': 'COALESCE(override_filename, basename)',
|
||||
'bitrate': '((bytes / 128) / duration)',
|
||||
'ratio': '(width / height)',
|
||||
}.get(column, column)
|
||||
|
||||
final_orderby.append( (column_friendly, column_expanded, direction) )
|
||||
|
|
|
@ -129,7 +129,7 @@ def search_by_argparse(args, yield_albums=False, yield_photos=False):
|
|||
area=args.area,
|
||||
width=args.width,
|
||||
height=args.height,
|
||||
ratio=args.ratio,
|
||||
aspectratio=args.aspectratio,
|
||||
bytes=args.bytes,
|
||||
duration=args.duration,
|
||||
author=args.author,
|
||||
|
@ -1272,7 +1272,7 @@ def main(argv):
|
|||
''',
|
||||
)
|
||||
p_search.add_argument(
|
||||
'--ratio',
|
||||
'--aspectratio',
|
||||
metavar='X-Y',
|
||||
default=None,
|
||||
help='''
|
||||
|
|
|
@ -399,10 +399,11 @@ def get_search_core():
|
|||
area = request.args.get('area')
|
||||
width = request.args.get('width')
|
||||
height = request.args.get('height')
|
||||
ratio = request.args.get('ratio')
|
||||
aspectratio = request.args.get('aspectratio')
|
||||
bytes = request.args.get('bytes')
|
||||
has_thumbnail = request.args.get('has_thumbnail')
|
||||
duration = request.args.get('duration')
|
||||
bitrate = request.args.get('bitrate')
|
||||
created = request.args.get('created')
|
||||
|
||||
# These are in a dictionary so I can pass them to the page template.
|
||||
|
@ -410,9 +411,10 @@ def get_search_core():
|
|||
'area': area,
|
||||
'width': width,
|
||||
'height': height,
|
||||
'ratio': ratio,
|
||||
'aspectratio': aspectratio,
|
||||
'bytes': bytes,
|
||||
'duration': duration,
|
||||
'bitrate': bitrate,
|
||||
|
||||
'author': author,
|
||||
'created': created,
|
||||
|
|
|
@ -59,6 +59,7 @@
|
|||
.photo_viewer_application,
|
||||
.photo_viewer_text
|
||||
{
|
||||
display: flex;
|
||||
justify-items: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
@ -183,7 +184,7 @@
|
|||
{% endif %}
|
||||
{% if photo.width %}
|
||||
<li title="{{photo.area}} px">Dimensions: {{photo.width}}x{{photo.height}} px</li>
|
||||
<li>Aspect ratio: {{photo.ratio}}</li>
|
||||
<li>Aspect ratio: {{photo.aspectratio|round(2)}}</li>
|
||||
{% endif %}
|
||||
<li>Size: {{photo.bytes|bytestring}}</li>
|
||||
{% if photo.duration %}
|
||||
|
|
|
@ -171,7 +171,7 @@
|
|||
<option value="area" {{"selected" if selected_column=="area" else ""}}>Area</option>
|
||||
<option value="width" {{"selected" if selected_column=="width" else ""}}>Width</option>
|
||||
<option value="height" {{"selected" if selected_column=="height" else ""}}>Height</option>
|
||||
<option value="ratio" {{"selected" if selected_column=="ratio" else ""}}>Aspect Ratio</option>
|
||||
<option value="aspectratio" {{"selected" if selected_column=="aspectratio" else ""}}>Aspect Ratio</option>
|
||||
<option value="bytes" {{"selected" if selected_column=="bytes" else ""}}>File size</option>
|
||||
<option value="duration" {{"selected" if selected_column=="duration" else ""}}>Duration</option>
|
||||
<option value="bitrate" {{"selected" if selected_column=="bitrate" else ""}}>Bitrate</option>
|
||||
|
|
|
@ -823,6 +823,67 @@ def upgrade_20_to_21(photodb):
|
|||
photodb.update(table=etiquette.objects.Photo, pairs={'id': photo.id, 'thumbnail': store_as}, where_key='id')
|
||||
photo.thumbnail = new_thumbnail
|
||||
|
||||
def upgrade_21_to_22(photodb):
|
||||
m = Migrator(photodb)
|
||||
|
||||
m.tables['photos']['create'] = '''
|
||||
CREATE TABLE IF NOT EXISTS photos(
|
||||
id INT PRIMARY KEY NOT NULL,
|
||||
filepath TEXT COLLATE NOCASE,
|
||||
override_filename TEXT COLLATE NOCASE,
|
||||
mtime INT,
|
||||
sha256 TEXT,
|
||||
width INT,
|
||||
height INT,
|
||||
duration INT,
|
||||
bytes INT,
|
||||
created INT,
|
||||
thumbnail TEXT,
|
||||
tagged_at INT,
|
||||
author_id INT,
|
||||
searchhidden INT,
|
||||
-- GENERATED COLUMNS
|
||||
area INT GENERATED ALWAYS AS (width * height) VIRTUAL,
|
||||
aspectratio REAL GENERATED ALWAYS AS (1.0 * width / height) VIRTUAL,
|
||||
-- Thank you ungalcrys
|
||||
-- https://stackoverflow.com/a/38330814/5430534
|
||||
basename TEXT GENERATED ALWAYS AS (
|
||||
COALESCE(
|
||||
override_filename,
|
||||
replace(filepath, rtrim(filepath, replace(replace(filepath, '\\', '/'), '/', '')), '')
|
||||
)
|
||||
) STORED COLLATE NOCASE,
|
||||
extension TEXT GENERATED ALWAYS AS (
|
||||
replace(basename, rtrim(basename, replace(basename, '.', '')), '')
|
||||
) VIRTUAL COLLATE NOCASE,
|
||||
bitrate REAL GENERATED ALWAYS AS ((bytes / 128) / duration) VIRTUAL,
|
||||
FOREIGN KEY(author_id) REFERENCES users(id)
|
||||
);
|
||||
'''
|
||||
m.tables['photos']['transfer'] = '''
|
||||
INSERT INTO photos SELECT
|
||||
id,
|
||||
filepath,
|
||||
override_filename,
|
||||
mtime,
|
||||
sha256,
|
||||
width,
|
||||
height,
|
||||
duration,
|
||||
bytes,
|
||||
created,
|
||||
thumbnail,
|
||||
tagged_at,
|
||||
author_id,
|
||||
searchhidden
|
||||
FROM photos_old;
|
||||
'''
|
||||
|
||||
m.go()
|
||||
|
||||
photodb.execute('DROP INDEX index_photos_override_filename')
|
||||
photodb.execute('CREATE INDEX IF NOT EXISTS index_photos_basename on photos(basename COLLATE NOCASE)')
|
||||
|
||||
def upgrade_all(data_directory):
|
||||
'''
|
||||
Given the directory containing a phototagger database, apply all of the
|
||||
|
|
Loading…
Reference in a new issue