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