checkpoint
This commit is contained in:
		
							parent
							
								
									7ad6160d38
								
							
						
					
					
						commit
						5de1736347
					
				
					 10 changed files with 424 additions and 337 deletions
				
			
		
							
								
								
									
										48
									
								
								etiquette/constants.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								etiquette/constants.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,48 @@ | ||||||
|  | import string | ||||||
|  | 
 | ||||||
|  | # Errors and warnings | ||||||
|  | ERROR_DATABASE_OUTOFDATE = 'Database is out-of-date. {current} should be {new}' | ||||||
|  | ERROR_INVALID_ACTION = 'Invalid action' | ||||||
|  | ERROR_NO_SUCH_TAG = 'Doesn\'t exist' | ||||||
|  | ERROR_NO_TAG_GIVEN = 'No tag name supplied' | ||||||
|  | ERROR_SYNONYM_ITSELF = 'Cant apply synonym to itself' | ||||||
|  | ERROR_TAG_TOO_SHORT = 'Not enough valid chars' | ||||||
|  | WARNING_MINMAX_INVALID = 'Field "{field}": "{value}" is not a valid request. Ignored.' | ||||||
|  | WARNING_MINMAX_OOO = 'Field "{field}": minimum "{min}" maximum "{max}" are out of order. Ignored.' | ||||||
|  | WARNING_NO_SUCH_TAG = 'Tag "{tag}" does not exist. Ignored.' | ||||||
|  | WARNING_ORDERBY_BADCOL = '"{column}" is not a sorting option. Ignored.' | ||||||
|  | WARNING_ORDERBY_BADSORTER = 'You can\'t order "{column}" by "{sorter}". Defaulting to descending.' | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | # Default settings | ||||||
|  | MIN_TAG_NAME_LENGTH = 1 | ||||||
|  | MAX_TAG_NAME_LENGTH = 32 | ||||||
|  | VALID_TAG_CHARS = string.ascii_lowercase + string.digits + '_' | ||||||
|  | 
 | ||||||
|  | DEFAULT_ID_LENGTH = 12 | ||||||
|  | DEFAULT_DBNAME = 'phototagger.db' | ||||||
|  | DEFAULT_THUMBDIR = '_etiquette\\site_thumbnails' | ||||||
|  | DEFAULT_DIGEST_EXCLUDE_FILES = [ | ||||||
|  |     DEFAULT_DBNAME, | ||||||
|  |     'desktop.ini', | ||||||
|  |     'thumbs.db' | ||||||
|  | ] | ||||||
|  | DEFAULT_DIGEST_EXCLUDE_DIRS = [ | ||||||
|  |     '_site_thumbnails', | ||||||
|  | ] | ||||||
|  | FILE_READ_CHUNK = 2 ** 20 | ||||||
|  | 
 | ||||||
|  | THUMBNAIL_WIDTH = 400 | ||||||
|  | THUMBNAIL_HEIGHT = 400 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | # Operational info | ||||||
|  | ADDITIONAL_MIMETYPES = { | ||||||
|  |     'srt': 'text', | ||||||
|  |     'mkv': 'video', | ||||||
|  | } | ||||||
|  | EXPRESSION_OPERATORS = {'(', ')', 'OR', 'AND', 'NOT'} | ||||||
|  | MOTD_STRINGS = [ | ||||||
|  | 'Good morning, Paul. What will your first sequence of the day be?', | ||||||
|  | #'Buckle up, it\'s time to:', | ||||||
|  | ] | ||||||
|  | @ -1,11 +1,14 @@ | ||||||
|  | import flask | ||||||
| from flask import request | from flask import request | ||||||
|  | import functools | ||||||
|  | import time | ||||||
| import uuid | import uuid | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
| def _generate_session_token(): | def _generate_session_token(): | ||||||
|     token = str(uuid.uuid4()) |     token = str(uuid.uuid4()) | ||||||
|     #print('MAKE SESSION', token) |     #print('MAKE SESSION', token) | ||||||
|     return token |     return token | ||||||
|  | 
 | ||||||
| def give_session_token(function): | def give_session_token(function): | ||||||
|     @functools.wraps(function) |     @functools.wraps(function) | ||||||
|     def wrapped(*args, **kwargs): |     def wrapped(*args, **kwargs): | ||||||
|  | @ -25,3 +28,23 @@ def give_session_token(function): | ||||||
| 
 | 
 | ||||||
|         return ret |         return ret | ||||||
|     return wrapped |     return wrapped | ||||||
|  | 
 | ||||||
|  | def not_implemented(function): | ||||||
|  |     ''' | ||||||
|  |     Decorator to remember what needs doing. | ||||||
|  |     ''' | ||||||
|  |     warnings.warn('%s is not implemented' % function.__name__) | ||||||
|  |     return function | ||||||
|  | 
 | ||||||
|  | def time_me(function): | ||||||
|  |     ''' | ||||||
|  |     Decorator. After the function is run, print the elapsed time. | ||||||
|  |     ''' | ||||||
|  |     @functools.wraps(function) | ||||||
|  |     def timed_function(*args, **kwargs): | ||||||
|  |         start = time.time() | ||||||
|  |         result = function(*args, **kwargs) | ||||||
|  |         end = time.time() | ||||||
|  |         print('%s: %0.8f' % (function.__name__, end-start)) | ||||||
|  |         return result | ||||||
|  |     return timed_function | ||||||
|  |  | ||||||
|  | @ -1,9 +1,6 @@ | ||||||
| import distutils.util |  | ||||||
| import flask | import flask | ||||||
| from flask import request | from flask import request | ||||||
| import functools |  | ||||||
| import json | import json | ||||||
| import math |  | ||||||
| import mimetypes | import mimetypes | ||||||
| import os | import os | ||||||
| import random | import random | ||||||
|  | @ -13,6 +10,10 @@ import sys | ||||||
| import time | import time | ||||||
| import warnings | import warnings | ||||||
| 
 | 
 | ||||||
|  | import constants | ||||||
|  | import decorators | ||||||
|  | import helpers | ||||||
|  | import jsonify | ||||||
| import phototagger | import phototagger | ||||||
| 
 | 
 | ||||||
| try: | try: | ||||||
|  | @ -32,23 +33,10 @@ site.config.update( | ||||||
|     TEMPLATES_AUTO_RELOAD=True, |     TEMPLATES_AUTO_RELOAD=True, | ||||||
| ) | ) | ||||||
| site.jinja_env.add_extension('jinja2.ext.do') | site.jinja_env.add_extension('jinja2.ext.do') | ||||||
| #site.debug = True | site.debug = True | ||||||
| 
 | 
 | ||||||
| P = phototagger.PhotoDB() | P = phototagger.PhotoDB() | ||||||
| 
 | 
 | ||||||
| FILE_READ_CHUNK = 2 ** 20 |  | ||||||
| 
 |  | ||||||
| MOTD_STRINGS = [ |  | ||||||
| 'Good morning, Paul. What will your first sequence of the day be?', |  | ||||||
| #'Buckle up, it\'s time to:', |  | ||||||
| ] |  | ||||||
| 
 |  | ||||||
| THUMBDIR = phototagger.DEFAULT_THUMBDIR |  | ||||||
| ERROR_INVALID_ACTION = 'Invalid action' |  | ||||||
| ERROR_NO_TAG_GIVEN = 'No tag name supplied' |  | ||||||
| ERROR_TAG_TOO_SHORT = 'Not enough valid chars' |  | ||||||
| ERROR_SYNONYM_ITSELF = 'Cant apply synonym to itself' |  | ||||||
| ERROR_NO_SUCH_TAG = 'Doesn\'t exist' |  | ||||||
| 
 | 
 | ||||||
| #################################################################################################### | #################################################################################################### | ||||||
| #################################################################################################### | #################################################################################################### | ||||||
|  | @ -56,15 +44,6 @@ ERROR_NO_SUCH_TAG = 'Doesn\'t exist' | ||||||
| #################################################################################################### | #################################################################################################### | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
| def _helper_comma_split(s): |  | ||||||
|     if s is None: |  | ||||||
|         return s |  | ||||||
|     s = s.replace(' ', ',') |  | ||||||
|     s = [x.strip() for x in s.split(',')] |  | ||||||
|     s = [x for x in s if x] |  | ||||||
|     return s |  | ||||||
| 
 |  | ||||||
| def create_tag(easybake_string): | def create_tag(easybake_string): | ||||||
|     notes = P.easybake(easybake_string) |     notes = P.easybake(easybake_string) | ||||||
|     notes = [{'action': action, 'tagname': tagname} for (action, tagname) in notes] |     notes = [{'action': action, 'tagname': tagname} for (action, tagname) in notes] | ||||||
|  | @ -91,16 +70,6 @@ def delete_synonym(synonym): | ||||||
|     master_tag.remove_synonym(synonym) |     master_tag.remove_synonym(synonym) | ||||||
|     return {'action':'delete_synonym', 'synonym': synonym} |     return {'action':'delete_synonym', 'synonym': synonym} | ||||||
| 
 | 
 | ||||||
| def edit_params(original, modifications): |  | ||||||
|     new_params = original.to_dict() |  | ||||||
|     new_params.update(modifications) |  | ||||||
|     if not new_params: |  | ||||||
|         return '' |  | ||||||
|     new_params = ['%s=%s' % (k, v) for (k, v) in new_params.items() if v] |  | ||||||
|     new_params = '&'.join(new_params) |  | ||||||
|     new_params = '?' + new_params |  | ||||||
|     return new_params |  | ||||||
| 
 |  | ||||||
| def make_json_response(j, *args, **kwargs): | def make_json_response(j, *args, **kwargs): | ||||||
|     dumped = json.dumps(j) |     dumped = json.dumps(j) | ||||||
|     response = flask.Response(dumped, *args, **kwargs) |     response = flask.Response(dumped, *args, **kwargs) | ||||||
|  | @ -125,34 +94,6 @@ def P_tag(tagname): | ||||||
|     except phototagger.NoSuchTag as e: |     except phototagger.NoSuchTag as e: | ||||||
|         flask.abort(404, 'That tag doesnt exist: %s' % e) |         flask.abort(404, 'That tag doesnt exist: %s' % e) | ||||||
| 
 | 
 | ||||||
