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 | ||||
| import functools | ||||
| import time | ||||
| import uuid | ||||
| 
 | ||||
| 
 | ||||
| def _generate_session_token(): | ||||
|     token = str(uuid.uuid4()) | ||||
|     #print('MAKE SESSION', token) | ||||
|     return token | ||||
| 
 | ||||
| def give_session_token(function): | ||||
|     @functools.wraps(function) | ||||
|     def wrapped(*args, **kwargs): | ||||
|  | @ -25,3 +28,23 @@ def give_session_token(function): | |||
| 
 | ||||
|         return ret | ||||
|     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 | ||||
| from flask import request | ||||
| import functools | ||||
| import json | ||||
| import math | ||||
| import mimetypes | ||||
| import os | ||||
| import random | ||||
|  | @ -13,6 +10,10 @@ import sys | |||
| import time | ||||
| import warnings | ||||
| 
 | ||||
| import constants | ||||
| import decorators | ||||
| import helpers | ||||
| import jsonify | ||||
| import phototagger | ||||
| 
 | ||||
| try: | ||||
|  | @ -32,23 +33,10 @@ site.config.update( | |||
|     TEMPLATES_AUTO_RELOAD=True, | ||||
| ) | ||||
| site.jinja_env.add_extension('jinja2.ext.do') | ||||
| #site.debug = True | ||||
| site.debug = True | ||||
| 
 | ||||
