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