| def read_filebytes(filepath, range_min, range_max): |  | ||||||
|     range_span = range_max - range_min |  | ||||||
| 
 |  | ||||||
|     #print('read span', range_min, range_max, range_span) |  | ||||||
|     f = open(filepath, 'rb') |  | ||||||
|     f.seek(range_min) |  | ||||||
|     sent_amount = 0 |  | ||||||
|     with f: |  | ||||||
|         while sent_amount < range_span: |  | ||||||
|             #print(sent_amount) |  | ||||||
|             chunk = f.read(FILE_READ_CHUNK) |  | ||||||
|             if len(chunk) == 0: |  | ||||||
|                 break |  | ||||||
| 
 |  | ||||||
|             yield chunk |  | ||||||
|             sent_amount += len(chunk) |  | ||||||
| 
 |  | ||||||
| def seconds_to_hms(seconds): |  | ||||||
|     seconds = math.ceil(seconds) |  | ||||||
|     (minutes, seconds) = divmod(seconds, 60) |  | ||||||
|     (hours, minutes) = divmod(minutes, 60) |  | ||||||
|     parts = [] |  | ||||||
|     if hours: parts.append(hours) |  | ||||||
|     if minutes: parts.append(minutes) |  | ||||||
|     parts.append(seconds) |  | ||||||
|     hms = ':'.join('%02d' % part for part in parts) |  | ||||||
|     return hms |  | ||||||
| 
 |  | ||||||
| def send_file(filepath): | def send_file(filepath): | ||||||
|     ''' |     ''' | ||||||
|     Range-enabled file sending. |     Range-enabled file sending. | ||||||
|  | @ -208,7 +149,7 @@ def send_file(filepath): | ||||||
|     if request.method == 'HEAD': |     if request.method == 'HEAD': | ||||||
|         outgoing_data = bytes() |         outgoing_data = bytes() | ||||||
|     else: |     else: | ||||||
|         outgoing_data = read_filebytes(filepath, range_min=range_min, range_max=range_max) |         outgoing_data = helpers.read_filebytes(filepath, range_min=range_min, range_max=range_max) | ||||||
| 
 | 
 | ||||||
|     response = flask.Response( |     response = flask.Response( | ||||||
|         outgoing_data, |         outgoing_data, | ||||||
|  | @ -217,64 +158,6 @@ def send_file(filepath): | ||||||
|     ) |     ) | ||||||
|     return response |     return response | ||||||
| 
 | 
 | ||||||
| def truthystring(s): |  | ||||||
|     if isinstance(s, (bool, int)) or s is None: |  | ||||||
|         return s |  | ||||||
|     s = s.lower() |  | ||||||
|     if s in {'1', 'true', 't', 'yes', 'y', 'on'}: |  | ||||||
|         return True |  | ||||||
|     if s in {'null', 'none'}: |  | ||||||
|         return None |  | ||||||
|     return False |  | ||||||
| 
 |  | ||||||
| #################################################################################################### |  | ||||||
| #################################################################################################### |  | ||||||
| #################################################################################################### |  | ||||||
| #################################################################################################### |  | ||||||
| 
 |  | ||||||
| def jsonify_album(album, minimal=False): |  | ||||||
|     j = { |  | ||||||
|         'id': album.id, |  | ||||||
|         'description': album.description, |  | ||||||
|         'title': album.title, |  | ||||||
|     } |  | ||||||
|     if minimal is False: |  | ||||||
|         j['photos'] = [jsonify_photo(photo) for photo in album.photos()] |  | ||||||
|         j['parent'] = album.parent() |  | ||||||
|         j['sub_albums'] = [child.id for child in album.children()] |  | ||||||
| 
 |  | ||||||
|     return j |  | ||||||
| 
 |  | ||||||
| def jsonify_photo(photo): |  | ||||||
|     tags = photo.tags() |  | ||||||
|     tags.sort(key=lambda x: x.name) |  | ||||||
|     j = { |  | ||||||
|         'id': photo.id, |  | ||||||
|         'extension': photo.extension, |  | ||||||
|         'width': photo.width, |  | ||||||
|         'height': photo.height, |  | ||||||
|         'ratio': photo.ratio, |  | ||||||
|         'area': photo.area, |  | ||||||
|         'bytes': photo.bytes, |  | ||||||
|         'duration': seconds_to_hms(photo.duration) if photo.duration is not None else None, |  | ||||||
|         'duration_int': photo.duration, |  | ||||||
|         'bytestring': photo.bytestring(), |  | ||||||
|         'has_thumbnail': bool(photo.thumbnail), |  | ||||||
|         'created': photo.created, |  | ||||||
|         'filename': photo.basename, |  | ||||||
|         'mimetype': photo.mimetype(), |  | ||||||
|         'albums': [jsonify_album(album, minimal=True) for album in photo.albums()], |  | ||||||
|         'tags': [jsonify_tag(tag) for tag in tags], |  | ||||||
|     } |  | ||||||
|     return j |  | ||||||
| 
 |  | ||||||
| def jsonify_tag(tag): |  | ||||||
|     j = { |  | ||||||
|         'id': tag.id, |  | ||||||
|         'name': tag.name, |  | ||||||
|         'qualified_name': tag.qualified_name(), |  | ||||||
|     } |  | ||||||
|     return j |  | ||||||
| 
 | 
 | ||||||
| #################################################################################################### | #################################################################################################### | ||||||
| #################################################################################################### | #################################################################################################### | ||||||
|  | @ -282,40 +165,45 @@ def jsonify_tag(tag): | ||||||
| #################################################################################################### | #################################################################################################### | ||||||
| 
 | 
 | ||||||
| @site.route('/') | @site.route('/') | ||||||
| @give_session_token | @decorators.give_session_token | ||||||
| def root(): | def root(): | ||||||
|     motd = random.choice(MOTD_STRINGS) |     motd = random.choice(constants.MOTD_STRINGS) | ||||||
|     return flask.render_template('root.html', motd=motd) |     return flask.render_template('root.html', motd=motd) | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
| @site.route('/favicon.ico') | @site.route('/favicon.ico') | ||||||
| @site.route('/favicon.png') | @site.route('/favicon.png') | ||||||
| def favicon(): | def favicon(): | ||||||
|     filename = os.path.join('static', 'favicon.png') |     filename = os.path.join('static', 'favicon.png') | ||||||
|     return flask.send_file(filename) |     return flask.send_file(filename) | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
| def get_album_core(albumid): | def get_album_core(albumid): | ||||||
|     album = P_album(albumid) |     album = P_album(albumid) | ||||||
|     album = jsonify_album(album) |     album = jsonify.album(album) | ||||||
|  |     album['sub_albums'] = [P_album(x) for x in album['sub_albums']] | ||||||
|  |     album['sub_albums'].sort(key=lambda x: (x.title or x.id).lower()) | ||||||
|  |     album['sub_albums'] = [jsonify.album(x, minimal=True) for x in album['sub_albums']] | ||||||
|     return album |     return album | ||||||
| 
 | 
 | ||||||
| @site.route('/album/<albumid>') | @site.route('/album/<albumid>') | ||||||
| @give_session_token | @decorators.give_session_token | ||||||
| def get_album_html(albumid): | def get_album_html(albumid): | ||||||
|     album = get_album_core(albumid) |     album = get_album_core(albumid) | ||||||
|     response = flask.render_template( |     response = flask.render_template( | ||||||
|         'album.html', |         'album.html', | ||||||
|         album=album, |         album=album, | ||||||
|         child_albums=[jsonify_album(P_album(x)) for x in album['sub_albums']], |  | ||||||
|         photos=album['photos'], |         photos=album['photos'], | ||||||
|     ) |     ) | ||||||
|     return response |     return response | ||||||
| 
 | 
 | ||||||
| @site.route('/album/<albumid>.json') | @site.route('/album/<albumid>.json') | ||||||
| @give_session_token | @decorators.give_session_token | ||||||
| def get_album_json(albumid): | def get_album_json(albumid): | ||||||
|     album = get_album_core(albumid) |     album = get_album_core(albumid) | ||||||
|     return make_json_response(album) |     return make_json_response(album) | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
| @site.route('/album/<albumid>.tar') | @site.route('/album/<albumid>.tar') | ||||||
| def get_album_tar(albumid): | def get_album_tar(albumid): | ||||||
|     album = P_album(albumid) |     album = P_album(albumid) | ||||||
|  | @ -326,21 +214,26 @@ def get_album_tar(albumid): | ||||||
|     outgoing_headers = {'Content-Type': 'application/octet-stream'} |     outgoing_headers = {'Content-Type': 'application/octet-stream'} | ||||||
|     return flask.Response(streamed_zip, headers=outgoing_headers) |     return flask.Response(streamed_zip, headers=outgoing_headers) | ||||||
| 
 | 
 | ||||||
| @site.route('/albums') | 
 | ||||||
| @give_session_token | def get_albums_core(): | ||||||
| def get_albums_html(): |  | ||||||
|     albums = P.get_albums() |     albums = P.get_albums() | ||||||
|     albums = [a for a in albums if a.parent() is None] |     albums = [a for a in albums if a.parent() is None] | ||||||
|  |     albums = [jsonify.album(album, minimal=True) for album in albums] | ||||||
|  |     return albums | ||||||
|  | 
 | ||||||
|  | @site.route('/albums') | ||||||
|  | @decorators.give_session_token | ||||||
|  | def get_albums_html(): | ||||||
|  |     albums = get_albums_core() | ||||||
|     return flask.render_template('albums.html', albums=albums) |     return flask.render_template('albums.html', albums=albums) | ||||||
| 
 | 
 | ||||||