| 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): | ||||
|     notes = P.easybake(easybake_string) | ||||
|     notes = [{'action': action, 'tagname': tagname} for (action, tagname) in notes] | ||||
|  | @ -91,16 +70,6 @@ def delete_synonym(synonym): | |||
|     master_tag.remove_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): | ||||
|     dumped = json.dumps(j) | ||||
|     response = flask.Response(dumped, *args, **kwargs) | ||||
|  | @ -125,34 +94,6 @@ def P_tag(tagname): | |||
|     except phototagger.NoSuchTag as 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): | ||||
|     ''' | ||||
|     Range-enabled file sending. | ||||
|  | @ -208,7 +149,7 @@ def send_file(filepath): | |||
|     if request.method == 'HEAD': | ||||
|         outgoing_data = bytes() | ||||
|     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( | ||||
|         outgoing_data, | ||||
|  | @ -217,64 +158,6 @@ def send_file(filepath): | |||
|     ) | ||||
|     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('/') | ||||
| @give_session_token | ||||
| @decorators.give_session_token | ||||
| def root(): | ||||
|     motd = random.choice(MOTD_STRINGS) | ||||
|     motd = random.choice(constants.MOTD_STRINGS) | ||||
|     return flask.render_template('root.html', motd=motd) | ||||
| 
 | ||||
| 
 | ||||
| @site.route('/favicon.ico') | ||||
| @site.route('/favicon.png') | ||||
| def favicon(): | ||||
|     filename = os.path.join('static', 'favicon.png') | ||||
|     return flask.send_file(filename) | ||||
| 
 | ||||
| 
 | ||||
| def get_album_core(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 | ||||
| 
 | ||||
| @site.route('/album/<albumid>') | ||||
| @give_session_token | ||||
| @decorators.give_session_token | ||||
| def get_album_html(albumid): | ||||
|     album = get_album_core(albumid) | ||||
|     response = flask.render_template( | ||||
|         'album.html', | ||||
|         album=album, | ||||
|         child_albums=[jsonify_album(P_album(x)) for x in album['sub_albums']], | ||||
|         photos=album['photos'], | ||||
|     ) | ||||
|     return response | ||||
| 
 | ||||
| @site.route('/album/<albumid>.json') | ||||
| @give_session_token | ||||
| @decorators.give_session_token | ||||
| def get_album_json(albumid): | ||||
|     album = get_album_core(albumid) | ||||
|     return make_json_response(album) | ||||
| 
 | ||||
| 
 | ||||
| @site.route('/album/<albumid>.tar') | ||||
| def get_album_tar(albumid): | ||||
|     album = P_album(albumid) | ||||
|  | @ -326,21 +214,26 @@ def get_album_tar(albumid): | |||
|     outgoing_headers = {'Content-Type': 'application/octet-stream'} | ||||
|     return flask.Response(streamed_zip, headers=outgoing_headers) | ||||
| 
 | ||||
| @site.route('/albums') | ||||
| @give_session_token | ||||
| def get_albums_html(): | ||||
| 
 | ||||
| def get_albums_core(): | ||||
|     albums = P.get_albums() | ||||
|     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) | ||||
| 
 | ||||
| @site.route('/albums.json') | ||||
| @give_session_token | ||||
| @decorators.give_session_token | ||||
| def get_albums_json(): | ||||
|     albums = P.get_albums() | ||||
|     albums = [a for a in albums if a.parent() is None] | ||||
|     albums = [jsonify_album(album, minimal=True) for album in albums] | ||||
|     albums = get_albums_core() | ||||
|     return make_json_response(albums) | ||||
| 
 | ||||
| 
 | ||||
| @site.route('/file/<photoid>') | ||||
| def get_file(photoid): | ||||
|     requested_photoid = photoid | ||||
|  | @ -348,10 +241,10 @@ def get_file(photoid): | |||
|     photo = P.get_photo(photoid) | ||||
| 
 | ||||
|     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 = truthystring(use_original_filename) | ||||
|     use_original_filename = helpers.truthystring(use_original_filename) | ||||
| 
 | ||||
|     if do_download: | ||||
|         if use_original_filename: | ||||
|  | @ -368,25 +261,27 @@ def get_file(photoid): | |||
|     else: | ||||
|         return send_file(photo.real_filepath) | ||||
| 
 | ||||
| 
 | ||||
| def get_photo_core(photoid): | ||||
|     photo = P_photo(photoid) | ||||
|     photo = jsonify_photo(photo) | ||||
|     photo = jsonify.photo(photo) | ||||
|     return photo | ||||
| 
 | ||||
| @site.route('/photo/<photoid>', methods=['GET']) | ||||
| @give_session_token | ||||
| @decorators.give_session_token | ||||
| def get_photo_html(photoid): | ||||
|     photo = get_photo_core(photoid) | ||||
|     photo['tags'].sort(key=lambda x: x['qualified_name']) | ||||
|     return flask.render_template('photo.html', photo=photo) | ||||
| 
 | ||||
| @site.route('/photo/<photoid>.json', methods=['GET']) | ||||
| @give_session_token | ||||
| @decorators.give_session_token | ||||
| def get_photo_json(photoid): | ||||
|     photo = get_photo_core(photoid) | ||||
|     photo = make_json_response(photo) | ||||
|     return photo | ||||
| 
 | ||||
| 
 | ||||
| def get_search_core(): | ||||
|     #print(request.args) | ||||
| 
 | ||||
|  | @ -396,9 +291,9 @@ def get_search_core(): | |||
|     extension_not_string = request.args.get('extension_not', None) | ||||
|     mimetype_string = request.args.get('mimetype', None) | ||||
| 
 | ||||
|     extension_list = _helper_comma_split(extension_string) | ||||
|     extension_not_list = _helper_comma_split(extension_not_string) | ||||
|     mimetype_list = _helper_comma_split(mimetype_string) | ||||
|     extension_list = helpers.comma_split(extension_string) | ||||
|     extension_not_list = helpers.comma_split(extension_not_string) | ||||
|     mimetype_list = helpers.comma_split(mimetype_string) | ||||
| 
 | ||||
|     # LIMIT | ||||
|     limit = request.args.get('limit', '') | ||||
|  | @ -440,7 +335,7 @@ def get_search_core(): | |||
|     if has_tags == '': | ||||
|         has_tags = None | ||||
|     else: | ||||
|         has_tags = truthystring(has_tags) | ||||
|         has_tags = helpers.truthystring(has_tags) | ||||
| 
 | ||||
|     # MINMAXERS | ||||
|     area = request.args.get('area', None) | ||||
|  | @ -480,7 +375,7 @@ def get_search_core(): | |||
|     #print(search_kwargs) | ||||
|     with warnings.catch_warnings(record=True) as catcher: | ||||
|         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] | ||||
|     #print(warns) | ||||
| 
 | ||||
|  | @ -493,13 +388,14 @@ def get_search_core(): | |||
| 
 | ||||
|     # PREV-NEXT PAGE URLS | ||||
|     offset = offset or 0 | ||||
|     original_params = request.args.to_dict() | ||||
|     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 | ||||
|     else: | ||||
|         next_page_url = None | ||||
|     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 | ||||
|     else: | ||||
|         prev_page_url = None | ||||
|  | @ -520,7 +416,7 @@ def get_search_core(): | |||
|     return final_results | ||||
| 
 | ||||
| @site.route('/search') | ||||
| @give_session_token | ||||
| @decorators.give_session_token | ||||
| def get_search_html(): | ||||
|     search_results = get_search_core() | ||||
|     search_kwargs = search_results['search_kwargs'] | ||||
|  | @ -538,16 +434,17 @@ def get_search_html(): | |||
|     return response | ||||
| 
 | ||||
| @site.route('/search.json') | ||||
| @give_session_token | ||||
| @decorators.give_session_token | ||||
| def get_search_json(): | ||||
|     search_results = get_search_core() | ||||
|     search_kwargs = search_results['search_kwargs'] | ||||
|     qualname_map = search_results['qualname_map'] | ||||
|     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: | ||||
|         search_results.pop('qualname_map') | ||||
|     return make_json_response(j) | ||||
|     return make_json_response(search_results) | ||||
| 
 | ||||
| 
 | ||||
| @site.route('/static/<filename>') | ||||
| def get_static(filename): | ||||
|  | @ -556,20 +453,33 @@ def get_static(filename): | |||
|     filename = os.path.join('static', filename) | ||||
|     return flask.send_file(filename) | ||||
| 
 | ||||
| @site.route('/tags') | ||||
| @site.route('/tags/<specific_tag>') | ||||
| @give_session_token | ||||
| def get_tags(specific_tag=None): | ||||
| 
 | ||||
| def get_tags_core(specific_tag=None): | ||||
|     try: | ||||
|         tags = P.export_tags(phototagger.tag_export_easybake, specific_tag=specific_tag) | ||||
|     except phototagger.NoSuchTag: | ||||
|         flask.abort(404, 'That tag doesnt exist') | ||||
| 
 | ||||
|     tags = tags.split('\n') | ||||
|     tags = [t for t in tags if t != ''] | ||||
|     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) | ||||
| 
 | ||||
| @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>') | ||||
| def get_thumbnail(photoid): | ||||
|     photoid = photoid.split('.')[0] | ||||
|  | @ -580,9 +490,10 @@ def get_thumbnail(photoid): | |||
|         flask.abort(404, 'That file doesnt have a thumbnail') | ||||
|     return send_file(path) | ||||
| 
 | ||||
| 
 | ||||
| @site.route('/album/<albumid>', methods=['POST']) | ||||
| @site.route('/album/<albumid>.json', methods=['POST']) | ||||
| @give_session_token | ||||
| @decorators.give_session_token | ||||
| def post_edit_album(albumid): | ||||
|     ''' | ||||
|     Edit the album's title and description. | ||||
|  | @ -601,15 +512,16 @@ def post_edit_album(albumid): | |||
|             response = {'error': 'That tag doesnt exist', 'tagname': tag} | ||||
|             return make_json_response(response, status=404) | ||||
|         recursive = request.form.get('recursive', False) | ||||
|         recursive = truthystring(recursive) | ||||
|         recursive = helpers.truthystring(recursive) | ||||
|         album.add_tag_to_all(tag, nested_children=recursive) | ||||
|         response['action'] = action | ||||
|         response['tagname'] = tag.name | ||||
|         return make_json_response(response) | ||||
| 
 | ||||
