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):
|
||||
|
@ -24,4 +27,24 @@ def give_session_token(function):
|
|||
ret.set_cookie('etiquette_session', value=token, max_age=60)
|
||||
|
||||
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
|
||||
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