| @site.route('/albums.json') | @site.route('/albums.json') | ||||||
| @give_session_token | @decorators.give_session_token | ||||||
| def get_albums_json(): | def get_albums_json(): | ||||||
|     albums = P.get_albums() |     albums = get_albums_core() | ||||||
|     albums = [a for a in albums if a.parent() is None] |  | ||||||
|     albums = [jsonify_album(album, minimal=True) for album in albums] |  | ||||||
|     return make_json_response(albums) |     return make_json_response(albums) | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
| @site.route('/file/<photoid>') | @site.route('/file/<photoid>') | ||||||
| def get_file(photoid): | def get_file(photoid): | ||||||
|     requested_photoid = photoid |     requested_photoid = photoid | ||||||
|  | @ -348,10 +241,10 @@ def get_file(photoid): | ||||||
|     photo = P.get_photo(photoid) |     photo = P.get_photo(photoid) | ||||||
| 
 | 
 | ||||||
|     do_download = request.args.get('download', False) |     do_download = request.args.get('download', False) | ||||||
|     do_download = truthystring(do_download) |     do_download = helpers.truthystring(do_download) | ||||||
| 
 | 
 | ||||||
|     use_original_filename = request.args.get('original_filename', False) |     use_original_filename = request.args.get('original_filename', False) | ||||||
|     use_original_filename = truthystring(use_original_filename) |     use_original_filename = helpers.truthystring(use_original_filename) | ||||||
| 
 | 
 | ||||||
|     if do_download: |     if do_download: | ||||||
|         if use_original_filename: |         if use_original_filename: | ||||||
|  | @ -368,25 +261,27 @@ def get_file(photoid): | ||||||
|     else: |     else: | ||||||
|         return send_file(photo.real_filepath) |         return send_file(photo.real_filepath) | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
| def get_photo_core(photoid): | def get_photo_core(photoid): | ||||||
|     photo = P_photo(photoid) |     photo = P_photo(photoid) | ||||||
|     photo = jsonify_photo(photo) |     photo = jsonify.photo(photo) | ||||||
|     return photo |     return photo | ||||||
| 
 | 
 | ||||||
| @site.route('/photo/<photoid>', methods=['GET']) | @site.route('/photo/<photoid>', methods=['GET']) | ||||||
| @give_session_token | @decorators.give_session_token | ||||||
| def get_photo_html(photoid): | def get_photo_html(photoid): | ||||||
|     photo = get_photo_core(photoid) |     photo = get_photo_core(photoid) | ||||||
|     photo['tags'].sort(key=lambda x: x['qualified_name']) |     photo['tags'].sort(key=lambda x: x['qualified_name']) | ||||||
|     return flask.render_template('photo.html', photo=photo) |     return flask.render_template('photo.html', photo=photo) | ||||||
| 
 | 
 | ||||||
| @site.route('/photo/<photoid>.json', methods=['GET']) | @site.route('/photo/<photoid>.json', methods=['GET']) | ||||||
| @give_session_token | @decorators.give_session_token | ||||||
| def get_photo_json(photoid): | def get_photo_json(photoid): | ||||||
|     photo = get_photo_core(photoid) |     photo = get_photo_core(photoid) | ||||||
|     photo = make_json_response(photo) |     photo = make_json_response(photo) | ||||||
|     return photo |     return photo | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
| def get_search_core(): | def get_search_core(): | ||||||
|     #print(request.args) |     #print(request.args) | ||||||
| 
 | 
 | ||||||
|  | @ -396,9 +291,9 @@ def get_search_core(): | ||||||
|     extension_not_string = request.args.get('extension_not', None) |     extension_not_string = request.args.get('extension_not', None) | ||||||
|     mimetype_string = request.args.get('mimetype', None) |     mimetype_string = request.args.get('mimetype', None) | ||||||
| 
 | 
 | ||||||
|     extension_list = _helper_comma_split(extension_string) |     extension_list = helpers.comma_split(extension_string) | ||||||
|     extension_not_list = _helper_comma_split(extension_not_string) |     extension_not_list = helpers.comma_split(extension_not_string) | ||||||
|     mimetype_list = _helper_comma_split(mimetype_string) |     mimetype_list = helpers.comma_split(mimetype_string) | ||||||
| 
 | 
 | ||||||
|     # LIMIT |     # LIMIT | ||||||
|     limit = request.args.get('limit', '') |     limit = request.args.get('limit', '') | ||||||
|  | @ -440,7 +335,7 @@ def get_search_core(): | ||||||
|     if has_tags == '': |     if has_tags == '': | ||||||
|         has_tags = None |         has_tags = None | ||||||
|     else: |     else: | ||||||
|         has_tags = truthystring(has_tags) |         has_tags = helpers.truthystring(has_tags) | ||||||
| 
 | 
 | ||||||
|     # MINMAXERS |     # MINMAXERS | ||||||
|     area = request.args.get('area', None) |     area = request.args.get('area', None) | ||||||
|  | @ -480,7 +375,7 @@ def get_search_core(): | ||||||
|     #print(search_kwargs) |     #print(search_kwargs) | ||||||
|     with warnings.catch_warnings(record=True) as catcher: |     with warnings.catch_warnings(record=True) as catcher: | ||||||
|         photos = list(P.search(**search_kwargs)) |         photos = list(P.search(**search_kwargs)) | ||||||
|         photos = [jsonify_photo(photo) for photo in photos] |         photos = [jsonify.photo(photo, include_albums=False) for photo in photos] | ||||||
|         warns = [str(warning.message) for warning in catcher] |         warns = [str(warning.message) for warning in catcher] | ||||||
|     #print(warns) |     #print(warns) | ||||||
| 
 | 
 | ||||||
|  | @ -493,13 +388,14 @@ def get_search_core(): | ||||||
| 
 | 
 | ||||||
|     # PREV-NEXT PAGE URLS |     # PREV-NEXT PAGE URLS | ||||||
|     offset = offset or 0 |     offset = offset or 0 | ||||||
|  |     original_params = request.args.to_dict() | ||||||
|     if len(photos) == limit: |     if len(photos) == limit: | ||||||
|         next_params = edit_params(request.args, {'offset': offset + limit}) |         next_params = helpers.edit_params(original_params, {'offset': offset + limit}) | ||||||
|         next_page_url = '/search' + next_params |         next_page_url = '/search' + next_params | ||||||
|     else: |     else: | ||||||
|         next_page_url = None |         next_page_url = None | ||||||
|     if offset > 0: |     if offset > 0: | ||||||
|         prev_params = edit_params(request.args, {'offset': max(0, offset - limit)}) |         prev_params = helpers.edit_params(original_params, {'offset': max(0, offset - limit)}) | ||||||
|         prev_page_url = '/search' + prev_params |         prev_page_url = '/search' + prev_params | ||||||
|     else: |     else: | ||||||
|         prev_page_url = None |         prev_page_url = None | ||||||
|  | @ -520,7 +416,7 @@ def get_search_core(): | ||||||
|     return final_results |     return final_results | ||||||
| 
 | 
 | ||||||
| @site.route('/search') | @site.route('/search') | ||||||
| @give_session_token | @decorators.give_session_token | ||||||
| def get_search_html(): | def get_search_html(): | ||||||
|     search_results = get_search_core() |     search_results = get_search_core() | ||||||
|     search_kwargs = search_results['search_kwargs'] |     search_kwargs = search_results['search_kwargs'] | ||||||
|  | @ -538,16 +434,17 @@ def get_search_html(): | ||||||
|     return response |     return response | ||||||
| 
 | 
 | ||||||
| @site.route('/search.json') | @site.route('/search.json') | ||||||
| @give_session_token | @decorators.give_session_token | ||||||
| def get_search_json(): | def get_search_json(): | ||||||
|     search_results = get_search_core() |     search_results = get_search_core() | ||||||
|     search_kwargs = search_results['search_kwargs'] |     search_kwargs = search_results['search_kwargs'] | ||||||
|     qualname_map = search_results['qualname_map'] |     qualname_map = search_results['qualname_map'] | ||||||
|     include_qualname_map = request.args.get('include_map', False) |     include_qualname_map = request.args.get('include_map', False) | ||||||
|     include_qualname_map = truthystring(include_qualname_map) |     include_qualname_map = helpers.truthystring(include_qualname_map) | ||||||
|     if not include_qualname_map: |     if not include_qualname_map: | ||||||
|         search_results.pop('qualname_map') |         search_results.pop('qualname_map') | ||||||
|     return make_json_response(j) |     return make_json_response(search_results) | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| @site.route('/static/<filename>') | @site.route('/static/<filename>') | ||||||
| def get_static(filename): | def get_static(filename): | ||||||
|  | @ -556,20 +453,33 @@ def get_static(filename): | ||||||
|     filename = os.path.join('static', filename) |     filename = os.path.join('static', filename) | ||||||
|     return flask.send_file(filename) |     return flask.send_file(filename) | ||||||
| 
 | 
 | ||||||
| @site.route('/tags') | 
 | ||||||
| @site.route('/tags/<specific_tag>') | def get_tags_core(specific_tag=None): | ||||||
| @give_session_token |  | ||||||
| def get_tags(specific_tag=None): |  | ||||||
|     try: |     try: | ||||||
|         tags = P.export_tags(phototagger.tag_export_easybake, specific_tag=specific_tag) |         tags = P.export_tags(phototagger.tag_export_easybake, specific_tag=specific_tag) | ||||||
|     except phototagger.NoSuchTag: |     except phototagger.NoSuchTag: | ||||||
|         flask.abort(404, 'That tag doesnt exist') |         flask.abort(404, 'That tag doesnt exist') | ||||||
| 
 |  | ||||||