| 
 | ||||
| @site.route('/photo/<photoid>', methods=['POST']) | ||||
| @site.route('/photo/<photoid>.json', methods=['POST']) | ||||
| @give_session_token | ||||
| @decorators.give_session_token | ||||
| def post_edit_photo(photoid): | ||||
|     ''' | ||||
|     Add and remove tags from photos. | ||||
|  | @ -642,8 +554,9 @@ def post_edit_photo(photoid): | |||
|     response['tagname'] = tag.name | ||||
|     return make_json_response(response) | ||||
| 
 | ||||
| 
 | ||||
| @site.route('/tags', methods=['POST']) | ||||
| @give_session_token | ||||
| @decorators.give_session_token | ||||
| def post_edit_tags(): | ||||
|     ''' | ||||
|     Create and delete tags and synonyms. | ||||
|  | @ -661,12 +574,12 @@ def post_edit_tags(): | |||
|         method = delete_tag | ||||
|     else: | ||||
|         status = 400 | ||||
|         response = {'error': ERROR_INVALID_ACTION} | ||||
|         response = {'error': constants.ERROR_INVALID_ACTION} | ||||
| 
 | ||||
|     if status == 200: | ||||
|         tag = request.form[action].strip() | ||||
|         if tag == '': | ||||
|             response = {'error': ERROR_NO_TAG_GIVEN} | ||||
|             response = {'error': constants.ERROR_NO_TAG_GIVEN} | ||||
|             status = 400 | ||||
| 
 | ||||
