Use SQL generated columns for area, aspectratio, basename, bitrate.

This commit is contained in:
voussoir 2022-08-13 18:08:45 -07:00
parent d819b23263
commit 57f1b80442
No known key found for this signature in database
GPG key ID: 5F7554F8C26DACCB
9 changed files with 102 additions and 56 deletions

View file

@ -41,7 +41,7 @@ ffmpeg = _load_ffmpeg()
# Database ######################################################################################### # Database #########################################################################################
DATABASE_VERSION = 21 DATABASE_VERSION = 22
DB_INIT = f''' DB_INIT = f'''
CREATE TABLE IF NOT EXISTS albums( 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( CREATE TABLE IF NOT EXISTS photos(
id INT PRIMARY KEY NOT NULL, id INT PRIMARY KEY NOT NULL,
filepath TEXT COLLATE NOCASE, filepath TEXT COLLATE NOCASE,
basename TEXT COLLATE NOCASE,
override_filename TEXT COLLATE NOCASE, override_filename TEXT COLLATE NOCASE,
extension TEXT COLLATE NOCASE,
mtime INT, mtime INT,
sha256 TEXT, sha256 TEXT,
width INT, width INT,
height INT, height INT,
ratio REAL,
area INT,
duration INT, duration INT,
bytes INT, bytes INT,
created INT, created INT,
@ -87,12 +83,26 @@ CREATE TABLE IF NOT EXISTS photos(
tagged_at INT, tagged_at INT,
author_id INT, author_id INT,
searchhidden 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) 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_id on photos(id);
CREATE INDEX IF NOT EXISTS index_photos_filepath on photos(filepath COLLATE NOCASE); CREATE INDEX IF NOT EXISTS index_photos_filepath on photos(filepath COLLATE NOCASE);
CREATE INDEX IF NOT EXISTS index_photos_override_filename on CREATE INDEX IF NOT EXISTS index_photos_basename on photos(basename COLLATE NOCASE);
photos(override_filename COLLATE NOCASE);
CREATE INDEX IF NOT EXISTS index_photos_created on photos(created); 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_extension on photos(extension);
CREATE INDEX IF NOT EXISTS index_photos_author_id on photos(author_id); CREATE INDEX IF NOT EXISTS index_photos_author_id on photos(author_id);
@ -194,7 +204,7 @@ ALLOWED_ORDERBY_COLUMNS = {
'extension', 'extension',
'height', 'height',
'random', 'random',
'ratio', 'aspectratio',
'tagged_at', 'tagged_at',
'width', 'width',
} }

View file

@ -850,11 +850,11 @@ class Photo(ObjectBase):
self.real_path = db_row['filepath'] self.real_path = db_row['filepath']
self.real_path = pathclass.Path(self.real_path) self.real_path = pathclass.Path(self.real_path)
self.basename = db_row['basename']
self.id = db_row['id'] self.id = db_row['id']
self.created_unix = db_row['created'] self.created_unix = db_row['created']
self._author_id = self.normalize_author_id(db_row['author_id']) 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.extension = self.real_path.extension.no_dot
self.mtime = db_row['mtime'] self.mtime = db_row['mtime']
self.sha256 = db_row['sha256'] self.sha256 = db_row['sha256']
@ -864,12 +864,13 @@ class Photo(ObjectBase):
else: else:
self.dot_extension = '.' + self.extension self.dot_extension = '.' + self.extension
self.area = db_row['area']
self.bytes = db_row['bytes'] self.bytes = db_row['bytes']
self.duration = db_row['duration'] self.duration = db_row['duration']
self.width = db_row['width'] self.width = db_row['width']
self.height = db_row['height'] 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.thumbnail = self.normalize_thumbnail(db_row['thumbnail'])
self.tagged_at_unix = db_row['tagged_at'] self.tagged_at_unix = db_row['tagged_at']
@ -1004,17 +1005,6 @@ class Photo(ObjectBase):
return soup 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 @property
def bytes_string(self) -> str: def bytes_string(self) -> str:
if self.bytes is not None: if self.bytes is not None:
@ -1181,11 +1171,11 @@ class Photo(ObjectBase):
j = { j = {
'type': 'photo', 'type': 'photo',
'id': self.id, 'id': self.id,
'aspectratio': self.aspectratio,
'author': self.author.jsonify() if self._author_id else None, 'author': self.author.jsonify() if self._author_id else None,
'extension': self.extension, 'extension': self.extension,
'width': self.width, 'width': self.width,
'height': self.height, 'height': self.height,
'ratio': self.ratio,
'area': self.area, 'area': self.area,
'bytes': self.bytes, 'bytes': self.bytes,
'duration_string': self.duration_string, 'duration_string': self.duration_string,
@ -1281,8 +1271,6 @@ class Photo(ObjectBase):
self.bytes = None self.bytes = None
self.width = None self.width = None
self.height = None self.height = None
self.area = None
self.ratio = None
self.duration = None self.duration = None
if self.real_path.is_file: if self.real_path.is_file:
@ -1302,10 +1290,6 @@ class Photo(ObjectBase):
elif self.simple_mimetype == 'audio': elif self.simple_mimetype == 'audio':
self._reload_audio_metadata() 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 {} hash_kwargs = hash_kwargs or {}
sha256 = spinal.hash_file(self.real_path, hash_class=hashlib.sha256, **hash_kwargs) sha256 = spinal.hash_file(self.real_path, hash_class=hashlib.sha256, **hash_kwargs)
self.sha256 = sha256.hexdigest() self.sha256 = sha256.hexdigest()
@ -1316,8 +1300,6 @@ class Photo(ObjectBase):
'sha256': self.sha256, 'sha256': self.sha256,
'width': self.width, 'width': self.width,
'height': self.height, 'height': self.height,
'area': self.area,
'ratio': self.ratio,
'duration': self.duration, 'duration': self.duration,
'bytes': self.bytes, 'bytes': self.bytes,
} }
@ -1352,8 +1334,6 @@ class Photo(ObjectBase):
data = { data = {
'id': self.id, 'id': self.id,
'filepath': new_filepath.absolute_path, '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.photodb.update(table=Photo, pairs=data, where_key='id')
self.real_path = new_filepath self.real_path = new_filepath
@ -1456,8 +1436,6 @@ class Photo(ObjectBase):
data = { data = {
'id': self.id, 'id': self.id,
'filepath': new_path.absolute_path, '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.photodb.update(table=Photo, pairs=data, where_key='id')
self.real_path = new_path self.real_path = new_path
@ -1494,7 +1472,6 @@ class Photo(ObjectBase):
'override_filename': new_filename, 'override_filename': new_filename,
} }
self.photodb.update(table=Photo, pairs=data, where_key='id') self.photodb.update(table=Photo, pairs=data, where_key='id')
self.override_filename = new_filename
self.__reinit__() self.__reinit__()

View file

@ -357,9 +357,7 @@ class PDBPhotoMixin:
data = { data = {
'id': photo_id, 'id': photo_id,
'filepath': filepath.absolute_path, 'filepath': filepath.absolute_path,
'basename': filepath.basename,
'override_filename': None, 'override_filename': None,
'extension': filepath.extension.no_dot,
'created': helpers.now().timestamp(), 'created': helpers.now().timestamp(),
'tagged_at': None, 'tagged_at': None,
'author_id': author_id, 'author_id': author_id,
@ -370,14 +368,12 @@ class PDBPhotoMixin:
'bytes': None, 'bytes': None,
'width': None, 'width': None,
'height': None, 'height': None,
'area': None,
'ratio': None,
'duration': None, 'duration': None,
'thumbnail': None, 'thumbnail': None,
} }
self.insert(table=objects.Photo, pairs=data) self.insert(table=objects.Photo, pairs=data)
photo = self.get_cached_instance(objects.Photo, data) photo = self.get_photo(photo_id)
if do_metadata: if do_metadata:
hash_kwargs = hash_kwargs or {} hash_kwargs = hash_kwargs or {}
@ -417,11 +413,12 @@ class PDBPhotoMixin:
self, self,
*, *,
area=None, area=None,
aspectratio=None,
width=None, width=None,
height=None, height=None,
ratio=None,
bytes=None, bytes=None,
duration=None, duration=None,
bitrate=None,
author=None, author=None,
created=None, created=None,
@ -450,7 +447,7 @@ class PDBPhotoMixin:
): ):
''' '''
PHOTO PROPERTIES 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 A dotdot_range string representing min and max. Or just a number
for lower bound. for lower bound.
@ -531,7 +528,7 @@ class PDBPhotoMixin:
How many *successful* results to skip before we start yielding. How many *successful* results to skip before we start yielding.
orderby: 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. and subsort the results.
Descending is assumed if not provided. Descending is assumed if not provided.
@ -562,9 +559,10 @@ class PDBPhotoMixin:
searchhelpers.minmax('created', created, minimums, maximums, warning_bag=warning_bag) searchhelpers.minmax('created', created, minimums, maximums, warning_bag=warning_bag)
searchhelpers.minmax('width', width, 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('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('bytes', bytes, minimums, maximums, warning_bag=warning_bag)
searchhelpers.minmax('duration', duration, 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) author = searchhelpers.normalize_author(author, photodb=self, warning_bag=warning_bag)
extension = searchhelpers.normalize_extension(extension) extension = searchhelpers.normalize_extension(extension)
@ -652,7 +650,8 @@ class PDBPhotoMixin:
'area': area, 'area': area,
'width': width, 'width': width,
'height': height, 'height': height,
'ratio': ratio, 'aspectratio': aspectratio,
'bitrate': bitrate,
'bytes': bytes, 'bytes': bytes,
'duration': duration, 'duration': duration,
'author': list(author) or None, 'author': list(author) or None,

View file

@ -347,10 +347,6 @@ def normalize_orderby(orderby, warning_bag=None):
column_friendly = column column_friendly = column
column_expanded = { column_expanded = {
'random': 'RANDOM()', 'random': 'RANDOM()',
'area': '(width * height)',
'basename': 'COALESCE(override_filename, basename)',
'bitrate': '((bytes / 128) / duration)',
'ratio': '(width / height)',
}.get(column, column) }.get(column, column)
final_orderby.append( (column_friendly, column_expanded, direction) ) final_orderby.append( (column_friendly, column_expanded, direction) )

View file

@ -129,7 +129,7 @@ def search_by_argparse(args, yield_albums=False, yield_photos=False):
area=args.area, area=args.area,
width=args.width, width=args.width,
height=args.height, height=args.height,
ratio=args.ratio, aspectratio=args.aspectratio,
bytes=args.bytes, bytes=args.bytes,
duration=args.duration, duration=args.duration,
author=args.author, author=args.author,
@ -1272,7 +1272,7 @@ def main(argv):
''', ''',
) )
p_search.add_argument( p_search.add_argument(
'--ratio', '--aspectratio',
metavar='X-Y', metavar='X-Y',
default=None, default=None,
help=''' help='''

View file

@ -399,10 +399,11 @@ def get_search_core():
area = request.args.get('area') area = request.args.get('area')
width = request.args.get('width') width = request.args.get('width')
height = request.args.get('height') height = request.args.get('height')
ratio = request.args.get('ratio') aspectratio = request.args.get('aspectratio')
bytes = request.args.get('bytes') bytes = request.args.get('bytes')
has_thumbnail = request.args.get('has_thumbnail') has_thumbnail = request.args.get('has_thumbnail')
duration = request.args.get('duration') duration = request.args.get('duration')
bitrate = request.args.get('bitrate')
created = request.args.get('created') created = request.args.get('created')
# These are in a dictionary so I can pass them to the page template. # These are in a dictionary so I can pass them to the page template.
@ -410,9 +411,10 @@ def get_search_core():
'area': area, 'area': area,
'width': width, 'width': width,
'height': height, 'height': height,
'ratio': ratio, 'aspectratio': aspectratio,
'bytes': bytes, 'bytes': bytes,
'duration': duration, 'duration': duration,
'bitrate': bitrate,
'author': author, 'author': author,
'created': created, 'created': created,

View file

@ -59,6 +59,7 @@
.photo_viewer_application, .photo_viewer_application,
.photo_viewer_text .photo_viewer_text
{ {
display: flex;
justify-items: center; justify-items: center;
align-items: center; align-items: center;
} }
@ -183,7 +184,7 @@
{% endif %} {% endif %}
{% if photo.width %} {% if photo.width %}
<li title="{{photo.area}} px">Dimensions: {{photo.width}}x{{photo.height}} px</li> <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 %} {% endif %}
<li>Size: {{photo.bytes|bytestring}}</li> <li>Size: {{photo.bytes|bytestring}}</li>
{% if photo.duration %} {% if photo.duration %}

View file

@ -171,7 +171,7 @@
<option value="area" {{"selected" if selected_column=="area" else ""}}>Area</option> <option value="area" {{"selected" if selected_column=="area" else ""}}>Area</option>
<option value="width" {{"selected" if selected_column=="width" else ""}}>Width</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="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="bytes" {{"selected" if selected_column=="bytes" else ""}}>File size</option>
<option value="duration" {{"selected" if selected_column=="duration" else ""}}>Duration</option> <option value="duration" {{"selected" if selected_column=="duration" else ""}}>Duration</option>
<option value="bitrate" {{"selected" if selected_column=="bitrate" else ""}}>Bitrate</option> <option value="bitrate" {{"selected" if selected_column=="bitrate" else ""}}>Bitrate</option>

View file

@ -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') photodb.update(table=etiquette.objects.Photo, pairs={'id': photo.id, 'thumbnail': store_as}, where_key='id')
photo.thumbnail = new_thumbnail 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): def upgrade_all(data_directory):
''' '''
Given the directory containing a phototagger database, apply all of the Given the directory containing a phototagger database, apply all of the