|     tags = tags.split('\n') |     tags = tags.split('\n') | ||||||
|     tags = [t for t in tags if t != ''] |     tags = [t for t in tags if t != ''] | ||||||
|     tags = [(t, t.split('.')[-1].split('+')[0]) for t in tags] |     tags = [(t, t.split('.')[-1].split('+')[0]) for t in tags] | ||||||
|  |     return tags | ||||||
|  |      | ||||||
|  | @site.route('/tags') | ||||||
|  | @site.route('/tags/<specific_tag>') | ||||||
|  | @decorators.give_session_token | ||||||
|  | def get_tags_html(specific_tag=None): | ||||||
|  |     tags = get_tags_core(specific_tag) | ||||||
|     return flask.render_template('tags.html', tags=tags) |     return flask.render_template('tags.html', tags=tags) | ||||||
| 
 | 
 | ||||||
|  | @site.route('/tags.json') | ||||||
|  | @site.route('/tags/<specific_tag>.json') | ||||||
|  | @decorators.give_session_token | ||||||
|  | def get_tags_json(specific_tag=None): | ||||||
|  |     tags = get_tags_core(specific_tag) | ||||||
|  |     tags = [t[0] for t in tags] | ||||||
|  |     return make_json_response(tags) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| @site.route('/thumbnail/<photoid>') | @site.route('/thumbnail/<photoid>') | ||||||
| def get_thumbnail(photoid): | def get_thumbnail(photoid): | ||||||
|     photoid = photoid.split('.')[0] |     photoid = photoid.split('.')[0] | ||||||
|  | @ -580,9 +490,10 @@ def get_thumbnail(photoid): | ||||||
|         flask.abort(404, 'That file doesnt have a thumbnail') |         flask.abort(404, 'That file doesnt have a thumbnail') | ||||||
|     return send_file(path) |     return send_file(path) | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
| @site.route('/album/<albumid>', methods=['POST']) | @site.route('/album/<albumid>', methods=['POST']) | ||||||
| @site.route('/album/<albumid>.json', methods=['POST']) | @site.route('/album/<albumid>.json', methods=['POST']) | ||||||
| @give_session_token | @decorators.give_session_token | ||||||
| def post_edit_album(albumid): | def post_edit_album(albumid): | ||||||
|     ''' |     ''' | ||||||
|     Edit the album's title and description. |     Edit the album's title and description. | ||||||
|  | @ -601,15 +512,16 @@ def post_edit_album(albumid): | ||||||
|             response = {'error': 'That tag doesnt exist', 'tagname': tag} |             response = {'error': 'That tag doesnt exist', 'tagname': tag} | ||||||
|             return make_json_response(response, status=404) |             return make_json_response(response, status=404) | ||||||
|         recursive = request.form.get('recursive', False) |         recursive = request.form.get('recursive', False) | ||||||
|         recursive = truthystring(recursive) |         recursive = helpers.truthystring(recursive) | ||||||
|         album.add_tag_to_all(tag, nested_children=recursive) |         album.add_tag_to_all(tag, nested_children=recursive) | ||||||
|         response['action'] = action |         response['action'] = action | ||||||
|         response['tagname'] = tag.name |         response['tagname'] = tag.name | ||||||
|         return make_json_response(response) |         return make_json_response(response) | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
| @site.route('/photo/<photoid>', methods=['POST']) | @site.route('/photo/<photoid>', methods=['POST']) | ||||||
| @site.route('/photo/<photoid>.json', methods=['POST']) | @site.route('/photo/<photoid>.json', methods=['POST']) | ||||||
| @give_session_token | @decorators.give_session_token | ||||||
| def post_edit_photo(photoid): | def post_edit_photo(photoid): | ||||||
|     ''' |     ''' | ||||||
|     Add and remove tags from photos. |     Add and remove tags from photos. | ||||||
|  | @ -642,8 +554,9 @@ def post_edit_photo(photoid): | ||||||
|     response['tagname'] = tag.name |     response['tagname'] = tag.name | ||||||
|     return make_json_response(response) |     return make_json_response(response) | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
| @site.route('/tags', methods=['POST']) | @site.route('/tags', methods=['POST']) | ||||||
| @give_session_token | @decorators.give_session_token | ||||||
| def post_edit_tags(): | def post_edit_tags(): | ||||||
|     ''' |     ''' | ||||||
|     Create and delete tags and synonyms. |     Create and delete tags and synonyms. | ||||||
|  | @ -661,12 +574,12 @@ def post_edit_tags(): | ||||||
|         method = delete_tag |         method = delete_tag | ||||||
|     else: |     else: | ||||||
|         status = 400 |         status = 400 | ||||||
|         response = {'error': ERROR_INVALID_ACTION} |         response = {'error': constants.ERROR_INVALID_ACTION} | ||||||
| 
 | 
 | ||||||
|     if status == 200: |     if status == 200: | ||||||
|         tag = request.form[action].strip() |         tag = request.form[action].strip() | ||||||
|         if tag == '': |         if tag == '': | ||||||
|             response = {'error': ERROR_NO_TAG_GIVEN} |             response = {'error': constants.ERROR_NO_TAG_GIVEN} | ||||||
|             status = 400 |             status = 400 | ||||||
| 
 | 
 | ||||||
|     if status == 200: |     if status == 200: | ||||||
|  | @ -675,11 +588,11 @@ def post_edit_tags(): | ||||||
|         try: |         try: | ||||||
|             response = method(tag) |             response = method(tag) | ||||||
|         except phototagger.TagTooShort: |         except phototagger.TagTooShort: | ||||||
|             response = {'error': ERROR_TAG_TOO_SHORT, 'tagname': tag} |             response = {'error': constants.ERROR_TAG_TOO_SHORT, 'tagname': tag} | ||||||
|         except phototagger.CantSynonymSelf: |         except phototagger.CantSynonymSelf: | ||||||
|             response = {'error': ERROR_SYNONYM_ITSELF, 'tagname': tag} |             response = {'error': constants.ERROR_SYNONYM_ITSELF, 'tagname': tag} | ||||||
|         except phototagger.NoSuchTag as e: |         except phototagger.NoSuchTag as e: | ||||||
|             response = {'error': ERROR_NO_SUCH_TAG, 'tagname': tag} |             response = {'error': constants.ERROR_NO_SUCH_TAG, 'tagname': tag} | ||||||
|         except ValueError as e: |         except ValueError as e: | ||||||
|             response = {'error': e.args[0], 'tagname': tag} |             response = {'error': e.args[0], 'tagname': tag} | ||||||
|         else: |         else: | ||||||
|  |  | ||||||
							
								
								
									
										134
									
								
								etiquette/helpers.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										134
									
								
								etiquette/helpers.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,134 @@ | ||||||
|  | import math | ||||||
|  | 
 | ||||||
|  | import constants | ||||||
|  | 
 | ||||||
|  | def chunk_sequence(sequence, chunk_length, allow_incomplete=True): | ||||||
|  |     ''' | ||||||
|  |     Given a sequence, divide it into sequences of length `chunk_length`. | ||||||
|  | 
 | ||||||
|  |     allow_incomplete: | ||||||
|  |         If True, allow the final chunk to be shorter if the | ||||||
|  |         given sequence is not an exact multiple of `chunk_length`. | ||||||
|  |         If False, the incomplete chunk will be discarded. | ||||||
|  |     ''' | ||||||
|  |     (complete, leftover) = divmod(len(sequence), chunk_length) | ||||||
|  |     if not allow_incomplete: | ||||||
|  |         leftover = 0 | ||||||
|  | 
 | ||||||
|  |     chunk_count = complete + min(leftover, 1) | ||||||
|  | 
 | ||||||
|  |     chunks = [] | ||||||
|  |     for x in range(chunk_count): | ||||||
|  |         left = chunk_length * x | ||||||
|  |         right = left + chunk_length | ||||||
|  |         chunks.append(sequence[left:right]) | ||||||
|  | 
 | ||||||
|  |     return chunks | ||||||
|  | 
 | ||||||
|  | def comma_split(s): | ||||||
|  |     ''' | ||||||
|  |     Split the string apart by commas, discarding all extra whitespace and | ||||||
|  |     blank phrases. | ||||||
|  |     ''' | ||||||
|  |     if s is None: | ||||||
|  |         return s | ||||||
|  |     s = s.replace(' ', ',') | ||||||
|  |     s = [x.strip() for x in s.split(',')] | ||||||
|  |     s = [x for x in s if x] | ||||||
|  |     return s | ||||||
|  | 
 | ||||||
|  | def edit_params(original, modifications): | ||||||
|  |     ''' | ||||||
|  |     Given a dictionary representing URL parameters, | ||||||
|  |     apply the modifications and return a URL parameter string. | ||||||
|  | 
 | ||||||
|  |     {'a':1, 'b':2}, {'b':3} => ?a=1&b=3 | ||||||
|  |     ''' | ||||||
|  |     new_params = original.copy() | ||||||
|  |     new_params.update(modifications) | ||||||
|  |     if not new_params: | ||||||
|  |         return '' | ||||||
|  |     new_params = ['%s=%s' % (k, v) for (k, v) in new_params.items() if v] | ||||||
|  |     new_params = '&'.join(new_params) | ||||||
|  |     new_params = '?' + new_params | ||||||
|  |     return new_params | ||||||
|  | 
 | ||||||
|  | def fit_into_bounds(image_width, image_height, frame_width, frame_height): | ||||||
|  |     ''' | ||||||
|  |     Given the w+h of the image and the w+h of the frame, | ||||||
|  |     return new w+h that fits the image into the frame | ||||||
|  |     while maintaining the aspect ratio. | ||||||
|  |     ''' | ||||||
|  |     ratio = min(frame_width/image_width, frame_height/image_height) | ||||||
|  | 
 | ||||||
|  |     new_width = int(image_width * ratio) | ||||||
|  |     new_height = int(image_height * ratio) | ||||||
|  | 
 | ||||||
|  |     return (new_width, new_height) | ||||||
|  | 
 | ||||||