|     if status == 200: | ||||
|  | @ -675,11 +588,11 @@ def post_edit_tags(): | |||
|         try: | ||||
|             response = method(tag) | ||||
|         except phototagger.TagTooShort: | ||||
|             response = {'error': ERROR_TAG_TOO_SHORT, 'tagname': tag} | ||||
|             response = {'error': constants.ERROR_TAG_TOO_SHORT, 'tagname': tag} | ||||
|         except phototagger.CantSynonymSelf: | ||||
|             response = {'error': ERROR_SYNONYM_ITSELF, 'tagname': tag} | ||||
|             response = {'error': constants.ERROR_SYNONYM_ITSELF, 'tagname': tag} | ||||
|         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: | ||||
|             response = {'error': e.args[0], 'tagname': tag} | ||||
|         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 warnings | ||||
| 
 | ||||
| sys.path.append('C:\\git\\else\\Bytestring'); import bytestring | ||||
| sys.path.append('C:\\git\\else\\SpinalTap'); import spinal | ||||
| import constants | ||||
| import decorators | ||||
| import helpers | ||||
| 
 | ||||
| VALID_TAG_CHARS = string.ascii_lowercase + string.digits + '_' | ||||
| MIN_TAG_NAME_LENGTH = 1 | ||||
| MAX_TAG_NAME_LENGTH = 32 | ||||
| DEFAULT_ID_LENGTH = 12 | ||||
| DEFAULT_DBNAME = 'phototagger.db' | ||||
| DEFAULT_THUMBDIR = '_etiquette\\site_thumbnails' | ||||
| THUMBNAIL_WIDTH = 400 | ||||
| THUMBNAIL_HEIGHT = 400 | ||||
| try: | ||||
|     sys.path.append('C:\\git\\else\\Bytestring') | ||||
|     sys.path.append('C:\\git\\else\\SpinalTap') | ||||
|     import bytestring | ||||
|     import spinal | ||||
| except ImportError: | ||||
|     # pip install | ||||
|     # https://raw.githubusercontent.com/voussoir/else/master/_voussoirkit/voussoirkit.zip | ||||
|     from vousoirkit import bytestring | ||||
|     from vousoirkit import spinal | ||||
| 
 | ||||
| try: | ||||
|     ffmpeg = converter.Converter( | ||||
|  | @ -40,18 +43,6 @@ logging.basicConfig(level=logging.DEBUG) | |||
| log = logging.getLogger(__name__) | ||||
| 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 = [ | ||||
|     'table', | ||||
|     '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_TAGGROUP = {key:index for (index, key) in enumerate(SQL_TAGGROUP_COLUMNS)} | ||||
| 
 | ||||
| 
 | ||||
| DATABASE_VERSION = 1 | ||||
| DB_INIT = ''' | ||||
| PRAGMA count_changes = OFF; | ||||
| PRAGMA cache_size = 10000; | ||||
| PRAGMA user_version = {user_version}; | ||||
| CREATE TABLE IF NOT EXISTS albums( | ||||
|     id TEXT, | ||||
|     title TEXT, | ||||
|  | @ -183,27 +175,8 @@ CREATE INDEX IF NOT EXISTS index_tagsyn_name on tag_synonyms(name); | |||
| -- Tag-group relation | ||||
| 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); | ||||
| ''' | ||||
| '''.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): | ||||
|     ''' | ||||
|  | @ -220,8 +193,6 @@ def _helper_extension(ext): | |||
| 
 | ||||
| def _helper_filenamefilter(subject, terms): | ||||
|     basename = subject.lower() | ||||
|     #print(basename) | ||||
|     #print(terms) | ||||
|     return all(term in basename for term in terms) | ||||
| 
 | ||||
| def _helper_minmax(key, value, minimums, maximums): | ||||
|  | @ -237,10 +208,10 @@ def _helper_minmax(key, value, minimums, maximums): | |||
|     try: | ||||
|         (low, high) = hyphen_range(value) | ||||
|     except ValueError: | ||||
|         warnings.warn(WARNING_MINMAX_INVALID.format(field=key, value=value)) | ||||
|         warnings.warn(constants.WARNING_MINMAX_INVALID.format(field=key, value=value)) | ||||
|         return | ||||
|     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 | ||||
|     if low is not None: | ||||
|         minimums[key] = low | ||||
|  | @ -278,13 +249,13 @@ def _helper_orderby(orderby): | |||
|         'random', | ||||
|     ] | ||||
|     if not sortable: | ||||
|         warnings.warn(WARNING_ORDERBY_BADCOL.format(column=column)) | ||||
|         warnings.warn(constants.WARNING_ORDERBY_BADCOL.format(column=column)) | ||||
|         return None | ||||
|     if column == 'random': | ||||
|         column = 'RANDOM()' | ||||
| 
 | ||||
|     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' | ||||
|     return (column, sorter) | ||||
| 
 | ||||
|  | @ -307,7 +278,7 @@ def _helper_setify(photodb, l, warn_bad_tags=False): | |||
|         except NoSuchTag: | ||||
|             if not warn_bad_tags: | ||||
|                 raise | ||||
|             warnings.warn(WARNING_NO_SUCH_TAG.format(tag=tag)) | ||||
|             warnings.warn(constants.WARNING_NO_SUCH_TAG.format(tag=tag)) | ||||
|             continue | ||||
|         else: | ||||
|             s.add(tag) | ||||
|  | @ -321,51 +292,12 @@ def _helper_unitconvert(value): | |||
|     if value is None: | ||||
|         return None | ||||
|     if ':' in value: | ||||
|         return hms_to_seconds(value) | ||||
|         return helpers.hms_to_seconds(value) | ||||
|     elif all(c in '0123456789.' for c in value): | ||||
|         return float(value) | ||||
|     else: | ||||
|         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): | ||||
|     ''' | ||||
|     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) | ||||
|     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): | ||||
|     extension = os.path.splitext(filepath)[1].replace('.', '') | ||||
|     if extension in ADDITIONAL_MIMETYPES: | ||||
|         return ADDITIONAL_MIMETYPES[extension] | ||||
|     if extension in constants.ADDITIONAL_MIMETYPES: | ||||
|         return constants.ADDITIONAL_MIMETYPES[extension] | ||||
|     mimetype = mimetypes.guess_type(filepath)[0] | ||||
|     if mimetype is not None: | ||||
|         mimetype = mimetype.split('/')[0] | ||||
|  | @ -424,12 +343,6 @@ def getnow(timestamp=True): | |||
|         return now.timestamp() | ||||
|     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): | ||||
|     ''' | ||||
|     Remove some bad characters. | ||||
|  | @ -450,12 +363,12 @@ def normalize_tagname(tagname): | |||
|     tagname = tagname.lower() | ||||
|     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) | ||||
| 
 | ||||
|     if len(tagname) < MIN_TAG_NAME_LENGTH: | ||||
|     if len(tagname) < constants.MIN_TAG_NAME_LENGTH: | ||||
|         raise TagTooShort(tagname) | ||||
|     if len(tagname) > MAX_TAG_NAME_LENGTH: | ||||
|     if len(tagname) > constants.MAX_TAG_NAME_LENGTH: | ||||
|         raise TagTooLong(tagname) | ||||
| 
 | ||||