|  | def hms_to_seconds(hms): | ||||||
|  |     ''' | ||||||
|  |     Convert hh:mm:ss string to an integer seconds. | ||||||
|  |     ''' | ||||||
|  |     hms = hms.split(':') | ||||||
|  |     seconds = 0 | ||||||
|  |     if len(hms) == 3: | ||||||
|  |         seconds += int(hms[0])*3600 | ||||||
|  |         hms.pop(0) | ||||||
|  |     if len(hms) == 2: | ||||||
|  |         seconds += int(hms[0])*60 | ||||||
|  |         hms.pop(0) | ||||||
|  |     if len(hms) == 1: | ||||||
|  |         seconds += int(hms[0]) | ||||||
|  |     return seconds | ||||||
|  | 
 | ||||||
|  | def is_xor(*args): | ||||||
|  |     ''' | ||||||
|  |     Return True if and only if one arg is truthy. | ||||||
|  |     ''' | ||||||
|  |     return [bool(a) for a in args].count(True) == 1 | ||||||
|  | 
 | ||||||
|  | def read_filebytes(filepath, range_min, range_max): | ||||||
|  |     ''' | ||||||
|  |     Yield chunks of bytes from the file between the endpoints. | ||||||
|  |     ''' | ||||||
|  |     range_span = range_max - range_min | ||||||
|  | 
 | ||||||
|  |     #print('read span', range_min, range_max, range_span) | ||||||
|  |     f = open(filepath, 'rb') | ||||||
|  |     f.seek(range_min) | ||||||
|  |     sent_amount = 0 | ||||||
|  |     with f: | ||||||
|  |         while sent_amount < range_span: | ||||||
|  |             #print(sent_amount) | ||||||
|  |             chunk = f.read(constants.FILE_READ_CHUNK) | ||||||
|  |             if len(chunk) == 0: | ||||||
|  |                 break | ||||||
|  | 
 | ||||||
|  |             yield chunk | ||||||
|  |             sent_amount += len(chunk) | ||||||
|  | 
 | ||||||
|  | def seconds_to_hms(seconds): | ||||||
|  |     ''' | ||||||
|  |     Convert integer number of seconds to an hh:mm:ss string. | ||||||
|  |     Only the necessary fields are used. | ||||||
|  |     ''' | ||||||
|  |     seconds = math.ceil(seconds) | ||||||
|  |     (minutes, seconds) = divmod(seconds, 60) | ||||||
|  |     (hours, minutes) = divmod(minutes, 60) | ||||||
|  |     parts = [] | ||||||
|  |     if hours: parts.append(hours) | ||||||
|  |     if minutes: parts.append(minutes) | ||||||
|  |     parts.append(seconds) | ||||||
|  |     hms = ':'.join('%02d' % part for part in parts) | ||||||
|  |     return hms | ||||||
|  | 
 | ||||||
|  | def truthystring(s): | ||||||
|  |     if isinstance(s, (bool, int)) or s is None: | ||||||
|  |         return s | ||||||
|  |     s = s.lower() | ||||||
|  |     if s in {'1', 'true', 't', 'yes', 'y', 'on'}: | ||||||
|  |         return True | ||||||
|  |     if s in {'null', 'none'}: | ||||||
|  |         return None | ||||||
|  |     return False | ||||||
							
								
								
									
										49
									
								
								etiquette/jsonify.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								etiquette/jsonify.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,49 @@ | ||||||
|  | import helpers | ||||||
|  | 
 | ||||||
|  | def album(a, minimal=False): | ||||||
|  |     j = { | ||||||
|  |         'id': a.id, | ||||||
|  |         'description': a.description, | ||||||
|  |         'title': a.title, | ||||||
|  |     } | ||||||
|  |     if not minimal: | ||||||
|  |         j['photos'] = [photo(p) for p in a.photos()] | ||||||
|  |         j['parent'] = a.parent() | ||||||
|  |         j['sub_albums'] = [child.id for child in a.children()] | ||||||
|  | 
 | ||||||
|  |     return j | ||||||
|  | 
 | ||||||
|  | def photo(p, include_albums=True, include_tags=True): | ||||||
|  |     tags = p.tags() | ||||||
|  |     tags.sort(key=lambda x: x.name) | ||||||
|  |     j = { | ||||||
|  |         'id': p.id, | ||||||
|  |         'extension': p.extension, | ||||||
|  |         'width': p.width, | ||||||
|  |         'height': p.height, | ||||||
|  |         'ratio': p.ratio, | ||||||
|  |         'area': p.area, | ||||||
|  |         'bytes': p.bytes, | ||||||
|  |         'duration_str': helpers.seconds_to_hms(p.duration) if p.duration is not None else None, | ||||||
|  |         'duration': p.duration, | ||||||
|  |         'bytestring': p.bytestring(), | ||||||
|  |         'has_thumbnail': bool(p.thumbnail), | ||||||
|  |         'created': p.created, | ||||||
|  |         'filename': p.basename, | ||||||
|  |         'mimetype': p.mimetype(), | ||||||
|  |     } | ||||||
|  |     if include_albums: | ||||||
|  |         j['albums'] = [album(a, minimal=True) for a in p.albums()] | ||||||
|  | 
 | ||||||
|  |     if include_tags: | ||||||
|  |         j['tags'] = [tag(t) for t in tags] | ||||||
|  | 
 | ||||||
|  |     return j | ||||||
|  | 
 | ||||||
|  | def tag(t): | ||||||
|  |     j = { | ||||||
|  |         'id': t.id, | ||||||
|  |         'name': t.name, | ||||||
|  |         'qualified_name': t.qualified_name(), | ||||||
|  |     } | ||||||
|  |     return j | ||||||
|  | @ -15,17 +15,20 @@ import time | ||||||
| import traceback | import traceback | ||||||
| import warnings | import warnings | ||||||
| 
 | 
 | ||||||
| sys.path.append('C:\\git\\else\\Bytestring'); import bytestring | import constants | ||||||
| sys.path.append('C:\\git\\else\\SpinalTap'); import spinal | import decorators | ||||||
|  | import helpers | ||||||
| 
 | 
 | ||||||
| VALID_TAG_CHARS = string.ascii_lowercase + string.digits + '_' | try: | ||||||
| MIN_TAG_NAME_LENGTH = 1 |     sys.path.append('C:\\git\\else\\Bytestring') | ||||||
| MAX_TAG_NAME_LENGTH = 32 |     sys.path.append('C:\\git\\else\\SpinalTap') | ||||||
| DEFAULT_ID_LENGTH = 12 |     import bytestring | ||||||
| DEFAULT_DBNAME = 'phototagger.db' |     import spinal | ||||||
| DEFAULT_THUMBDIR = '_etiquette\\site_thumbnails' | except ImportError: | ||||||
| THUMBNAIL_WIDTH = 400 |     # pip install | ||||||
| THUMBNAIL_HEIGHT = 400 |     # https://raw.githubusercontent.com/voussoir/else/master/_voussoirkit/voussoirkit.zip | ||||||
|  |     from vousoirkit import bytestring | ||||||
|  |     from vousoirkit import spinal | ||||||
| 
 | 
 | ||||||