|     return tagname | ||||
|  | @ -509,13 +422,13 @@ def searchfilter_expression(photo_tags, expression, frozen_children, warn_bad_ta | |||
|         if can_shortcircuit and token != ')': | ||||
|             continue | ||||
| 
 | ||||
|         if token not in OPERATORS: | ||||
|         if token not in constants.EXPRESSION_OPERATORS: | ||||
|             try: | ||||
|                 token = normalize_tagname(token) | ||||
|                 value = any(option in photo_tags for option in frozen_children[token]) | ||||
|             except KeyError: | ||||
|                 if warn_bad_tags: | ||||
|                     warnings.warn(WARNING_NO_SUCH_TAG.format(tag=token)) | ||||
|                     warnings.warn(constants.NO_SUCH_TAG.format(tag=token)) | ||||
|                 else: | ||||
|                     raise NoSuchTag(token) | ||||
|                 return False | ||||
|  | @ -536,13 +449,17 @@ def searchfilter_expression(photo_tags, expression, frozen_children, warn_bad_ta | |||
|             has_operand = True | ||||
|             continue | ||||
| 
 | ||||
|         if has_operand and ((operand_stack[-1] == 0 and token == 'AND') or (operand_stack[-1] == 1 and token == 'OR')): | ||||
|             can_shortcircuit = True | ||||
|         can_shortcircuit = ( | ||||
|             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] == '(': | ||||
|                 operator_stack.pop() | ||||
|             continue | ||||
|         else: | ||||
|             can_shortcircuit = False | ||||
| 
 | ||||
|         operator_stack.append(token) | ||||
|         #time.sleep(.3) | ||||
|  | @ -636,7 +553,7 @@ def tag_export_stdout(tags, depth=0): | |||
|         if tag.parent() is None: | ||||
|             print() | ||||
| 
 | ||||
| @time_me | ||||
| @decorators.time_me | ||||
| def tag_export_totally_flat(tags): | ||||
|     result = {} | ||||
|     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. | ||||
|         ''' | ||||
|         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.') | ||||
| 
 | ||||
|         if id is not None: | ||||
|  | @ -1192,19 +1109,35 @@ class PhotoDB(PDBAlbumMixin, PDBPhotoMixin, PDBTagMixin): | |||
|         The `rename` method of Tag objects includes a parameter | ||||
|         `apply_to_synonyms` if you do want them to follow. | ||||
|     ''' | ||||
|     def __init__(self, databasename=DEFAULT_DBNAME, thumbnail_folder=DEFAULT_THUMBDIR, id_length=None): | ||||
|         if id_length is None: | ||||
|             self.id_length = DEFAULT_ID_LENGTH | ||||
|     def __init__( | ||||
|             self, | ||||
|             databasename=constants.DEFAULT_DBNAME, | ||||
|             thumbnail_folder=constants.DEFAULT_THUMBDIR, | ||||
|             id_length=constants.DEFAULT_ID_LENGTH, | ||||
|         ): | ||||
|         self.databasename = databasename | ||||
|         self.database_abspath = os.path.abspath(databasename) | ||||
|         self.thumbnail_folder = os.path.abspath(thumbnail_folder) | ||||
|         os.makedirs(thumbnail_folder, exist_ok=True) | ||||
|         existing_database = os.path.exists(databasename) | ||||
|         self.sql = sqlite3.connect(databasename) | ||||
|         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(';') | ||||
|         for statement in statements: | ||||
|             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._cached_frozen_children = None | ||||
| 
 | ||||
|  | @ -1232,15 +1165,9 @@ class PhotoDB(PDBAlbumMixin, PDBPhotoMixin, PDBTagMixin): | |||
|         if not os.path.isdir(directory): | ||||
|             raise ValueError('Not a directory: %s' % directory) | ||||
|         if exclude_directories is None: | ||||
|             exclude_directories = [ | ||||
|                 '_site_thumbnails', | ||||
|             ] | ||||
|             exclude_directories = constants.DEFAULT_DIGEST_EXCLUDE_DIRS | ||||
|         if exclude_filenames is None: | ||||
|             exclude_filenames = [ | ||||
|                 DEFAULT_DBNAME, | ||||
|                 'desktop.ini', | ||||
|                 'thumbs.db' | ||||
|             ] | ||||
|             exclude_filenames = constants.DEFAULT_DIGEST_EXCLUDE_FILES | ||||
| 
 | ||||
|         directory = spinal.str_to_fp(directory) | ||||
|         directory.correct_case() | ||||
|  | @ -1306,15 +1233,9 @@ class PhotoDB(PDBAlbumMixin, PDBPhotoMixin, PDBTagMixin): | |||
|         if not os.path.isdir(directory): | ||||
|             raise ValueError('Not a directory: %s' % directory) | ||||
|         if exclude_directories is None: | ||||
|             exclude_directories = [ | ||||
|                 '_site_thumbnails', | ||||
|             ] | ||||
|             exclude_directories = constants.DEFAULT_DIGEST_EXCLUDE_DIRS | ||||
|         if exclude_filenames is None: | ||||
|             exclude_filenames = [ | ||||
|                 DEFAULT_DBNAME, | ||||
|                 'desktop.ini', | ||||
|                 'thumbs.db' | ||||
|             ] | ||||
|             exclude_filenames = constants.DEFAULT_DIGEST_EXCLUDE_FILES | ||||
| 
 | ||||
|         directory = spinal.str_to_fp(directory) | ||||
|         generator = spinal.walk_generator( | ||||
|  | @ -1806,7 +1727,7 @@ class Photo(ObjectBase): | |||
|             log.debug('Committing - delete photo') | ||||
|             self.photodb.commit() | ||||
| 
 | ||||
|     @time_me | ||||
|     @decorators.time_me | ||||
|     def generate_thumbnail(self, commit=True, **special): | ||||
|         ''' | ||||
|         special: | ||||
|  | @ -1825,11 +1746,11 @@ class Photo(ObjectBase): | |||
|                 pass | ||||
|             else: | ||||
|                 (width, height) = image.size | ||||
|                 (new_width, new_height) = fit_into_bounds( | ||||
|                 (new_width, new_height) = helpers.fit_into_bounds( | ||||
|                     image_width=width, | ||||
|                     image_height=height, | ||||
|                     frame_width=THUMBNAIL_WIDTH, | ||||
|                     frame_height=THUMBNAIL_HEIGHT, | ||||
|                     frame_width=constants.THUMBNAIL_WIDTH, | ||||
|                     frame_height=constants.THUMBNAIL_HEIGHT, | ||||
|                 ) | ||||
|                 if new_width < width: | ||||
|                     image = image.resize((new_width, new_height)) | ||||
|  | @ -1841,11 +1762,11 @@ class Photo(ObjectBase): | |||
|             probe = ffmpeg.probe(self.real_filepath) | ||||
|             try: | ||||
|                 if probe.video: | ||||
|                     size = fit_into_bounds( | ||||
|                     size = helpers.fit_into_bounds( | ||||
|                         image_width=probe.video.video_width, | ||||
|                         image_height=probe.video.video_height, | ||||
|                         frame_width=THUMBNAIL_WIDTH, | ||||
|                         frame_height=THUMBNAIL_HEIGHT, | ||||
|                         frame_width=constants.THUMBNAIL_WIDTH, | ||||
|                         frame_height=constants.THUMBNAIL_HEIGHT, | ||||
|                     ) | ||||
|                     size = '%dx%d' % size | ||||
|                     duration = probe.video.duration | ||||
|  | @ -1898,7 +1819,7 @@ class Photo(ObjectBase): | |||
|         return False | ||||
| 
 | ||||
|     def make_thumbnail_filepath(self): | ||||
|         chunked_id = chunk_sequence(self.id, 3) | ||||
|         chunked_id = helpers.chunk_sequence(self.id, 3) | ||||
|         basename = chunked_id[-1] | ||||
|         folder = chunked_id[:-1] | ||||
|         folder = os.sep.join(folder) | ||||
|  | @ -1911,7 +1832,7 @@ class Photo(ObjectBase): | |||
|     def mimetype(self): | ||||
|         return get_mimetype(self.real_filepath) | ||||
| 
 | ||||
|     @time_me | ||||
|     @decorators.time_me | ||||
|     def reload_metadata(self, commit=True): | ||||
|         ''' | ||||
|         Load the file's height, width, etc as appropriate for this type of file. | ||||
|  |  | |||
|  | @ -25,15 +25,15 @@ | |||
|     {% else %} | ||||
|     <h3>Parent: <a href="/albums">Albums</a></h3> | ||||
|     {% endif %} | ||||
|     {% if child_albums %} | ||||
|     {% if album["sub_albums"] %} | ||||
|     <h3>Sub-albums</h3> | ||||
|     <ul> | ||||
|         {% for album in child_albums %} | ||||
|         <li><a href="/album/{{album["id"]}}"> | ||||
|             {% if album["title"] %} | ||||
|             {{album["title"]}} | ||||
|         {% for sub_album in album["sub_albums"] %} | ||||
|         <li><a href="/album/{{sub_album["id"]}}"> | ||||
|             {% if sub_album["title"] %} | ||||
|             {{sub_album["title"]}} | ||||
|             {% else %} | ||||
|             {{album["id"]}} | ||||
|             {{sub_album["id"]}} | ||||
|             {% endif %}</a> | ||||
|         </li> | ||||
|         {% endfor %} | ||||
|  |  | |||
|  | @ -17,12 +17,12 @@ | |||
| {{header.make_header()}} | ||||
| <div id="content_body"> | ||||
|     {% for album in albums %} | ||||
|     {% if album.title %} | ||||
|     {% set title=album.id + " " + album.title %} | ||||
|     {% if album["title"] %} | ||||
|     {% set title=album["id"] + " " + album["title"] %} | ||||
|     {% else %} | ||||
|     {% set title=album.id %} | ||||
|     {% set title=album["id"] %} | ||||
|     {% endif %} | ||||
|     <a href="/album/{{album.id}}">{{title}}</a> | ||||
|     <a href="/album/{{album["id"]}}">{{title}}</a> | ||||
|     {% endfor %} | ||||
| </div> | ||||
| </body> | ||||
|  |  | |||
|  | @ -115,7 +115,7 @@ | |||
|             <li>Size: {{photo["bytestring"]}}</li> | ||||
|         {% endif %} | ||||
|         {% if photo["duration"] %} | ||||
|             <li>Duration: {{photo["duration"]}}</li> | ||||
|             <li>Duration: {{photo["duration_str"]}}</li> | ||||
|         {% 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&original_filename=1">Download as "{{photo["filename"]}}"</a></li> | ||||
|  |  | |||
|  | @ -1,4 +1,4 @@ | |||
| {% set basics = | ||||
| {% set thumbnails = | ||||
|     { | ||||
|         "audio": "audio", | ||||
|         "txt": "txt", | ||||
|  | @ -15,12 +15,11 @@ | |||
|             src="/thumbnail/{{photo["id"]}}.jpg" | ||||
|         {% else %} | ||||
|             {% set choice = | ||||
|                 photo['extension'] if photo['extension'] in basics else | ||||
|                 photo['mimetype'] if photo['mimetype'] in basics else | ||||
|                 'other' | ||||
|                 thumbnails.get(photo["extension"], | ||||
|                 thumbnails.get(photo["mimetype"], | ||||
|                 'other')) | ||||
|             %} | ||||
|             src="/static/basic_thumbnails/{{choice}}.png" | ||||
|              | ||||
|         {% endif %} | ||||
|         </a> | ||||
|     </div> | ||||
|  | @ -31,7 +30,7 @@ | |||
|             {{photo["width"]}}x{{photo["height"]}}, | ||||
|         {% endif %} | ||||
|         {% if photo["duration"] %} | ||||
|             {{photo["duration"]}}, | ||||
|             {{photo["duration_str"]}}, | ||||
|         {% endif %} | ||||
|         {{photo["bytestring"]}} | ||||
|         </span> | ||||
|  |  | |||
		Loading…
	
		Reference in a new issue