| try: | try: | ||||||
|     ffmpeg = converter.Converter( |     ffmpeg = converter.Converter( | ||||||
|  | @ -40,18 +43,6 @@ logging.basicConfig(level=logging.DEBUG) | ||||||
| log = logging.getLogger(__name__) | log = logging.getLogger(__name__) | ||||||
| logging.getLogger("PIL.PngImagePlugin").setLevel(logging.WARNING) | logging.getLogger("PIL.PngImagePlugin").setLevel(logging.WARNING) | ||||||
| 
 | 
 | ||||||
| ADDITIONAL_MIMETYPES = { |  | ||||||
|     'srt': 'text', |  | ||||||
|     'mkv': 'video', |  | ||||||
| } |  | ||||||
| WARNING_MINMAX_INVALID = 'Field "{field}": "{value}" is not a valid request. Ignored.' |  | ||||||
| WARNING_MINMAX_OOO = 'Field "{field}": minimum "{min}" maximum "{max}" are out of order. Ignored.' |  | ||||||
| WARNING_NO_SUCH_TAG = 'Tag "{tag}" does not exist. Ignored.' |  | ||||||
| WARNING_ORDERBY_BADCOL = '"{column}" is not a sorting option. Ignored.' |  | ||||||
| WARNING_ORDERBY_BADSORTER = 'You can\'t order "{column}" by "{sorter}". Defaulting to descending.' |  | ||||||
| 
 |  | ||||||
| OPERATORS = {'(', ')', 'OR', 'AND', 'NOT'} |  | ||||||
| 
 |  | ||||||
| SQL_LASTID_COLUMNS = [ | SQL_LASTID_COLUMNS = [ | ||||||
|     'table', |     'table', | ||||||
|     'last_id', |     'last_id', | ||||||
|  | @ -107,10 +98,11 @@ SQL_SYN = {key:index for (index, key) in enumerate(SQL_SYN_COLUMNS)} | ||||||
| SQL_TAG = {key:index for (index, key) in enumerate(SQL_TAG_COLUMNS)} | SQL_TAG = {key:index for (index, key) in enumerate(SQL_TAG_COLUMNS)} | ||||||
| SQL_TAGGROUP = {key:index for (index, key) in enumerate(SQL_TAGGROUP_COLUMNS)} | SQL_TAGGROUP = {key:index for (index, key) in enumerate(SQL_TAGGROUP_COLUMNS)} | ||||||
| 
 | 
 | ||||||
| 
 | DATABASE_VERSION = 1 | ||||||
| DB_INIT = ''' | DB_INIT = ''' | ||||||
| PRAGMA count_changes = OFF; | PRAGMA count_changes = OFF; | ||||||
| PRAGMA cache_size = 10000; | PRAGMA cache_size = 10000; | ||||||
|  | PRAGMA user_version = {user_version}; | ||||||
| CREATE TABLE IF NOT EXISTS albums( | CREATE TABLE IF NOT EXISTS albums( | ||||||
|     id TEXT, |     id TEXT, | ||||||
|     title TEXT, |     title TEXT, | ||||||
|  | @ -183,27 +175,8 @@ CREATE INDEX IF NOT EXISTS index_tagsyn_name on tag_synonyms(name); | ||||||
| -- Tag-group relation | -- Tag-group relation | ||||||
| CREATE INDEX IF NOT EXISTS index_grouprel_parentid on tag_group_rel(parentid); | CREATE INDEX IF NOT EXISTS index_grouprel_parentid on tag_group_rel(parentid); | ||||||
| CREATE INDEX IF NOT EXISTS index_grouprel_memberid on tag_group_rel(memberid); | CREATE INDEX IF NOT EXISTS index_grouprel_memberid on tag_group_rel(memberid); | ||||||
| ''' | '''.format(user_version=DATABASE_VERSION) | ||||||
| 
 | 
 | ||||||
| def not_implemented(function): |  | ||||||
|     ''' |  | ||||||
|     Decorator to remember what needs doing. |  | ||||||
|     ''' |  | ||||||
|     warnings.warn('%s is not implemented' % function.__name__) |  | ||||||
|     return function |  | ||||||
| 
 |  | ||||||
| def time_me(function): |  | ||||||
|     ''' |  | ||||||
|     Decorator. After the function is run, print the elapsed time. |  | ||||||
|     ''' |  | ||||||
|     @functools.wraps(function) |  | ||||||
|     def timed_function(*args, **kwargs): |  | ||||||
|         start = time.time() |  | ||||||
|         result = function(*args, **kwargs) |  | ||||||
|         end = time.time() |  | ||||||
|         print('%s: %0.8f' % (function.__name__, end-start)) |  | ||||||
|         return result |  | ||||||
|     return timed_function |  | ||||||
| 
 | 
 | ||||||
| def _helper_extension(ext): | def _helper_extension(ext): | ||||||
|     ''' |     ''' | ||||||
|  | @ -220,8 +193,6 @@ def _helper_extension(ext): | ||||||
| 
 | 
 | ||||||
| def _helper_filenamefilter(subject, terms): | def _helper_filenamefilter(subject, terms): | ||||||
|     basename = subject.lower() |     basename = subject.lower() | ||||||
|     #print(basename) |  | ||||||
|     #print(terms) |  | ||||||
|     return all(term in basename for term in terms) |     return all(term in basename for term in terms) | ||||||
| 
 | 
 | ||||||
| def _helper_minmax(key, value, minimums, maximums): | def _helper_minmax(key, value, minimums, maximums): | ||||||
|  | @ -237,10 +208,10 @@ def _helper_minmax(key, value, minimums, maximums): | ||||||
|     try: |     try: | ||||||
|         (low, high) = hyphen_range(value) |         (low, high) = hyphen_range(value) | ||||||
|     except ValueError: |     except ValueError: | ||||||
|         warnings.warn(WARNING_MINMAX_INVALID.format(field=key, value=value)) |         warnings.warn(constants.WARNING_MINMAX_INVALID.format(field=key, value=value)) | ||||||
|         return |         return | ||||||
|     except OutOfOrder as e: |     except OutOfOrder as e: | ||||||
|         warnings.warn(WARNING_MINMAX_OOO.format(field=key, min=e.args[1], max=e.args[2])) |         warnings.warn(constants.WARNING_MINMAX_OOO.format(field=key, min=e.args[1], max=e.args[2])) | ||||||
|         return |         return | ||||||
|     if low is not None: |     if low is not None: | ||||||
|         minimums[key] = low |         minimums[key] = low | ||||||
|  | @ -278,13 +249,13 @@ def _helper_orderby(orderby): | ||||||
|         'random', |         'random', | ||||||
|     ] |     ] | ||||||
|     if not sortable: |     if not sortable: | ||||||
|         warnings.warn(WARNING_ORDERBY_BADCOL.format(column=column)) |         warnings.warn(constants.WARNING_ORDERBY_BADCOL.format(column=column)) | ||||||
|         return None |         return None | ||||||
|     if column == 'random': |     if column == 'random': | ||||||
|         column = 'RANDOM()' |         column = 'RANDOM()' | ||||||
| 
 | 
 | ||||||
|     if sorter not in ['desc', 'asc']: |     if sorter not in ['desc', 'asc']: | ||||||
|         warnings.warn(WARNING_ORDERBY_BADSORTER.format(column=column, sorter=sorter)) |         warnings.warn(constants.WARNING_ORDERBY_BADSORTER.format(column=column, sorter=sorter)) | ||||||
|         sorter = 'desc' |         sorter = 'desc' | ||||||
|     return (column, sorter) |     return (column, sorter) | ||||||
| 
 | 
 | ||||||
|  | @ -307,7 +278,7 @@ def _helper_setify(photodb, l, warn_bad_tags=False): | ||||||
|         except NoSuchTag: |         except NoSuchTag: | ||||||
|             if not warn_bad_tags: |             if not warn_bad_tags: | ||||||
|                 raise |                 raise | ||||||
|             warnings.warn(WARNING_NO_SUCH_TAG.format(tag=tag)) |             warnings.warn(constants.WARNING_NO_SUCH_TAG.format(tag=tag)) | ||||||
|             continue |             continue | ||||||
|         else: |         else: | ||||||
|             s.add(tag) |             s.add(tag) | ||||||
|  | @ -321,51 +292,12 @@ def _helper_unitconvert(value): | ||||||
|     if value is None: |     if value is None: | ||||||
|         return None |         return None | ||||||
|     if ':' in value: |     if ':' in value: | ||||||
|         return hms_to_seconds(value) |         return helpers.hms_to_seconds(value) | ||||||
|     elif all(c in '0123456789.' for c in value): |     elif all(c in '0123456789.' for c in value): | ||||||
|         return float(value) |         return float(value) | ||||||
|     else: |     else: | ||||||
|         return bytestring.parsebytes(value) |         return bytestring.parsebytes(value) | ||||||
| 
 | 
 | ||||||
| def chunk_sequence(sequence, chunk_length, allow_incomplete=True): |  | ||||||
|     ''' |  | ||||||
|     Given a sequence, divide it into sequences of length `chunk_length`. |  | ||||||
| 
 |  | ||||||
|     allow_incomplete: |  | ||||||
|         If True, allow the final chunk to be shorter if the |  | ||||||
|         given sequence is not an exact multiple of `chunk_length`. |  | ||||||
|         If False, the incomplete chunk will be discarded. |  | ||||||
|     ''' |  | ||||||
|     (complete, leftover) = divmod(len(sequence), chunk_length) |  | ||||||
|     if not allow_incomplete: |  | ||||||
|         leftover = 0 |  | ||||||
| 
 |  | ||||||
|     chunk_count = complete + min(leftover, 1) |  | ||||||
| 
 |  | ||||||
|     chunks = [] |  | ||||||
|     for x in range(chunk_count): |  | ||||||
|         left = chunk_length * x |  | ||||||
|         right = left + chunk_length |  | ||||||
|         chunks.append(sequence[left:right]) |  | ||||||
| 
 |  | ||||||
|     return chunks |  | ||||||
| 
 |  | ||||||
| def hms_to_seconds(hms): |  | ||||||
|     ''' |  | ||||||
|     Convert hh:mm:ss string to an integer seconds. |  | ||||||
|     ''' |  | ||||||
|     hms = hms.split(':') |  | ||||||
|     seconds = 0 |  | ||||||
|     if len(hms) == 3: |  | ||||||
|         seconds += int(hms[0])*3600 |  | ||||||
|         hms.pop(0) |  | ||||||
|     if len(hms) == 2: |  | ||||||
|         seconds += int(hms[0])*60 |  | ||||||
|         hms.pop(0) |  | ||||||
|     if len(hms) == 1: |  | ||||||
|         seconds += int(hms[0]) |  | ||||||
|     return seconds |  | ||||||
| 
 |  | ||||||
| def hyphen_range(s): | def hyphen_range(s): | ||||||
|     ''' |     ''' | ||||||
|     Given a string like '1-3', return ints (1, 3) representing lower |     Given a string like '1-3', return ints (1, 3) representing lower | ||||||
|  | @ -393,23 +325,10 @@ def hyphen_range(s): | ||||||
|         raise OutOfOrder(s, low, high) |         raise OutOfOrder(s, low, high) | ||||||
|     return low, high |     return low, high | ||||||
| 
 | 
 | ||||||
| def fit_into_bounds(image_width, image_height, frame_width, frame_height): |  | ||||||
|     ''' |  | ||||||
|     Given the w+h of the image and the w+h of the frame, |  | ||||||
|     return new w+h that fits the image into the frame |  | ||||||
|     while maintaining the aspect ratio. |  | ||||||
|     ''' |  | ||||||
|     ratio = min(frame_width/image_width, frame_height/image_height) |  | ||||||
| 
 |  | ||||||
|     new_width = int(image_width * ratio) |  | ||||||
|     new_height = int(image_height * ratio) |  | ||||||
| 
 |  | ||||||
|     return (new_width, new_height) |  | ||||||
| 
 |  | ||||||
| def get_mimetype(filepath): | def get_mimetype(filepath): | ||||||
|     extension = os.path.splitext(filepath)[1].replace('.', '') |     extension = os.path.splitext(filepath)[1].replace('.', '') | ||||||
|     if extension in ADDITIONAL_MIMETYPES: |     if extension in constants.ADDITIONAL_MIMETYPES: | ||||||
|         return ADDITIONAL_MIMETYPES[extension] |         return constants.ADDITIONAL_MIMETYPES[extension] | ||||||
|     mimetype = mimetypes.guess_type(filepath)[0] |     mimetype = mimetypes.guess_type(filepath)[0] | ||||||
|     if mimetype is not None: |     if mimetype is not None: | ||||||
|         mimetype = mimetype.split('/')[0] |         mimetype = mimetype.split('/')[0] | ||||||
|  | @ -424,12 +343,6 @@ def getnow(timestamp=True): | ||||||
|         return now.timestamp() |         return now.timestamp() | ||||||
|     return now |     return now | ||||||
| 
 | 
 | ||||||
| def is_xor(*args): |  | ||||||
|     ''' |  | ||||||
|     Return True if and only if one arg is truthy. |  | ||||||
|     ''' |  | ||||||
|     return [bool(a) for a in args].count(True) == 1 |  | ||||||
| 
 |  | ||||||
| def normalize_filepath(filepath): | def normalize_filepath(filepath): | ||||||
|     ''' |     ''' | ||||||
|     Remove some bad characters. |     Remove some bad characters. | ||||||
|  | @ -450,12 +363,12 @@ def normalize_tagname(tagname): | ||||||
|     tagname = tagname.lower() |     tagname = tagname.lower() | ||||||
|     tagname = tagname.replace('-', '_') |     tagname = tagname.replace('-', '_') | ||||||
|     tagname = tagname.replace(' ', '_') |     tagname = tagname.replace(' ', '_') | ||||||
|     tagname = (c for c in tagname if c in VALID_TAG_CHARS) |     tagname = (c for c in tagname if c in constants.VALID_TAG_CHARS) | ||||||
|     tagname = ''.join(tagname) |     tagname = ''.join(tagname) | ||||||
| 
 | 
 | ||||||
|     if len(tagname) < MIN_TAG_NAME_LENGTH: |     if len(tagname) < constants.MIN_TAG_NAME_LENGTH: | ||||||
|         raise TagTooShort(tagname) |         raise TagTooShort(tagname) | ||||||
|     if len(tagname) > MAX_TAG_NAME_LENGTH: |     if len(tagname) > constants.MAX_TAG_NAME_LENGTH: | ||||||
|         raise TagTooLong(tagname) |         raise TagTooLong(tagname) | ||||||
| 
 | 
 | ||||||
|     return tagname |     return tagname | ||||||
|  | @ -509,13 +422,13 @@ def searchfilter_expression(photo_tags, expression, frozen_children, warn_bad_ta | ||||||
|         if can_shortcircuit and token != ')': |         if can_shortcircuit and token != ')': | ||||||
|             continue |             continue | ||||||
| 
 | 
 | ||||||
|         if token not in OPERATORS: |         if token not in constants.EXPRESSION_OPERATORS: | ||||||
|             try: |             try: | ||||||
|                 token = normalize_tagname(token) |                 token = normalize_tagname(token) | ||||||
|                 value = any(option in photo_tags for option in frozen_children[token]) |                 value = any(option in photo_tags for option in frozen_children[token]) | ||||||
|             except KeyError: |             except KeyError: | ||||||
|                 if warn_bad_tags: |                 if warn_bad_tags: | ||||||
|                     warnings.warn(WARNING_NO_SUCH_TAG.format(tag=token)) |                     warnings.warn(constants.NO_SUCH_TAG.format(tag=token)) | ||||||
|                 else: |                 else: | ||||||
|                     raise NoSuchTag(token) |                     raise NoSuchTag(token) | ||||||
|                 return False |                 return False | ||||||
|  | @ -536,13 +449,17 @@ def searchfilter_expression(photo_tags, expression, frozen_children, warn_bad_ta | ||||||
|             has_operand = True |             has_operand = True | ||||||
|             continue |             continue | ||||||
| 
 | 
 | ||||||
|         if has_operand and ((operand_stack[-1] == 0 and token == 'AND') or (operand_stack[-1] == 1 and token == 'OR')): |         can_shortcircuit = ( | ||||||
|             can_shortcircuit = True |             has_operand and | ||||||
|  |             ( | ||||||
|  |                 (operand_stack[-1] == 0 and token == 'AND') or | ||||||
|  |                 (operand_stack[-1] == 1 and token == 'OR') | ||||||
|  |             ) | ||||||
|  |         ) | ||||||
|  |         if can_shortcircuit: | ||||||
|             if operator_stack and operator_stack[-1] == '(': |             if operator_stack and operator_stack[-1] == '(': | ||||||
|                 operator_stack.pop() |                 operator_stack.pop() | ||||||
|             continue |             continue | ||||||
|         else: |  | ||||||
|             can_shortcircuit = False |  | ||||||
| 
 | 
 | ||||||
|         operator_stack.append(token) |         operator_stack.append(token) | ||||||
|         #time.sleep(.3) |         #time.sleep(.3) | ||||||
|  | @ -636,7 +553,7 @@ def tag_export_stdout(tags, depth=0): | ||||||
|         if tag.parent() is None: |         if tag.parent() is None: | ||||||
|             print() |             print() | ||||||
| 
 | 
 | ||||||
| @time_me | @decorators.time_me | ||||||
| def tag_export_totally_flat(tags): | def tag_export_totally_flat(tags): | ||||||
|     result = {} |     result = {} | ||||||
|     for tag in tags: |     for tag in tags: | ||||||
|  | @ -1097,7 +1014,7 @@ class PDBTagMixin: | ||||||
|         ''' |         ''' | ||||||
|         Redirect to get_tag_by_id or get_tag_by_name after xor-checking the parameters. |         Redirect to get_tag_by_id or get_tag_by_name after xor-checking the parameters. | ||||||
|         ''' |         ''' | ||||||
|         if not is_xor(id, name): |         if not helpers.is_xor(id, name): | ||||||
|             raise XORException('One and only one of `id`, `name` can be passed.') |             raise XORException('One and only one of `id`, `name` can be passed.') | ||||||
| 
 | 
 | ||||||
|         if id is not None: |         if id is not None: | ||||||
|  | @ -1192,19 +1109,35 @@ class PhotoDB(PDBAlbumMixin, PDBPhotoMixin, PDBTagMixin): | ||||||
|         The `rename` method of Tag objects includes a parameter |         The `rename` method of Tag objects includes a parameter | ||||||
|         `apply_to_synonyms` if you do want them to follow. |         `apply_to_synonyms` if you do want them to follow. | ||||||
|     ''' |     ''' | ||||||
|     def __init__(self, databasename=DEFAULT_DBNAME, thumbnail_folder=DEFAULT_THUMBDIR, id_length=None): |     def __init__( | ||||||
|         if id_length is None: |             self, | ||||||
|             self.id_length = DEFAULT_ID_LENGTH |             databasename=constants.DEFAULT_DBNAME, | ||||||
|  |             thumbnail_folder=constants.DEFAULT_THUMBDIR, | ||||||
|  |             id_length=constants.DEFAULT_ID_LENGTH, | ||||||
|  |         ): | ||||||
|         self.databasename = databasename |         self.databasename = databasename | ||||||
|         self.database_abspath = os.path.abspath(databasename) |         self.database_abspath = os.path.abspath(databasename) | ||||||
|         self.thumbnail_folder = os.path.abspath(thumbnail_folder) |         existing_database = os.path.exists(databasename) | ||||||
|         os.makedirs(thumbnail_folder, exist_ok=True) |  | ||||||
|         self.sql = sqlite3.connect(databasename) |         self.sql = sqlite3.connect(databasename) | ||||||
|         self.cur = self.sql.cursor() |         self.cur = self.sql.cursor() | ||||||
|  |         if existing_database: | ||||||
|  |             self.cur.execute('PRAGMA user_version') | ||||||
|  |             existing_version = self.cur.fetchone()[0] | ||||||
|  |             if existing_version != DATABASE_VERSION: | ||||||
|  |                 message = constants.ERROR_DATABASE_OUTOFDATE | ||||||
|  |                 message = message.format(current=existing_version, new=DATABASE_VERSION) | ||||||
|  |                 log.critical(message) | ||||||
|  |                 raise SystemExit | ||||||
|  | 
 | ||||||
|         statements = DB_INIT.split(';') |         statements = DB_INIT.split(';') | ||||||
|         for statement in statements: |         for statement in statements: | ||||||
|             self.cur.execute(statement) |             self.cur.execute(statement) | ||||||
| 
 | 
 | ||||||
|  |         self.thumbnail_folder = os.path.abspath(thumbnail_folder) | ||||||
|  |         os.makedirs(thumbnail_folder, exist_ok=True) | ||||||
|  | 
 | ||||||
|  |         self.id_length = id_length | ||||||
|  | 
 | ||||||
|         self.on_commit_queue = [] |         self.on_commit_queue = [] | ||||||
|         self._cached_frozen_children = None |         self._cached_frozen_children = None | ||||||
| 
 | 
 | ||||||
|  | @ -1232,15 +1165,9 @@ class PhotoDB(PDBAlbumMixin, PDBPhotoMixin, PDBTagMixin): | ||||||
|         if not os.path.isdir(directory): |         if not os.path.isdir(directory): | ||||||
|             raise ValueError('Not a directory: %s' % directory) |             raise ValueError('Not a directory: %s' % directory) | ||||||
|         if exclude_directories is None: |         if exclude_directories is None: | ||||||
|             exclude_directories = [ |             exclude_directories = constants.DEFAULT_DIGEST_EXCLUDE_DIRS | ||||||
|                 '_site_thumbnails', |  | ||||||
|             ] |  | ||||||
|         if exclude_filenames is None: |         if exclude_filenames is None: | ||||||
|             exclude_filenames = [ |             exclude_filenames = constants.DEFAULT_DIGEST_EXCLUDE_FILES | ||||||
|                 DEFAULT_DBNAME, |  | ||||||
|                 'desktop.ini', |  | ||||||
|                 'thumbs.db' |  | ||||||
|             ] |  | ||||||
| 
 | 
 | ||||||
|         directory = spinal.str_to_fp(directory) |         directory = spinal.str_to_fp(directory) | ||||||
|         directory.correct_case() |         directory.correct_case() | ||||||
|  | @ -1306,15 +1233,9 @@ class PhotoDB(PDBAlbumMixin, PDBPhotoMixin, PDBTagMixin): | ||||||
|         if not os.path.isdir(directory): |         if not os.path.isdir(directory): | ||||||
|             raise ValueError('Not a directory: %s' % directory) |             raise ValueError('Not a directory: %s' % directory) | ||||||
|         if exclude_directories is None: |         if exclude_directories is None: | ||||||
|             exclude_directories = [ |             exclude_directories = constants.DEFAULT_DIGEST_EXCLUDE_DIRS | ||||||
|                 '_site_thumbnails', |  | ||||||
|             ] |  | ||||||
|         if exclude_filenames is None: |         if exclude_filenames is None: | ||||||
|             exclude_filenames = [ |             exclude_filenames = constants.DEFAULT_DIGEST_EXCLUDE_FILES | ||||||
|                 DEFAULT_DBNAME, |  | ||||||
|                 'desktop.ini', |  | ||||||
|                 'thumbs.db' |  | ||||||
|             ] |  | ||||||
| 
 | 
 | ||||||
|         directory = spinal.str_to_fp(directory) |         directory = spinal.str_to_fp(directory) | ||||||
|         generator = spinal.walk_generator( |         generator = spinal.walk_generator( | ||||||
|  | @ -1806,7 +1727,7 @@ class Photo(ObjectBase): | ||||||
|             log.debug('Committing - delete photo') |             log.debug('Committing - delete photo') | ||||||
|             self.photodb.commit() |             self.photodb.commit() | ||||||
| 
 | 
 | ||||||
|     @time_me |     @decorators.time_me | ||||||
|     def generate_thumbnail(self, commit=True, **special): |     def generate_thumbnail(self, commit=True, **special): | ||||||
|         ''' |         ''' | ||||||
|         special: |         special: | ||||||
|  | @ -1825,11 +1746,11 @@ class Photo(ObjectBase): | ||||||
|                 pass |                 pass | ||||||
|             else: |             else: | ||||||
|                 (width, height) = image.size |                 (width, height) = image.size | ||||||
|                 (new_width, new_height) = fit_into_bounds( |                 (new_width, new_height) = helpers.fit_into_bounds( | ||||||
|                     image_width=width, |                     image_width=width, | ||||||
|                     image_height=height, |                     image_height=height, | ||||||
|                     frame_width=THUMBNAIL_WIDTH, |                     frame_width=constants.THUMBNAIL_WIDTH, | ||||||
|                     frame_height=THUMBNAIL_HEIGHT, |                     frame_height=constants.THUMBNAIL_HEIGHT, | ||||||
|                 ) |                 ) | ||||||
|                 if new_width < width: |                 if new_width < width: | ||||||
|                     image = image.resize((new_width, new_height)) |                     image = image.resize((new_width, new_height)) | ||||||
|  | @ -1841,11 +1762,11 @@ class Photo(ObjectBase): | ||||||
|             probe = ffmpeg.probe(self.real_filepath) |             probe = ffmpeg.probe(self.real_filepath) | ||||||
|             try: |             try: | ||||||
|                 if probe.video: |                 if probe.video: | ||||||
|                     size = fit_into_bounds( |                     size = helpers.fit_into_bounds( | ||||||
|                         image_width=probe.video.video_width, |                         image_width=probe.video.video_width, | ||||||
|                         image_height=probe.video.video_height, |                         image_height=probe.video.video_height, | ||||||
|                         frame_width=THUMBNAIL_WIDTH, |                         frame_width=constants.THUMBNAIL_WIDTH, | ||||||
|                         frame_height=THUMBNAIL_HEIGHT, |                         frame_height=constants.THUMBNAIL_HEIGHT, | ||||||
|                     ) |                     ) | ||||||
|                     size = '%dx%d' % size |                     size = '%dx%d' % size | ||||||
|                     duration = probe.video.duration |                     duration = probe.video.duration | ||||||
|  | @ -1898,7 +1819,7 @@ class Photo(ObjectBase): | ||||||
|         return False |         return False | ||||||
| 
 | 
 | ||||||
|     def make_thumbnail_filepath(self): |     def make_thumbnail_filepath(self): | ||||||
|         chunked_id = chunk_sequence(self.id, 3) |         chunked_id = helpers.chunk_sequence(self.id, 3) | ||||||
|         basename = chunked_id[-1] |         basename = chunked_id[-1] | ||||||
|         folder = chunked_id[:-1] |         folder = chunked_id[:-1] | ||||||
|         folder = os.sep.join(folder) |         folder = os.sep.join(folder) | ||||||
|  | @ -1911,7 +1832,7 @@ class Photo(ObjectBase): | ||||||
|     def mimetype(self): |     def mimetype(self): | ||||||
|         return get_mimetype(self.real_filepath) |         return get_mimetype(self.real_filepath) | ||||||
| 
 | 
 | ||||||
|     @time_me |     @decorators.time_me | ||||||
|     def reload_metadata(self, commit=True): |     def reload_metadata(self, commit=True): | ||||||
|         ''' |         ''' | ||||||
|         Load the file's height, width, etc as appropriate for this type of file. |         Load the file's height, width, etc as appropriate for this type of file. | ||||||
|  |  | ||||||
|  | @ -25,15 +25,15 @@ | ||||||
|     {% else %} |     {% else %} | ||||||
|     <h3>Parent: <a href="/albums">Albums</a></h3> |     <h3>Parent: <a href="/albums">Albums</a></h3> | ||||||
|     {% endif %} |     {% endif %} | ||||||
|     {% if child_albums %} |     {% if album["sub_albums"] %} | ||||||
|     <h3>Sub-albums</h3> |     <h3>Sub-albums</h3> | ||||||
|     <ul> |     <ul> | ||||||
|         {% for album in child_albums %} |         {% for sub_album in album["sub_albums"] %} | ||||||
|         <li><a href="/album/{{album["id"]}}"> |         <li><a href="/album/{{sub_album["id"]}}"> | ||||||
|             {% if album["title"] %} |             {% if sub_album["title"] %} | ||||||
|             {{album["title"]}} |             {{sub_album["title"]}} | ||||||
|             {% else %} |             {% else %} | ||||||
|             {{album["id"]}} |             {{sub_album["id"]}} | ||||||
|             {% endif %}</a> |             {% endif %}</a> | ||||||
|         </li> |         </li> | ||||||
|         {% endfor %} |         {% endfor %} | ||||||
|  |  | ||||||
|  | @ -17,12 +17,12 @@ | ||||||
| {{header.make_header()}} | {{header.make_header()}} | ||||||
| <div id="content_body"> | <div id="content_body"> | ||||||
|     {% for album in albums %} |     {% for album in albums %} | ||||||
|     {% if album.title %} |     {% if album["title"] %} | ||||||
|     {% set title=album.id + " " + album.title %} |     {% set title=album["id"] + " " + album["title"] %} | ||||||
|     {% else %} |     {% else %} | ||||||
|     {% set title=album.id %} |     {% set title=album["id"] %} | ||||||
|     {% endif %} |     {% endif %} | ||||||
|     <a href="/album/{{album.id}}">{{title}}</a> |     <a href="/album/{{album["id"]}}">{{title}}</a> | ||||||
|     {% endfor %} |     {% endfor %} | ||||||
| </div> | </div> | ||||||
| </body> | </body> | ||||||
|  |  | ||||||
|  | @ -115,7 +115,7 @@ | ||||||
|             <li>Size: {{photo["bytestring"]}}</li> |             <li>Size: {{photo["bytestring"]}}</li> | ||||||
|         {% endif %} |         {% endif %} | ||||||
|         {% if photo["duration"] %} |         {% if photo["duration"] %} | ||||||
|             <li>Duration: {{photo["duration"]}}</li> |             <li>Duration: {{photo["duration_str"]}}</li> | ||||||
|         {% endif %} |         {% endif %} | ||||||
|             <li><a href="/file/{{photo["id"]}}.{{photo["extension"]}}?download=1">Download as {{photo["id"]}}.{{photo["extension"]}}</a></li> |             <li><a href="/file/{{photo["id"]}}.{{photo["extension"]}}?download=1">Download as {{photo["id"]}}.{{photo["extension"]}}</a></li> | ||||||
|             <li><a href="/file/{{photo["id"]}}.{{photo["extension"]}}?download=1&original_filename=1">Download as "{{photo["filename"]}}"</a></li> |             <li><a href="/file/{{photo["id"]}}.{{photo["extension"]}}?download=1&original_filename=1">Download as "{{photo["filename"]}}"</a></li> | ||||||
|  |  | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| {% set basics = | {% set thumbnails = | ||||||
|     { |     { | ||||||
|         "audio": "audio", |         "audio": "audio", | ||||||
|         "txt": "txt", |         "txt": "txt", | ||||||
|  | @ -15,12 +15,11 @@ | ||||||
|             src="/thumbnail/{{photo["id"]}}.jpg" |             src="/thumbnail/{{photo["id"]}}.jpg" | ||||||
|         {% else %} |         {% else %} | ||||||
|             {% set choice = |             {% set choice = | ||||||
|                 photo['extension'] if photo['extension'] in basics else |                 thumbnails.get(photo["extension"], | ||||||
|                 photo['mimetype'] if photo['mimetype'] in basics else |                 thumbnails.get(photo["mimetype"], | ||||||
|                 'other' |                 'other')) | ||||||
|             %} |             %} | ||||||
|             src="/static/basic_thumbnails/{{choice}}.png" |             src="/static/basic_thumbnails/{{choice}}.png" | ||||||
|              |  | ||||||
|         {% endif %} |         {% endif %} | ||||||
|         </a> |         </a> | ||||||
|     </div> |     </div> | ||||||
|  | @ -31,7 +30,7 @@ | ||||||
|             {{photo["width"]}}x{{photo["height"]}}, |             {{photo["width"]}}x{{photo["height"]}}, | ||||||
|         {% endif %} |         {% endif %} | ||||||
|         {% if photo["duration"] %} |         {% if photo["duration"] %} | ||||||
|             {{photo["duration"]}}, |             {{photo["duration_str"]}}, | ||||||
|         {% endif %} |         {% endif %} | ||||||
|         {{photo["bytestring"]}} |         {{photo["bytestring"]}} | ||||||
|         </span> |         </span> | ||||||
|  |  | ||||||
		Loading…
	
		Reference in a new issue