checkpoint

This commit is contained in:
voussoir 2016-10-09 20:50:13 -07:00
parent 6c5580c1bc
commit 0b85c309f8
19 changed files with 1196 additions and 488 deletions

View file

@ -1,22 +1,29 @@
import distutils.util 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
import re import re
import requests import requests
import sys
import time
import uuid
import warnings import warnings
import phototagger
sys.path.append('C:\\git\\else\\Bytestring'); import bytestring
site = flask.Flask(__name__) site = flask.Flask(__name__)
site.config.update( site.config.update(
SEND_FILE_MAX_AGE_DEFAULT=180, SEND_FILE_MAX_AGE_DEFAULT=180,
TEMPLATES_AUTO_RELOAD=True, TEMPLATES_AUTO_RELOAD=True,
) )
site.jinja_env.add_extension('jinja2.ext.do')
print(os.getcwd())
import phototagger
P = phototagger.PhotoDB() P = phototagger.PhotoDB()
FILE_READ_CHUNK = 2 ** 20 FILE_READ_CHUNK = 2 ** 20
@ -32,17 +39,60 @@ ERROR_TAG_TOO_SHORT = 'Not enough valid chars'
ERROR_SYNONYM_ITSELF = 'Cant apply synonym to itself' ERROR_SYNONYM_ITSELF = 'Cant apply synonym to itself'
ERROR_NO_SUCH_TAG = 'Doesn\'t exist' ERROR_NO_SUCH_TAG = 'Doesn\'t exist'
####################################################################################################
####################################################################################################
####################################################################################################
####################################################################################################
def give_session_token(function):
@functools.wraps(function)
def wrapped(*args, **kwargs):
# Inject new token so the function doesn't know the difference
token = request.cookies.get('etiquette_session', None)
if not token:
token = generate_session_token()
request.cookies = dict(request.cookies)
request.cookies['etiquette_session'] = token
ret = function(*args, **kwargs)
# Send the token back to the client
if not isinstance(ret, flask.Response):
ret = flask.Response(ret)
ret.set_cookie('etiquette_session', value=token, max_age=60)
return ret
return wrapped
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 edit_params(original, modifications): def edit_params(original, modifications):
new_params = original.to_dict() new_params = original.to_dict()
new_params.update(modifications) new_params.update(modifications)
if not new_params: if not new_params:
return '' return ''
keep_params = {}
new_params = ['%s=%s' % (k, v) for (k, v) in new_params.items() if v] new_params = ['%s=%s' % (k, v) for (k, v) in new_params.items() if v]
new_params = '&'.join(new_params) new_params = '&'.join(new_params)
new_params = '?' + new_params new_params = '?' + new_params
return new_params return new_params
def generate_session_token():
token = str(uuid.uuid4())
#print('MAKE SESSION', token)
return token
def make_json_response(j, *args, **kwargs):
dumped = json.dumps(j)
response = flask.Response(dumped, *args, **kwargs)
response.headers['Content-Type'] = 'application/json;charset=utf-8'
return response
def P_album(albumid): def P_album(albumid):
try: try:
return P.get_album(albumid) return P.get_album(albumid)
@ -80,6 +130,7 @@ def read_filebytes(filepath, range_min, range_max):
sent_amount = 0 sent_amount = 0
with f: with f:
while sent_amount < range_span: while sent_amount < range_span:
print(sent_amount)
chunk = f.read(FILE_READ_CHUNK) chunk = f.read(FILE_READ_CHUNK)
if len(chunk) == 0: if len(chunk) == 0:
break break
@ -91,6 +142,11 @@ def send_file(filepath):
''' '''
Range-enabled file sending. Range-enabled file sending.
''' '''
try:
file_size = os.path.getsize(filepath)
except FileNotFoundError:
flask.abort(404)
outgoing_headers = {} outgoing_headers = {}
mimetype = mimetypes.guess_type(filepath)[0] mimetype = mimetypes.guess_type(filepath)[0]
if mimetype is not None: if mimetype is not None:
@ -98,202 +154,54 @@ def send_file(filepath):
mimetype += '; charset=utf-8' mimetype += '; charset=utf-8'
outgoing_headers['Content-Type'] = mimetype outgoing_headers['Content-Type'] = mimetype
if 'range' not in request.headers: if 'range' in request.headers:
response = flask.make_response(flask.send_file(filepath)) desired_range = request.headers['range'].lower()
for (k, v) in outgoing_headers.items(): desired_range = desired_range.split('bytes=')[-1]
response.headers[k] = v
return response
try: int_helper = lambda x: int(x) if x.isdigit() else None
file_size = os.path.getsize(filepath) if '-' in desired_range:
except FileNotFoundError: (desired_min, desired_max) = desired_range.split('-')
flask.abort(404) range_min = int_helper(desired_min)
range_max = int_helper(desired_max)
else:
range_min = int_helper(desired_range)
desired_range = request.headers['range'].lower() if range_min is None:
desired_range = desired_range.split('bytes=')[-1] range_min = 0
if range_max is None:
range_max = file_size
inthelper = lambda x: int(x) if x.isdigit() else None # because ranges are 0-indexed
if '-' in desired_range: range_max = min(range_max, file_size - 1)
(desired_min, desired_max) = desired_range.split('-') range_min = max(range_min, 0)
range_min = inthelper(desired_min)
range_max = inthelper(desired_max) range_header = 'bytes {min}-{max}/{outof}'.format(
min=range_min,
max=range_max,
outof=file_size,
)
outgoing_headers['Content-Range'] = range_header
status = 206
else: else:
range_min = inthelper(desired_range) range_max = file_size - 1
if range_min is None:
range_min = 0 range_min = 0
if range_max is None: status = 200
range_max = file_size
# because ranges are 0-indexed
range_max = min(range_max, file_size - 1)
range_min = max(range_min, 0)
range_header = 'bytes {min}-{max}/{outof}'.format(
min=range_min,
max=range_max,
outof=file_size,
)
outgoing_headers['Content-Range'] = range_header
outgoing_headers['Accept-Ranges'] = 'bytes' outgoing_headers['Accept-Ranges'] = 'bytes'
outgoing_headers['Content-Length'] = (range_max - range_min) + 1 outgoing_headers['Content-Length'] = (range_max - range_min) + 1
outgoing_data = read_filebytes(filepath, range_min=range_min, range_max=range_max) if request.method == 'HEAD':
outgoing_data = bytes()
else:
outgoing_data = read_filebytes(filepath, range_min=range_min, range_max=range_max)
response = flask.Response( response = flask.Response(
outgoing_data, outgoing_data,
status=206, status=status,
headers=outgoing_headers, headers=outgoing_headers,
) )
return response return response
@site.route('/')
def root():
motd = random.choice(MOTD_STRINGS)
return flask.render_template('root.html', motd=motd)
@site.route('/album/<albumid>')
def get_album(albumid):
album = P_album(albumid)
response = flask.render_template(
'album.html',
album=album,
child_albums=album.children(),
photos=album.photos()
)
return response
@site.route('/file/<photoid>')
def get_file(photoid):
requested_photoid = photoid
photoid = photoid.split('.')[0]
photo = P.get_photo(photoid)
do_download = request.args.get('download', False)
do_download = truthystring(do_download)
use_original_filename = request.args.get('original_filename', False)
use_original_filename = truthystring(use_original_filename)
if do_download:
if use_original_filename:
download_as = photo.basename
else:
download_as = photo.id + '.' + photo.extension
# Sorry, but otherwise the attachment filename gets terminated
#download_as = download_as.replace(';', '-')
download_as = download_as.replace('"', '\\"')
response = flask.make_response(send_file(photo.real_filepath))
response.headers['Content-Disposition'] = 'attachment; filename="%s"' % download_as
return response
else:
return send_file(photo.real_filepath)
@site.route('/albums')
def get_albums():
albums = P.get_albums()
albums = [a for a in albums if a.parent() is None]
return flask.render_template('albums.html', albums=albums)
@site.route('/photo/<photoid>', methods=['GET'])
def get_photo(photoid):
photo = P_photo(photoid)
tags = photo.tags()
tags.sort(key=lambda x: x.qualified_name())
return flask.render_template('photo.html', photo=photo, tags=tags)
@site.route('/tags')
@site.route('/tags/<specific_tag>')
def get_tags(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 flask.render_template('tags.html', tags=tags)
@site.route('/thumbnail/<photoid>')
def get_thumbnail(photoid):
photoid = photoid.split('.')[0]
photo = P_photo(photoid)
if photo.thumbnail:
path = photo.thumbnail
else:
flask.abort(404, 'That file doesnt have a thumbnail')
return send_file(path)
@site.route('/photo/<photoid>', methods=['POST'])
def edit_photo(photoid):
print(request.form)
response = {}
photo = P_photo(photoid)
if 'add_tag' in request.form:
action = 'add_tag'
method = photo.add_tag
elif 'remove_tag' in request.form:
action = 'remove_tag'
method = photo.remove_tag
else:
flask.abort(400, 'Invalid action')
tag = request.form[action].strip()
if tag == '':
flask.abort(400, 'No tag supplied')
try:
tag = P.get_tag(tag)
except phototagger.NoSuchTag:
return flask.Response('{"error": "That tag doesnt exist", "tagname":"%s"}'%tag, status=404)
method(tag)
response['action'] = action
response['tagid'] = tag.id
response['tagname'] = tag.name
return json.dumps(response)
@site.route('/tags', methods=['POST'])
def edit_tags():
print(request.form)
status = 200
if 'create_tag' in request.form:
action = 'create_tag'
method = create_tag
elif 'delete_tag_synonym' in request.form:
action = 'delete_tag_synonym'
method = delete_synonym
elif 'delete_tag' in request.form:
action = 'delete_tag'
method = delete_tag
else:
response = {'error': ERROR_INVALID_ACTION}
if status == 200:
status = 400
tag = request.form[action].strip()
if tag == '':
response = {'error': ERROR_NO_TAG_GIVEN}
try:
response = method(tag)
except phototagger.TagTooShort:
response = {'error': ERROR_TAG_TOO_SHORT, 'tagname': tag}
except phototagger.CantSynonymSelf:
response = {'error': ERROR_SYNONYM_ITSELF, 'tagname': tag}
except phototagger.NoSuchTag as e:
response = {'error': ERROR_NO_SUCH_TAG, 'tagname': tag}
except ValueError as e:
response = {'error': e.args[0], 'tagname': tag}
else:
status = 200
response = json.dumps(response)
response = flask.Response(response, status=status)
return response
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]
@ -320,24 +228,170 @@ 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}
####################################################################################################
####################################################################################################
####################################################################################################
####################################################################################################
@site.route('/search') def jsonify_album(album, minimal=False):
def search(): 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 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 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 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
####################################################################################################
####################################################################################################
####################################################################################################
####################################################################################################
@site.route('/')
@give_session_token
def root():
motd = random.choice(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)
return album
@site.route('/album/<albumid>')
@give_session_token
def get_album_html(albumid):
album = get_album_core(albumid)
response = flask.render_template(
'album.html',
album=album,
child_albums=album['sub_albums'],
photos=album['photos'],
)
return response
@site.route('/album/<albumid>')
@give_session_token
def get_album_json(albumid):
album = get_album_core(albumid)
return make_json_response(album)
@site.route('/albums')
@give_session_token
def get_albums():
albums = P.get_albums()
albums = [a for a in albums if a.parent() is None]
return flask.render_template('albums.html', albums=albums)
@site.route('/file/<photoid>')
def get_file(photoid):
requested_photoid = photoid
photoid = photoid.split('.')[0]
photo = P.get_photo(photoid)
do_download = request.args.get('download', False)
do_download = truthystring(do_download)
use_original_filename = request.args.get('original_filename', False)
use_original_filename = truthystring(use_original_filename)
if do_download:
if use_original_filename:
download_as = photo.basename
else:
download_as = photo.id + '.' + photo.extension
## Sorry, but otherwise the attachment filename gets terminated
#download_as = download_as.replace(';', '-')
download_as = download_as.replace('"', '\\"')
response = flask.make_response(send_file(photo.real_filepath))
response.headers['Content-Disposition'] = 'attachment; filename="%s"' % download_as
return response
else:
return send_file(photo.real_filepath)
def get_photo_core(photoid):
photo = P_photo(photoid)
photo = jsonify_photo(photo)
return photo
@site.route('/photo/<photoid>', methods=['GET'])
@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
def get_photo_json(photoid):
photo = get_photo_core(photoid)
photo = make_json_response(photo)
return photo
def get_search_core():
print(request.args) print(request.args)
def comma_split_helper(s):
s = s.replace(' ', ',')
s = [x.strip() for x in s.split(',')]
s = [x for x in s if x]
return s
# EXTENSION # EXTENSION
extension_string = request.args.get('extension', '') extension_string = request.args.get('extension', None)
extension_not_string = request.args.get('extension_not', '') extension_not_string = request.args.get('extension_not', None)
mimetype_string = request.args.get('mimetype', '') mimetype_string = request.args.get('mimetype', None)
extension_list = comma_split_helper(extension_string) extension_list = _helper_comma_split(extension_string)
extension_not_list = comma_split_helper(extension_not_string) extension_not_list = _helper_comma_split(extension_not_string)
mimetype_list = comma_split_helper(mimetype_string) mimetype_list = _helper_comma_split(mimetype_string)
# LIMIT # LIMIT
limit = request.args.get('limit', '') limit = request.args.get('limit', '')
@ -387,7 +441,7 @@ def search():
height = request.args.get('height', None) height = request.args.get('height', None)
ratio = request.args.get('ratio', None) ratio = request.args.get('ratio', None)
bytes = request.args.get('bytes', None) bytes = request.args.get('bytes', None)
length = request.args.get('length', None) duration = request.args.get('duration', None)
created = request.args.get('created', None) created = request.args.get('created', None)
# These are in a dictionary so I can pass them to the page template. # These are in a dictionary so I can pass them to the page template.
@ -397,7 +451,7 @@ def search():
'height': height, 'height': height,
'ratio': ratio, 'ratio': ratio,
'bytes': bytes, 'bytes': bytes,
'length': length, 'duration': duration,
'created': created, 'created': created,
'extension': extension_list, 'extension': extension_list,
@ -418,14 +472,16 @@ def search():
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]
warns = [str(warning.message) for warning in catcher] warns = [str(warning.message) for warning in catcher]
print(warns) print(warns)
# TAGS ON THIS PAGE
total_tags = set() total_tags = set()
for photo in photos: for photo in photos:
total_tags.update(photo.tags()) for tag in photo['tags']:
for tag in total_tags: total_tags.add(tag['qualified_name'])
tag._cached_qualname = qualname_map[tag.name] total_tags = sorted(total_tags)
total_tags = sorted(total_tags, key=lambda x: x._cached_qualname)
# PREV-NEXT PAGE URLS # PREV-NEXT PAGE URLS
offset = offset or 0 offset = offset or 0
@ -443,23 +499,149 @@ def search():
search_kwargs['extension'] = extension_string search_kwargs['extension'] = extension_string
search_kwargs['extension_not'] = extension_not_string search_kwargs['extension_not'] = extension_not_string
search_kwargs['mimetype'] = mimetype_string search_kwargs['mimetype'] = mimetype_string
final_results = {
'next_page_url': next_page_url,
'prev_page_url': prev_page_url,
'photos': photos,
'total_tags': total_tags,
'warns': warns,
'search_kwargs': search_kwargs,
'qualname_map': qualname_map,
}
return final_results
@site.route('/search')
@give_session_token
def get_search_html():
search_results = get_search_core()
search_kwargs = search_results['search_kwargs']
qualname_map = search_results['qualname_map']
response = flask.render_template( response = flask.render_template(
'search.html', 'search.html',
photos=photos, next_page_url=search_results['next_page_url'],
search_kwargs=search_kwargs, prev_page_url=search_results['prev_page_url'],
total_tags=total_tags, photos=search_results['photos'],
prev_page_url=prev_page_url,
next_page_url=next_page_url,
qualname_map=json.dumps(qualname_map), qualname_map=json.dumps(qualname_map),
warns=warns, search_kwargs=search_kwargs,
total_tags=search_results['total_tags'],
warns=search_results['warns'],
) )
return response return response
@site.route('/search.json')
@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)
if not include_qualname_map:
search_results.pop('qualname_map')
return make_json_response(j)
@site.route('/static/<filename>') @site.route('/static/<filename>')
def get_resource(filename): def get_static(filename):
print(filename) filename = filename.replace('\\', os.sep)
return flask.send_file('.\\static\\%s' % filename) filename = filename.replace('/', os.sep)
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):
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 flask.render_template('tags.html', tags=tags)
@site.route('/thumbnail/<photoid>')
def get_thumbnail(photoid):
photoid = photoid.split('.')[0]
photo = P_photo(photoid)
if photo.thumbnail:
path = photo.thumbnail
else:
flask.abort(404, 'That file doesnt have a thumbnail')
return send_file(path)
@site.route('/photo/<photoid>', methods=['POST'])
@give_session_token
def post_edit_photo(photoid):
print(request.form)
response = {}
photo = P_photo(photoid)
if 'add_tag' in request.form:
action = 'add_tag'
method = photo.add_tag
elif 'remove_tag' in request.form:
action = 'remove_tag'
method = photo.remove_tag
else:
flask.abort(400, 'Invalid action')
tag = request.form[action].strip()
if tag == '':
flask.abort(400, 'No tag supplied')
try:
tag = P.get_tag(tag)
except phototagger.NoSuchTag:
return flask.Response('{"error": "That tag doesnt exist", "tagname":"%s"}'%tag, status=404)
method(tag)
response['action'] = action
response['tagid'] = tag.id
response['tagname'] = tag.name
return json.dumps(response)
@site.route('/tags', methods=['POST'])
@give_session_token
def post_edit_tags():
print(request.form)
status = 200
if 'create_tag' in request.form:
action = 'create_tag'
method = create_tag
elif 'delete_tag_synonym' in request.form:
action = 'delete_tag_synonym'
method = delete_synonym
elif 'delete_tag' in request.form:
action = 'delete_tag'
method = delete_tag
else:
response = {'error': ERROR_INVALID_ACTION}
if status == 200:
status = 400
tag = request.form[action].strip()
if tag == '':
response = {'error': ERROR_NO_TAG_GIVEN}
try:
response = method(tag)
except phototagger.TagTooShort:
response = {'error': ERROR_TAG_TOO_SHORT, 'tagname': tag}
except phototagger.CantSynonymSelf:
response = {'error': ERROR_SYNONYM_ITSELF, 'tagname': tag}
except phototagger.NoSuchTag as e:
response = {'error': ERROR_NO_SUCH_TAG, 'tagname': tag}
except ValueError as e:
response = {'error': e.args[0], 'tagname': tag}
else:
status = 200
response = json.dumps(response)
response = flask.Response(response, status=status)
return response
if __name__ == '__main__': if __name__ == '__main__':
site.run(threaded=True) site.run(threaded=True)

View file

@ -27,10 +27,13 @@ DEFAULT_THUMBDIR = '_site_thumbnails'
THUMBNAIL_WIDTH = 400 THUMBNAIL_WIDTH = 400
THUMBNAIL_HEIGHT = 400 THUMBNAIL_HEIGHT = 400
ffmpeg = converter.Converter( try:
ffmpeg_path='C:\\software\\ffmpeg\\bin\\ffmpeg.exe', ffmpeg = converter.Converter(
ffprobe_path='C:\\software\\ffmpeg\\bin\\ffprobe.exe', ffmpeg_path='C:\\software\\ffmpeg\\bin\\ffmpeg.exe',
) ffprobe_path='C:\\software\\ffmpeg\\bin\\ffprobe.exe',
)
except converter.ffmpeg.FFMpegError:
ffmpeg = None
logging.basicConfig(level=logging.DEBUG) logging.basicConfig(level=logging.DEBUG)
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -40,8 +43,8 @@ ADDITIONAL_MIMETYPES = {
'srt': 'text', 'srt': 'text',
'mkv': 'video', 'mkv': 'video',
} }
WARNING_MINMAX_INVALID = 'Field {field}: "{value}" is not a valid request. Ignored.' 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_MINMAX_OOO = 'Field "{field}": minimum "{min}" maximum "{max}" are out of order. Ignored.'
WARNING_NO_SUCH_TAG = 'Tag "{tag}" does not exist. Ignored.' WARNING_NO_SUCH_TAG = 'Tag "{tag}" does not exist. Ignored.'
WARNING_ORDERBY_BADCOL = '"{column}" is not a sorting option. Ignored.' WARNING_ORDERBY_BADCOL = '"{column}" is not a sorting option. Ignored.'
WARNING_ORDERBY_BADSORTER = 'You can\'t order "{column}" by "{sorter}". Defaulting to descending.' WARNING_ORDERBY_BADSORTER = 'You can\'t order "{column}" by "{sorter}". Defaulting to descending.'
@ -66,7 +69,7 @@ SQL_PHOTO_COLUMNS = [
'height', 'height',
'ratio', 'ratio',
'area', 'area',
'length', 'duration',
'bytes', 'bytes',
'created', 'created',
'thumbnail', 'thumbnail',
@ -79,7 +82,6 @@ SQL_SYN_COLUMNS = [
'name', 'name',
'master', 'master',
] ]
SQL_ALBUMPHOTO_COLUMNS = [ SQL_ALBUMPHOTO_COLUMNS = [
'albumid', 'albumid',
'photoid', 'photoid',
@ -119,7 +121,7 @@ CREATE TABLE IF NOT EXISTS photos(
height INT, height INT,
ratio REAL, ratio REAL,
area INT, area INT,
length INT, duration INT,
bytes INT, bytes INT,
created INT, created INT,
thumbnail TEXT thumbnail TEXT
@ -176,8 +178,30 @@ 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);
''' '''
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):
'''
When searching, this function normalizes the list of permissible extensions.
'''
if isinstance(ext, str): if isinstance(ext, str):
ext = [ext] ext = [ext]
if ext is None: if ext is None:
@ -188,6 +212,10 @@ def _helper_extension(ext):
return ext return ext
def _helper_minmax(key, value, minimums, maximums): def _helper_minmax(key, value, minimums, maximums):
'''
When searching, this function dissects a hyphenated range string
and inserts the correct k:v pair into both minimums and maximums.
'''
if value is None: if value is None:
return return
if isinstance(value, (int, float)): if isinstance(value, (int, float)):
@ -207,6 +235,10 @@ def _helper_minmax(key, value, minimums, maximums):
maximums[key] = high maximums[key] = high
def _helper_orderby(orderby): def _helper_orderby(orderby):
'''
When searching, this function ensures that the user has entered a valid orderby
query, and normalizes the query text.
'''
orderby = orderby.lower().strip() orderby = orderby.lower().strip()
if orderby == '': if orderby == '':
return None return None
@ -227,7 +259,7 @@ def _helper_orderby(orderby):
'height', 'height',
'ratio', 'ratio',
'area', 'area',
'length', 'duration',
'bytes', 'bytes',
'created', 'created',
'random', 'random',
@ -244,6 +276,11 @@ def _helper_orderby(orderby):
return (column, sorter) return (column, sorter)
def _helper_setify(photodb, l, warn_bad_tags=False): def _helper_setify(photodb, l, warn_bad_tags=False):
'''
When searching, this function converts the list of tag strings that the user
requested into Tag objects. If a tag doesn't exist we'll either raise an exception
or just issue a warning.
'''
if l is None: if l is None:
return set() return set()
@ -263,6 +300,18 @@ def _helper_setify(photodb, l, warn_bad_tags=False):
s.add(tag) s.add(tag)
return s return s
def _helper_unitconvert(value):
'''
When parsing hyphenated ranges, this function is used to convert
strings like "1k" to 1024 and "1:00" to 60.
'''
if value is None:
return None
if ':' in value:
return hms_to_seconds(value)
else:
return bytestring.parsebytes(value)
def chunk_sequence(sequence, chunk_length, allow_incomplete=True): def chunk_sequence(sequence, chunk_length, allow_incomplete=True):
''' '''
Given a sequence, divide it into sequences of length `chunk_length`. Given a sequence, divide it into sequences of length `chunk_length`.
@ -286,25 +335,45 @@ def chunk_sequence(sequence, chunk_length, allow_incomplete=True):
return chunks 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 floats (1.0, 3.0) representing lower Given a string like '1-3', return ints (1, 3) representing lower
and upper bounds. and upper bounds.
Supports bytestring.parsebytes and hh:mm:ss format.
''' '''
s = s.strip() s = s.strip()
s = s.replace(' ', '')
if not s: if not s:
return (None, None) return (None, None)
pattern = r'^(\d*\.?\d*)-(\d*\.?\d*)$' parts = s.split('-')
try: parts = [part.strip() or None for part in parts]
match = re.search(pattern, s) if len(parts) == 1:
low = match.group(1) low = parts[0]
high = match.group(2)
except AttributeError:
low = float(s)
high = None high = None
elif len(parts) == 2:
(low, high) = parts
else: else:
low = float(low) if low.strip() else None raise ValueError('Too many hyphens')
high = float(high) if high.strip() else None
low = _helper_unitconvert(low)
high = _helper_unitconvert(high)
if low is not None and high is not None and low > high: if low is not None and high is not None and low > high:
raise OutOfOrder(s, low, high) raise OutOfOrder(s, low, high)
return low, high return low, high
@ -349,7 +418,7 @@ def is_xor(*args):
def normalize_filepath(filepath): def normalize_filepath(filepath):
''' '''
Remove some bad characters Remove some bad characters.
''' '''
filepath = filepath.replace('<', '') filepath = filepath.replace('<', '')
filepath = filepath.replace('>', '') filepath = filepath.replace('>', '')
@ -375,13 +444,6 @@ def normalize_tagname(tagname):
return tagname return tagname
def not_implemented(function):
'''
Decorator for keeping track of which functions still need to be filled out.
'''
warnings.warn('%s is not implemented' % function.__name__)
return function
def operate(operand_stack, operator_stack): def operate(operand_stack, operator_stack):
#print('before:', operand_stack, operator_stack) #print('before:', operand_stack, operator_stack)
operator = operator_stack.pop() operator = operator_stack.pop()
@ -488,7 +550,8 @@ def searchfilter_must_may_forbid(photo_tags, tag_musts, tag_mays, tag_forbids, f
return True return True
def select(sql, query, bindings=[]): def select(sql, query, bindings=None):
bindings = bindings or []
cursor = sql.cursor() cursor = sql.cursor()
cursor.execute(query, bindings) cursor.execute(query, bindings)
while True: while True:
@ -556,6 +619,7 @@ def tag_export_stdout(tags, depth=0):
if tag.parent() is None: if tag.parent() is None:
print() print()
@time_me
def tag_export_totally_flat(tags): def tag_export_totally_flat(tags):
result = {} result = {}
for tag in tags: for tag in tags:
@ -566,18 +630,6 @@ def tag_export_totally_flat(tags):
result[synonym] = children result[synonym] = children
return result return result
def time_me(function):
@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
tag_export_totally_flat = time_me(tag_export_totally_flat)
#################################################################################################### ####################################################################################################
#################################################################################################### ####################################################################################################
@ -765,7 +817,7 @@ class PDBPhotoMixin:
data[SQL_PHOTO['height']] = None data[SQL_PHOTO['height']] = None
data[SQL_PHOTO['area']] = None data[SQL_PHOTO['area']] = None
data[SQL_PHOTO['ratio']] = None data[SQL_PHOTO['ratio']] = None
data[SQL_PHOTO['length']] = None data[SQL_PHOTO['duration']] = None
data[SQL_PHOTO['thumbnail']] = None data[SQL_PHOTO['thumbnail']] = None
self.cur.execute('INSERT INTO photos VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', data) self.cur.execute('INSERT INTO photos VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', data)
@ -782,7 +834,6 @@ class PDBPhotoMixin:
if commit: if commit:
log.debug('Commiting - new_photo') log.debug('Commiting - new_photo')
self.sql.commit()
return photo return photo
def search( def search(
@ -792,7 +843,7 @@ class PDBPhotoMixin:
height=None, height=None,
ratio=None, ratio=None,
bytes=None, bytes=None,
length=None, duration=None,
created=None, created=None,
extension=None, extension=None,
@ -811,12 +862,12 @@ class PDBPhotoMixin:
): ):
''' '''
PHOTO PROPERTISE PHOTO PROPERTISE
area, width, height, ratio, bytes, length: area, width, height, ratio, bytes, duration:
A hyphen_range string representing min and max. Or just a number for lower bound. A hyphen_range string representing min and max. Or just a number for lower bound.
TAGS AND FILTERS TAGS AND FILTERS
created: created:
A hyphen_range string respresenting min and max. Or jjust a number for lower bound. A hyphen_range string respresenting min and max. Or just a number for lower bound.
extension: extension:
A string or list of strings of acceptable file extensions. A string or list of strings of acceptable file extensions.
@ -854,7 +905,7 @@ class PDBPhotoMixin:
Otherwise, a NoSuchTag exception would be raised. Otherwise, a NoSuchTag exception would be raised.
limit: limit:
The maximum number of *successful* results to yield. The maximum number of *successful* results to yield.
offset: offset:
How many *successful* results to skip before we start yielding. How many *successful* results to skip before we start yielding.
@ -868,11 +919,12 @@ class PDBPhotoMixin:
maximums = {} maximums = {}
minimums = {} minimums = {}
_helper_minmax('area', area, minimums, maximums) _helper_minmax('area', area, minimums, maximums)
_helper_minmax('created', created, minimums, maximums)
_helper_minmax('width', width, minimums, maximums) _helper_minmax('width', width, minimums, maximums)
_helper_minmax('height', height, minimums, maximums) _helper_minmax('height', height, minimums, maximums)
_helper_minmax('ratio', ratio, minimums, maximums) _helper_minmax('ratio', ratio, minimums, maximums)
_helper_minmax('bytes', bytes, minimums, maximums) _helper_minmax('bytes', bytes, minimums, maximums)
_helper_minmax('length', length, minimums, maximums) _helper_minmax('duration', duration, minimums, maximums)
orderby = orderby or [] orderby = orderby or []
extension = _helper_extension(extension) extension = _helper_extension(extension)
@ -907,7 +959,14 @@ class PDBPhotoMixin:
# EVERY tag in the db is a key, and the value is a list of ALL ITS NESTED CHILDREN. # EVERY tag in the db is a key, and the value is a list of ALL ITS NESTED CHILDREN.
# This representation is memory inefficient, but it is faster than repeated # This representation is memory inefficient, but it is faster than repeated
# database lookups # database lookups
frozen_children = self.export_tags(tag_export_totally_flat) is_must_may_forbid = bool(tag_musts or tag_mays or tag_forbids)
is_tagsearch = is_must_may_forbid or tag_expression
if is_tagsearch:
if self._cached_frozen_children:
frozen_children = self._cached_frozen_children
else:
frozen_children = self.export_tags(tag_export_totally_flat)
self._cached_frozen_children = frozen_children
photos_received = 0 photos_received = 0
for fetch in generator: for fetch in generator:
@ -932,7 +991,7 @@ class PDBPhotoMixin:
#print('Failed minimums') #print('Failed minimums')
continue continue
if (has_tags is not None) or tag_musts or tag_mays or tag_forbids or tag_expression: if (has_tags is not None) or is_tagsearch:
photo_tags = photo.tags() photo_tags = photo.tags()
if has_tags is False and len(photo_tags) > 0: if has_tags is False and len(photo_tags) > 0:
@ -946,7 +1005,7 @@ class PDBPhotoMixin:
if tag_expression: if tag_expression:
if not searchfilter_expression(photo_tags, tag_expression, frozen_children, warn_bad_tags): if not searchfilter_expression(photo_tags, tag_expression, frozen_children, warn_bad_tags):
continue continue
else: elif is_must_may_forbid:
if not searchfilter_must_may_forbid(photo_tags, tag_musts, tag_mays, tag_forbids, frozen_children): if not searchfilter_must_may_forbid(photo_tags, tag_musts, tag_mays, tag_forbids, frozen_children):
continue continue
@ -956,7 +1015,7 @@ class PDBPhotoMixin:
if limit is not None and photos_received >= limit: if limit is not None and photos_received >= limit:
break break
photos_received += 1 photos_received += 1
yield photo yield photo
@ -1031,7 +1090,9 @@ class PDBTagMixin:
pass pass
else: else:
raise TagExists(tagname) raise TagExists(tagname)
tagid = self.generate_id('tags') tagid = self.generate_id('tags')
self._cached_frozen_children = None
self.cur.execute('INSERT INTO tags VALUES(?, ?)', [tagid, tagname]) self.cur.execute('INSERT INTO tags VALUES(?, ?)', [tagid, tagname])
if commit: if commit:
log.debug('Commiting - new_tag') log.debug('Commiting - new_tag')
@ -1047,7 +1108,7 @@ class PhotoDB(PDBAlbumMixin, PDBPhotoMixin, PDBTagMixin):
albums: albums:
Rows represent the inclusion of a photo in an album Rows represent the inclusion of a photo in an album
photos: photos:
Rows represent image files on the local disk. Rows represent image files on the local disk.
Entries contain a unique ID, the image's filepath, and metadata Entries contain a unique ID, the image's filepath, and metadata
like dimensions and filesize. like dimensions and filesize.
@ -1088,9 +1149,14 @@ class PhotoDB(PDBAlbumMixin, PDBPhotoMixin, PDBTagMixin):
for statement in statements: for statement in statements:
self.cur.execute(statement) self.cur.execute(statement)
self._cached_frozen_children = None
def __repr__(self): def __repr__(self):
return 'PhotoDB(databasename={dbname})'.format(dbname=repr(self.databasename)) return 'PhotoDB(databasename={dbname})'.format(dbname=repr(self.databasename))
def _uncache(self):
self._cached_frozen_children = None
def digest_directory(self, directory, exclude_directories=None, exclude_filenames=None, commit=True): def digest_directory(self, directory, exclude_directories=None, exclude_filenames=None, commit=True):
''' '''
Create an album, and add the directory's contents to it. Create an album, and add the directory's contents to it.
@ -1138,7 +1204,14 @@ class PhotoDB(PDBAlbumMixin, PDBPhotoMixin, PDBTagMixin):
self.sql.commit() self.sql.commit()
return album return album
def digest_new_files(self, directory, exclude_directories=None, exclude_filenames=None, commit=True): def digest_new_files(
self,
directory,
exclude_directories=None,
exclude_filenames=None,
recurse=True,
commit=True
):
''' '''
Walk the directory and add new files as Photos. Walk the directory and add new files as Photos.
Does NOT create or modify any albums like `digest_directory` does. Does NOT create or modify any albums like `digest_directory` does.
@ -1161,6 +1234,7 @@ class PhotoDB(PDBAlbumMixin, PDBPhotoMixin, PDBTagMixin):
directory, directory,
exclude_directories=exclude_directories, exclude_directories=exclude_directories,
exclude_filenames=exclude_filenames, exclude_filenames=exclude_filenames,
recurse=recurse,
yield_style='flat', yield_style='flat',
) )
for filepath in generator: for filepath in generator:
@ -1171,7 +1245,10 @@ class PhotoDB(PDBAlbumMixin, PDBPhotoMixin, PDBTagMixin):
pass pass
else: else:
continue continue
photo = self.new_photo(filepath) photo = self.new_photo(filepath, commit=False)
if commit:
log.debug('Committing - digest_new_files')
self.sql.commit()
def easybake(self, string): def easybake(self, string):
@ -1229,7 +1306,6 @@ class PhotoDB(PDBAlbumMixin, PDBPhotoMixin, PDBTagMixin):
output_notes.append(note) output_notes.append(note)
else: else:
tag_parts = tag.split('.') tag_parts = tag.split('.')
print('wtf')
tags = [create_or_get(t) for t in tag_parts] tags = [create_or_get(t) for t in tag_parts]
for (higher, lower) in zip(tags, tags[1:]): for (higher, lower) in zip(tags, tags[1:]):
try: try:
@ -1271,7 +1347,7 @@ class PhotoDB(PDBAlbumMixin, PDBPhotoMixin, PDBTagMixin):
else: else:
# Use database value # Use database value
new_id_int = int(fetch[SQL_LASTID['last_id']]) + 1 new_id_int = int(fetch[SQL_LASTID['last_id']]) + 1
new_id = str(new_id_int).rjust(self.id_length, '0') new_id = str(new_id_int).rjust(self.id_length, '0')
if do_insert: if do_insert:
self.cur.execute('INSERT INTO id_numbers VALUES(?, ?)', [table, new_id]) self.cur.execute('INSERT INTO id_numbers VALUES(?, ?)', [table, new_id])
@ -1367,6 +1443,7 @@ class GroupableMixin:
that_group = self.group_getter(id=fetch[SQL_TAGGROUP['parentid']]) that_group = self.group_getter(id=fetch[SQL_TAGGROUP['parentid']])
raise GroupExists('%s already in group %s' % (member.name, that_group.name)) raise GroupExists('%s already in group %s' % (member.name, that_group.name))
self.photodb._cached_frozen_children = None
self.photodb.cur.execute('INSERT INTO tag_group_rel VALUES(?, ?)', [self.id, member.id]) self.photodb.cur.execute('INSERT INTO tag_group_rel VALUES(?, ?)', [self.id, member.id])
if commit: if commit:
log.debug('Commiting - add to group') log.debug('Commiting - add to group')
@ -1394,6 +1471,7 @@ class GroupableMixin:
If True, all children will be deleted. If True, all children will be deleted.
Otherwise they'll just be raised up one level. Otherwise they'll just be raised up one level.
''' '''
self.photodb._cached_frozen_children = None
if delete_children: if delete_children:
for child in self.children(): for child in self.children():
child.delete(delete_children=delete_children, commit=False) child.delete(delete_children=delete_children, commit=False)
@ -1412,6 +1490,7 @@ class GroupableMixin:
# Note that this part comes after the deletion of children to prevent issues of recursion. # Note that this part comes after the deletion of children to prevent issues of recursion.
self.photodb.cur.execute('DELETE FROM tag_group_rel WHERE memberid == ?', [self.id]) self.photodb.cur.execute('DELETE FROM tag_group_rel WHERE memberid == ?', [self.id])
if commit: if commit:
log.debug('Committing - delete tag')
self.photodb.sql.commit() self.photodb.sql.commit()
def parent(self): def parent(self):
@ -1435,7 +1514,6 @@ class GroupableMixin:
if not isinstance(group, type(self)): if not isinstance(group, type(self)):
raise TypeError('Group must also be %s' % type(self)) raise TypeError('Group must also be %s' % type(self))
print('what')
if self == group: if self == group:
raise ValueError('Cant join self') raise ValueError('Cant join self')
@ -1446,8 +1524,10 @@ class GroupableMixin:
''' '''
Leave the current group and become independent. Leave the current group and become independent.
''' '''
self.photodb._cached_frozen_children = None
self.photodb.cur.execute('DELETE FROM tag_group_rel WHERE memberid == ?', [self.id]) self.photodb.cur.execute('DELETE FROM tag_group_rel WHERE memberid == ?', [self.id])
if commit: if commit:
log.debug('Committing - leave group')
self.photodb.sql.commit() self.photodb.sql.commit()
def walk_children(self): def walk_children(self):
@ -1478,6 +1558,7 @@ class Album(ObjectBase, GroupableMixin):
return return
self.photodb.cur.execute('INSERT INTO album_photo_rel VALUES(?, ?)', [self.id, photo.id]) self.photodb.cur.execute('INSERT INTO album_photo_rel VALUES(?, ?)', [self.id, photo.id])
if commit: if commit:
log.debug('Committing - add photo to album')
self.photodb.sql.commit() self.photodb.sql.commit()
def add_tag_to_all(self, tag, nested_children=True): def add_tag_to_all(self, tag, nested_children=True):
@ -1495,6 +1576,7 @@ class Album(ObjectBase, GroupableMixin):
self.photodb.cur.execute('DELETE FROM albums WHERE id == ?', [self.id]) self.photodb.cur.execute('DELETE FROM albums WHERE id == ?', [self.id])
self.photodb.cur.execute('DELETE FROM album_photo_rel WHERE albumid == ?', [self.id]) self.photodb.cur.execute('DELETE FROM album_photo_rel WHERE albumid == ?', [self.id])
if commit: if commit:
log.debug('Committing - delete album')
self.photodb.sql.commit() self.photodb.sql.commit()
def edit(self, title=None, description=None, commit=True): def edit(self, title=None, description=None, commit=True):
@ -1509,6 +1591,7 @@ class Album(ObjectBase, GroupableMixin):
self.title = title self.title = title
self.description = description self.description = description
if commit: if commit:
log.debug('Committing - edit album')
self.photodb.sql.commit() self.photodb.sql.commit()
def has_photo(self, photo): def has_photo(self, photo):
@ -1553,7 +1636,7 @@ class Photo(ObjectBase):
''' '''
A PhotoDB entry containing information about an image file. A PhotoDB entry containing information about an image file.
Photo objects cannot exist without a corresponding PhotoDB object, because Photo objects cannot exist without a corresponding PhotoDB object, because
Photos are not the actual image data, just the database entry. Photos are not the actual image data, just the database entry.
''' '''
def __init__(self, photodb, row_tuple): def __init__(self, photodb, row_tuple):
self.photodb = photodb self.photodb = photodb
@ -1568,6 +1651,7 @@ class Photo(ObjectBase):
self.ratio = row_tuple[SQL_PHOTO['ratio']] self.ratio = row_tuple[SQL_PHOTO['ratio']]
self.area = row_tuple[SQL_PHOTO['area']] self.area = row_tuple[SQL_PHOTO['area']]
self.bytes = row_tuple[SQL_PHOTO['bytes']] self.bytes = row_tuple[SQL_PHOTO['bytes']]
self.duration = row_tuple[SQL_PHOTO['duration']]
self.created = row_tuple[SQL_PHOTO['created']] self.created = row_tuple[SQL_PHOTO['created']]
self.thumbnail = row_tuple[SQL_PHOTO['thumbnail']] self.thumbnail = row_tuple[SQL_PHOTO['thumbnail']]
self.basename = self.real_filepath.split(os.sep)[-1] self.basename = self.real_filepath.split(os.sep)[-1]
@ -1598,6 +1682,7 @@ class Photo(ObjectBase):
log.debug('Applying tag {tag:s} to photo {pho:s}'.format(tag=tag, pho=self)) log.debug('Applying tag {tag:s} to photo {pho:s}'.format(tag=tag, pho=self))
self.photodb.cur.execute('INSERT INTO photo_tag_rel VALUES(?, ?)', [self.id, tag.id]) self.photodb.cur.execute('INSERT INTO photo_tag_rel VALUES(?, ?)', [self.id, tag.id])
if commit: if commit:
log.debug('Committing - add photo tag')
self.photodb.sql.commit() self.photodb.sql.commit()
def albums(self): def albums(self):
@ -1625,6 +1710,7 @@ class Photo(ObjectBase):
self.photodb.cur.execute('DELETE FROM photo_tag_rel WHERE photoid == ?', [self.id]) self.photodb.cur.execute('DELETE FROM photo_tag_rel WHERE photoid == ?', [self.id])
self.photodb.cur.execute('DELETE FROM album_photo_rel WHERE photoid == ?', [self.id]) self.photodb.cur.execute('DELETE FROM album_photo_rel WHERE photoid == ?', [self.id])
if commit: if commit:
log.debug('Committing - delete photo')
self.photodb.sql.commit() self.photodb.sql.commit()
@time_me @time_me
@ -1659,7 +1745,7 @@ class Photo(ObjectBase):
image.save(hopeful_filepath, quality=50) image.save(hopeful_filepath, quality=50)
return_filepath = hopeful_filepath return_filepath = hopeful_filepath
elif mime == 'video': elif mime == 'video' and ffmpeg:
print('video') print('video')
probe = ffmpeg.probe(self.real_filepath) probe = ffmpeg.probe(self.real_filepath)
try: try:
@ -1691,6 +1777,7 @@ class Photo(ObjectBase):
self.thumbnail = return_filepath self.thumbnail = return_filepath
if commit: if commit:
log.debug('Committing - generate thumbnail')
self.photodb.sql.commit() self.photodb.sql.commit()
return self.thumbnail return self.thumbnail
@ -1729,7 +1816,7 @@ class Photo(ObjectBase):
self.height = None self.height = None
self.area = None self.area = None
self.ratio = None self.ratio = None
self.length = None self.duration = None
mime = self.mimetype() mime = self.mimetype()
if mime == 'image': if mime == 'image':
@ -1742,11 +1829,11 @@ class Photo(ObjectBase):
image.close() image.close()
log.debug('Loaded image data for {photo:r}'.format(photo=self)) log.debug('Loaded image data for {photo:r}'.format(photo=self))
elif mime == 'video': elif mime == 'video' and ffmpeg:
try: try:
probe = ffmpeg.probe(self.real_filepath) probe = ffmpeg.probe(self.real_filepath)
if probe and probe.video: if probe and probe.video:
self.length = probe.video.duration self.duration = probe.video.duration
self.width = probe.video.video_width self.width = probe.video.video_width
self.height = probe.video.video_height self.height = probe.video.video_height
except: except:
@ -1756,7 +1843,7 @@ class Photo(ObjectBase):
try: try:
probe = ffmpeg.probe(self.real_filepath) probe = ffmpeg.probe(self.real_filepath)
if probe and probe.audio: if probe and probe.audio:
self.length = probe.audio.duration self.duration = probe.audio.duration
except: except:
traceback.print_exc() traceback.print_exc()
@ -1764,10 +1851,11 @@ class Photo(ObjectBase):
self.area = self.width * self.height self.area = self.width * self.height
self.ratio = round(self.width / self.height, 2) self.ratio = round(self.width / self.height, 2)
self.photodb.cur.execute('UPDATE photos SET width=?, height=?, area=?, ratio=?, length=?, bytes=? WHERE id==?', self.photodb.cur.execute('UPDATE photos SET width=?, height=?, area=?, ratio=?, duration=?, bytes=? WHERE id==?',
[self.width, self.height, self.area, self.ratio, self.length, self.bytes, self.id] [self.width, self.height, self.area, self.ratio, self.duration, self.bytes, self.id],
) )
if commit: if commit:
log.debug('Committing - reload metadata')
self.photodb.sql.commit() self.photodb.sql.commit()
def remove_tag(self, tag, commit=True): def remove_tag(self, tag, commit=True):
@ -1781,6 +1869,7 @@ class Photo(ObjectBase):
[self.id, tag.id] [self.id, tag.id]
) )
if commit: if commit:
log.debug('Committing - remove photo tag')
self.photodb.sql.commit() self.photodb.sql.commit()
def tags(self): def tags(self):
@ -1809,12 +1898,13 @@ class Tag(ObjectBase, GroupableMixin):
self.id = row_tuple[SQL_TAG['id']] self.id = row_tuple[SQL_TAG['id']]
self.name = row_tuple[SQL_TAG['name']] self.name = row_tuple[SQL_TAG['name']]
self.group_getter = self.photodb.get_tag self.group_getter = self.photodb.get_tag
self._cached_qualified_name = None
def __eq__(self, other): def __eq__(self, other):
if isinstance(other, str): if isinstance(other, str):
return self.name == other return self.name == other
elif isinstance(other, Tag): elif isinstance(other, Tag):
return self.id == other.id return self.id == other.id and self.name == other.name
else: else:
return False return False
@ -1842,9 +1932,11 @@ class Tag(ObjectBase, GroupableMixin):
else: else:
raise TagExists(synname) raise TagExists(synname)
self.photodb._cached_frozen_children = None
self.photodb.cur.execute('INSERT INTO tag_synonyms VALUES(?, ?)', [synname, self.name]) self.photodb.cur.execute('INSERT INTO tag_synonyms VALUES(?, ?)', [synname, self.name])
if commit: if commit:
log.debug('Committing - add synonym')
self.photodb.sql.commit() self.photodb.sql.commit()
def convert_to_synonym(self, mastertag, commit=True): def convert_to_synonym(self, mastertag, commit=True):
@ -1860,6 +1952,7 @@ class Tag(ObjectBase, GroupableMixin):
# Migrate the old tag's synonyms to the new one # Migrate the old tag's synonyms to the new one
# UPDATE is safe for this operation because there is no chance of duplicates. # UPDATE is safe for this operation because there is no chance of duplicates.
self.photodb._cached_frozen_children = None
self.photodb.cur.execute( self.photodb.cur.execute(
'UPDATE tag_synonyms SET mastername = ? WHERE mastername == ?', 'UPDATE tag_synonyms SET mastername = ? WHERE mastername == ?',
[mastertag.name, self.name] [mastertag.name, self.name]
@ -1876,28 +1969,34 @@ class Tag(ObjectBase, GroupableMixin):
# Then delete the relationships with the old tag # Then delete the relationships with the old tag
self.delete() self.delete()
# Enjoy your new life as a monk. # Enjoy your new life as a monk.
mastertag.add_synonym(self.name, commit=False) mastertag.add_synonym(self.name, commit=False)
if commit: if commit:
log.debug('Committing - convert to synonym')
self.photodb.sql.commit() self.photodb.sql.commit()
def delete(self, delete_children=False, commit=True): def delete(self, delete_children=False, commit=True):
log.debug('Deleting tag {tag:r}'.format(tag=self)) log.debug('Deleting tag {tag:r}'.format(tag=self))
self.photodb._cached_frozen_children = None
GroupableMixin.delete(self, delete_children=delete_children, commit=False) GroupableMixin.delete(self, delete_children=delete_children, commit=False)
self.photodb.cur.execute('DELETE FROM tags WHERE id == ?', [self.id]) self.photodb.cur.execute('DELETE FROM tags WHERE id == ?', [self.id])
self.photodb.cur.execute('DELETE FROM photo_tag_rel WHERE tagid == ?', [self.id]) self.photodb.cur.execute('DELETE FROM photo_tag_rel WHERE tagid == ?', [self.id])
self.photodb.cur.execute('DELETE FROM tag_synonyms WHERE mastername == ?', [self.name]) self.photodb.cur.execute('DELETE FROM tag_synonyms WHERE mastername == ?', [self.name])
if commit: if commit:
log.debug('Committing - delete tag')
self.photodb.sql.commit() self.photodb.sql.commit()
def qualified_name(self): def qualified_name(self):
''' '''
Return the 'group1.group2.tag' string for this tag. Return the 'group1.group2.tag' string for this tag.
''' '''
if self._cached_qualified_name:
return self._cached_qualified_name
string = self.name string = self.name
for parent in self.walk_parents(): for parent in self.walk_parents():
string = parent.name + '.' + string string = parent.name + '.' + string
self._cached_qualified_name = string
return string return string
def remove_synonym(self, synname, commit=True): def remove_synonym(self, synname, commit=True):
@ -1912,8 +2011,10 @@ class Tag(ObjectBase, GroupableMixin):
if fetch is None: if fetch is None:
raise NoSuchSynonym(synname) raise NoSuchSynonym(synname)
self.photodb._cached_frozen_children = None
self.photodb.cur.execute('DELETE FROM tag_synonyms WHERE name == ?', [synname]) self.photodb.cur.execute('DELETE FROM tag_synonyms WHERE name == ?', [synname])
if commit: if commit:
log.debug('Committing - remove synonym')
self.photodb.sql.commit() self.photodb.sql.commit()
def rename(self, new_name, apply_to_synonyms=True, commit=True): def rename(self, new_name, apply_to_synonyms=True, commit=True):
@ -1931,6 +2032,8 @@ class Tag(ObjectBase, GroupableMixin):
else: else:
raise TagExists(new_name) raise TagExists(new_name)
self._cached_qualified_name = None
self.photodb._cached_frozen_children = None
self.photodb.cur.execute('UPDATE tags SET name = ? WHERE id == ?', [new_name, self.id]) self.photodb.cur.execute('UPDATE tags SET name = ? WHERE id == ?', [new_name, self.id])
if apply_to_synonyms: if apply_to_synonyms:
self.photodb.cur.execute( self.photodb.cur.execute(
@ -1940,6 +2043,7 @@ class Tag(ObjectBase, GroupableMixin):
self.name = new_name self.name = new_name
if commit: if commit:
log.debug('Committing - rename tag')
self.photodb.sql.commit() self.photodb.sql.commit()
def synonyms(self): def synonyms(self):
@ -1952,4 +2056,4 @@ class Tag(ObjectBase, GroupableMixin):
if __name__ == '__main__': if __name__ == '__main__':
p = PhotoDB() p = PhotoDB()
print(p) print(p)

View file

@ -0,0 +1,3 @@
flask
gevent
pillow

View file

@ -1,6 +1,4 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg <svg
xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#" xmlns:cc="http://creativecommons.org/ns#"
@ -10,20 +8,36 @@
xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="400" width="640"
height="400" height="640"
viewBox="0 0 400.00002 400" viewBox="0 0 640.00003 640"
id="svg2" id="svg2"
version="1.1" version="1.1"
inkscape:version="0.91 r13725" inkscape:version="0.91 r13725"
sodipodi:docname="audio.svg" sodipodi:docname="audio.svg">
inkscape:export-filename="C:\Git\else\Etiquette\static\basic_thumbnails\audio.png" <sodipodi:namedview
inkscape:export-xdpi="144" pagecolor="#ffffff"
inkscape:export-ydpi="144"> bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="686"
inkscape:window-height="480"
id="namedview19"
showgrid="false"
inkscape:zoom="0.36875"
inkscape:cx="320"
inkscape:cy="320"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="svg2" />
<defs <defs
id="defs4"> id="defs4">
<linearGradient <linearGradient
inkscape:collect="always"
id="linearGradient4723"> id="linearGradient4723">
<stop <stop
style="stop-color:#157a05;stop-opacity:1" style="stop-color:#157a05;stop-opacity:1"
@ -38,13 +52,11 @@
id="clipPath5600" id="clipPath5600"
clipPathUnits="userSpaceOnUse"> clipPathUnits="userSpaceOnUse">
<path <path
inkscape:connector-curvature="0"
id="path5602" id="path5602"
d="m 416.04743,209.57467 a 210.71427,210.71427 0 0 0 -201.63871,156.16785 210.71427,210.71427 0 0 0 148.9961,258.07232 210.71427,210.71427 0 0 0 258.07031,-148.99795 210.71427,210.71427 0 0 0 -148.9961,-258.07035 210.71427,210.71427 0 0 0 -56.4316,-7.17187 z m 0.088,10 a 200.71428,200.71428 0 0 1 53.75188,6.83 A 200.71428,200.71428 0 0 1 611.81495,472.22889 200.71428,200.71428 0 0 1 365.99079,614.15457 200.71428,200.71428 0 0 1 224.0669,368.3325 200.71428,200.71428 0 0 1 416.13517,219.57467 Z m 0.67771,10.66015 c -28.9322,-0.0158 -57.57612,6.77648 -84.07229,20.47847 -5.58051,2.89034 -7.71121,5.24233 -5.71092,8.93562 l 84.62499,146.57421 c 3.9199,6.67208 6.92071,6.5608 8.37693,-0.1625 L 463.9225,242.25987 c 1.2404,-4.75141 0.77432,-5.32621 -7.65239,-7.81065 -13.09518,-2.80222 -26.30598,-4.21013 -39.45697,-4.21472 z m 55.19328,8.75394 c -2.49601,-0.0656 -4.01546,0.93883 -4.7636,3.45495 l -43.80467,163.4825 c -1.94613,7.48962 0.25549,9.53469 6.03899,5.81055 l 146.85941,-84.78907 c 4.23688,-2.48265 4.31319,-3.22025 0.11141,-10.93556 -23.2904,-35.97209 -57.50302,-62.6885 -98.8477,-75.84771 -2.24622,-0.71332 -4.09611,-1.13676 -5.59382,-1.17566 z m -150.93155,19.66206 c -1.45873,-0.0237 -3.57203,1.00841 -7.42973,3.10929 -35.97206,23.29037 -62.69049,57.50296 -75.8496,98.84772 -1.90228,5.98981 -1.74657,9.15834 2.2793,10.35536 l 163.48439,43.80474 c 7.48959,1.94628 9.5327,-0.25337 5.8086,-6.03706 l -84.78918,-146.8593 c -1.2413,-2.11844 -2.04519,-3.19729 -3.50392,-3.22075 z m 260.03112,70.01171 c -0.7849,-0.0158 -1.62359,0.23625 -2.54681,0.73629 l -146.57616,84.62511 c -6.67213,3.91977 -6.56093,6.92076 0.16224,8.37677 l 163.8008,43.89074 c 4.75141,1.24035 5.32608,0.77437 7.8105,-7.65244 8.96741,-41.90478 3.66661,-84.98932 -16.2636,-123.52924 -2.1678,-4.18535 -4.0319,-6.43032 -6.38678,-6.44723 z m -344.04481,45.09563 c -2.28245,0 -3.07026,1.84403 -4.93359,8.16404 -8.96736,41.90477 -3.66848,84.9895 16.2617,123.52942 2.89032,5.58046 5.2424,7.71121 8.93556,5.71094 l 146.57419,-84.62495 c 6.67208,-3.91978 6.5628,-6.92092 -0.16013,-8.37709 L 239.93603,374.26994 c -1.18786,-0.31005 -2.1142,-0.51275 -2.87501,-0.51178 z m 190.67183,51.3438 c -3.55996,0.069 -4.01188,2.39073 -1.21878,6.72855 l 84.79113,146.86128 c 2.48259,4.23704 3.2182,4.31319 10.93359,0.11167 35.97198,-23.29037 62.6885,-57.50509 75.8476,-98.8497 1.90228,-5.98979 1.7466,-9.1585 -2.27929,-10.35552 L 432.3247,425.79345 c -1.87239,-0.4865 -3.4051,-0.7143 -4.5918,-0.69133 z m -16.97067,1.91211 c -1.09041,0.0477 -2.54811,0.64684 -4.35552,1.81055 l -146.8593,84.79104 c -4.23692,2.48265 -4.31319,3.21828 -0.11142,10.93359 23.2903,35.97209 57.50296,62.6885 98.84765,75.84755 5.98981,1.90226 9.15839,1.74668 10.35542,-2.27929 l 43.80668,-163.48234 c 1.33799,-5.14927 0.7152,-7.72466 -1.68359,-7.6211 z m 8.35348,2.38285 c -1.42229,0.0345 -2.53748,1.74194 -3.26558,5.1035 L 371.9615,598.30117 c -1.2405,4.75143 -0.7744,5.32621 7.65242,7.8105 41.90477,8.96746 84.98929,3.66653 123.52929,-16.26358 5.58051,-2.89034 7.7112,-5.24251 5.71091,-8.93562 l -84.625,-146.57421 c -1.95992,-3.33596 -3.69103,-4.97547 -5.11333,-4.94134 z" d="m 416.04743,209.57467 a 210.71427,210.71427 0 0 0 -201.63871,156.16785 210.71427,210.71427 0 0 0 148.9961,258.07232 210.71427,210.71427 0 0 0 258.07031,-148.99795 210.71427,210.71427 0 0 0 -148.9961,-258.07035 210.71427,210.71427 0 0 0 -56.4316,-7.17187 z m 0.088,10 a 200.71428,200.71428 0 0 1 53.75188,6.83 A 200.71428,200.71428 0 0 1 611.81495,472.22889 200.71428,200.71428 0 0 1 365.99079,614.15457 200.71428,200.71428 0 0 1 224.0669,368.3325 200.71428,200.71428 0 0 1 416.13517,219.57467 Z m 0.67771,10.66015 c -28.9322,-0.0158 -57.57612,6.77648 -84.07229,20.47847 -5.58051,2.89034 -7.71121,5.24233 -5.71092,8.93562 l 84.62499,146.57421 c 3.9199,6.67208 6.92071,6.5608 8.37693,-0.1625 L 463.9225,242.25987 c 1.2404,-4.75141 0.77432,-5.32621 -7.65239,-7.81065 -13.09518,-2.80222 -26.30598,-4.21013 -39.45697,-4.21472 z m 55.19328,8.75394 c -2.49601,-0.0656 -4.01546,0.93883 -4.7636,3.45495 l -43.80467,163.4825 c -1.94613,7.48962 0.25549,9.53469 6.03899,5.81055 l 146.85941,-84.78907 c 4.23688,-2.48265 4.31319,-3.22025 0.11141,-10.93556 -23.2904,-35.97209 -57.50302,-62.6885 -98.8477,-75.84771 -2.24622,-0.71332 -4.09611,-1.13676 -5.59382,-1.17566 z m -150.93155,19.66206 c -1.45873,-0.0237 -3.57203,1.00841 -7.42973,3.10929 -35.97206,23.29037 -62.69049,57.50296 -75.8496,98.84772 -1.90228,5.98981 -1.74657,9.15834 2.2793,10.35536 l 163.48439,43.80474 c 7.48959,1.94628 9.5327,-0.25337 5.8086,-6.03706 l -84.78918,-146.8593 c -1.2413,-2.11844 -2.04519,-3.19729 -3.50392,-3.22075 z m 260.03112,70.01171 c -0.7849,-0.0158 -1.62359,0.23625 -2.54681,0.73629 l -146.57616,84.62511 c -6.67213,3.91977 -6.56093,6.92076 0.16224,8.37677 l 163.8008,43.89074 c 4.75141,1.24035 5.32608,0.77437 7.8105,-7.65244 8.96741,-41.90478 3.66661,-84.98932 -16.2636,-123.52924 -2.1678,-4.18535 -4.0319,-6.43032 -6.38678,-6.44723 z m -344.04481,45.09563 c -2.28245,0 -3.07026,1.84403 -4.93359,8.16404 -8.96736,41.90477 -3.66848,84.9895 16.2617,123.52942 2.89032,5.58046 5.2424,7.71121 8.93556,5.71094 l 146.57419,-84.62495 c 6.67208,-3.91978 6.5628,-6.92092 -0.16013,-8.37709 L 239.93603,374.26994 c -1.18786,-0.31005 -2.1142,-0.51275 -2.87501,-0.51178 z m 190.67183,51.3438 c -3.55996,0.069 -4.01188,2.39073 -1.21878,6.72855 l 84.79113,146.86128 c 2.48259,4.23704 3.2182,4.31319 10.93359,0.11167 35.97198,-23.29037 62.6885,-57.50509 75.8476,-98.8497 1.90228,-5.98979 1.7466,-9.1585 -2.27929,-10.35552 L 432.3247,425.79345 c -1.87239,-0.4865 -3.4051,-0.7143 -4.5918,-0.69133 z m -16.97067,1.91211 c -1.09041,0.0477 -2.54811,0.64684 -4.35552,1.81055 l -146.8593,84.79104 c -4.23692,2.48265 -4.31319,3.21828 -0.11142,10.93359 23.2903,35.97209 57.50296,62.6885 98.84765,75.84755 5.98981,1.90226 9.15839,1.74668 10.35542,-2.27929 l 43.80668,-163.48234 c 1.33799,-5.14927 0.7152,-7.72466 -1.68359,-7.6211 z m 8.35348,2.38285 c -1.42229,0.0345 -2.53748,1.74194 -3.26558,5.1035 L 371.9615,598.30117 c -1.2405,4.75143 -0.7744,5.32621 7.65242,7.8105 41.90477,8.96746 84.98929,3.66653 123.52929,-16.26358 5.58051,-2.89034 7.7112,-5.24251 5.71091,-8.93562 l -84.625,-146.57421 c -1.95992,-3.33596 -3.69103,-4.97547 -5.11333,-4.94134 z"
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:8;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:8;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
</clipPath> </clipPath>
<linearGradient <linearGradient
inkscape:collect="always"
xlink:href="#linearGradient4723" xlink:href="#linearGradient4723"
id="linearGradient4739" id="linearGradient4739"
x1="196.13852" x1="196.13852"
@ -54,26 +66,6 @@
gradientUnits="userSpaceOnUse" gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.74021631,-0.67236881,0.67236881,0.74021631,-530.7894,361.49036)" /> gradientTransform="matrix(0.74021631,-0.67236881,0.67236881,0.74021631,-530.7894,361.49036)" />
</defs> </defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.67124999"
inkscape:cx="199.31367"
inkscape:cy="266.33033"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1366"
inkscape:window-height="706"
inkscape:window-x="1432"
inkscape:window-y="204"
inkscape:window-maximized="1"
units="px"
showguides="false" />
<metadata <metadata
id="metadata7"> id="metadata7">
<rdf:RDF> <rdf:RDF>
@ -87,41 +79,33 @@
</rdf:RDF> </rdf:RDF>
</metadata> </metadata>
<g <g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1" id="layer1"
transform="translate(0,-652.36221)"> transform="matrix(1.5999999,0,0,1.5999999,-4.414061e-6,-1043.7794)">
<path <path
cx="356.42856" cx="356.42856"
cy="515.93359" cy="515.93359"
r="190.61275" r="190.61275"
id="path4146" id="path4146"
style="opacity:1;fill:#f2dc39;fill-opacity:1;stroke:none;stroke-width:8;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" style="opacity:1;fill:#f2dc39;fill-opacity:1;stroke:none;stroke-width:8;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
d="" d="" />
inkscape:connector-curvature="0" />
<path <path
cx="356.42856" cx="356.42856"
cy="515.93359" cy="515.93359"
r="190.61275" r="190.61275"
id="path4148" id="path4148"
style="opacity:1;fill:#f2dc39;fill-opacity:1;stroke:none;stroke-width:8;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" style="opacity:1;fill:#f2dc39;fill-opacity:1;stroke:none;stroke-width:8;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
d="" d="" />
inkscape:connector-curvature="0" />
<g <g
id="g4759" id="g4759"
transform="matrix(2.0529613,0,0,2.0529613,-208.02761,-862.66247)"> transform="matrix(2.0529613,0,0,2.0529613,-208.02761,-862.66247)">
<path <path
id="path4176" id="path4176"
d="m 160.49771,775.78875 c -17.18761,15.61221 -30.85175,51.16826 -26.54481,80.55638 -5.68961,15.27438 -2.23888,26.77908 5.86857,35.70464 8.41129,9.26005 20.42276,13.46722 36.74655,8.76774 28.38226,5.82869 63.31581,-4.19178 79.95892,-19.3094 z" d="m 160.49771,775.78875 c -17.18761,15.61221 -30.85175,51.16826 -26.54481,80.55638 -5.68961,15.27438 -2.23888,26.77908 5.86857,35.70464 8.41129,9.26005 20.42276,13.46722 36.74655,8.76774 28.38226,5.82869 63.31581,-4.19178 79.95892,-19.3094 z"
style="opacity:1;fill:url(#linearGradient4739);fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:4;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" style="opacity:1;fill:url(#linearGradient4739);fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:4;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccsccc" />
<path <path
style="opacity:1;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:1.00000012;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" style="opacity:1;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:1.00000012;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
d="m 187.71478,901.36194 c -15.6087,-1.19621 -30.19953,-6.46109 -40.11858,-17.38106 -3.59314,-3.95572 -6.38887,-8.45054 -8.4789,-13.31879" d="m 187.71478,901.36194 c -15.6087,-1.19621 -30.19953,-6.46109 -40.11858,-17.38106 -3.59314,-3.95572 -6.38887,-8.45054 -8.4789,-13.31879"
id="path4179" id="path4179" />
inkscape:connector-curvature="0"
sodipodi:nodetypes="csc" />
<ellipse <ellipse
transform="matrix(0.74021631,-0.67236881,0.67236881,0.74021631,0,0)" transform="matrix(0.74021631,-0.67236881,0.67236881,0.74021631,0,0)"
ry="71.356659" ry="71.356659"
@ -131,8 +115,6 @@
id="path4189" id="path4189"
style="opacity:1;fill:#bdff7f;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:4.10868788;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> style="opacity:1;fill:#bdff7f;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:4.10868788;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
<path <path
sodipodi:nodetypes="ccccccccccccccccccccccccccccccccccccccccccccccccccc"
inkscape:connector-curvature="0"
id="circle5376" id="circle5376"
d="m 167.97402,779.73201 c 12.95394,14.14301 25.67999,28.49473 38.76045,42.52164 1.09188,1.09288 1.57468,2.03532 1.12908,0.24144 -3.81326,-15.37536 -7.34553,-30.80883 -11.73933,-46.02909 -9.33962,-3.59468 -21.01947,-4.53564 -29.35443,1.94027 z m -3.40652,1.08631 c -6.35037,7.84741 -6.53271,18.97849 -3.33194,28.11951 14.61665,5.15311 29.16348,10.48467 43.81316,15.53604 2.17968,0.89623 1.61044,0.35664 0.43632,-0.95963 -13.16593,-14.74917 -26.57361,-29.28008 -39.82726,-43.95011 l -1.09034,1.25425 z m 34.52343,-0.80156 10.96907,43.65806 c 0.3333,2.09058 0.60304,1.61585 1.77113,0.51394 7.58595,-7.11871 15.71168,-13.70923 22.85068,-21.25393 -10.11646,-11.06159 -22.40233,-20.30395 -36.34759,-25.93267 z m -36.91782,33.66852 c 4.32187,13.48593 12.1718,25.57007 21.57681,36.03286 l 22.88728,-20.78947 c 1.94371,-1.55853 0.90464,-1.6299 -0.63476,-2.19321 L 161.23898,810.7949 Z m 52.1086,14.12822 44.66065,15.90394 c -4.32392,-14.54689 -12.41384,-27.7685 -22.56898,-38.9523 l -23.01225,20.90297 c -1.67223,1.35326 -1.17245,1.5415 0.92058,2.14539 z m -28.6948,23.78756 c 9.97733,11.13211 22.30826,20.25112 36.19297,25.86994 l -11.81394,-47.01872 c -0.24809,-1.18978 -0.36986,-1.35104 -1.49167,-0.33205 l -23.22275,21.09417 0.32766,0.37768 0.008,0.009 z m 27.58834,-15.54751 c 3.45815,13.76858 6.91919,27.53657 10.37846,41.30493 9.14559,4.6969 21.26842,5.13014 29.7965,-1.27057 -13.33458,-14.60073 -26.5137,-29.34346 -39.93482,-43.86507 -0.84001,-1.17611 -1.91288,-2.39494 -1.22768,-0.34375 z m 3.75391,-2.44675 37.61326,41.40874 c 7.1511,-7.87583 7.83178,-19.8138 4.31773,-29.47142 -14.6668,-5.17311 -29.26398,-10.5241 -43.96637,-15.58822 -2.34003,-1.03127 -1.12256,0.0763 -0.0843,1.25149 0.7066,0.79977 1.41314,1.59962 2.11974,2.39938 z" d="m 167.97402,779.73201 c 12.95394,14.14301 25.67999,28.49473 38.76045,42.52164 1.09188,1.09288 1.57468,2.03532 1.12908,0.24144 -3.81326,-15.37536 -7.34553,-30.80883 -11.73933,-46.02909 -9.33962,-3.59468 -21.01947,-4.53564 -29.35443,1.94027 z m -3.40652,1.08631 c -6.35037,7.84741 -6.53271,18.97849 -3.33194,28.11951 14.61665,5.15311 29.16348,10.48467 43.81316,15.53604 2.17968,0.89623 1.61044,0.35664 0.43632,-0.95963 -13.16593,-14.74917 -26.57361,-29.28008 -39.82726,-43.95011 l -1.09034,1.25425 z m 34.52343,-0.80156 10.96907,43.65806 c 0.3333,2.09058 0.60304,1.61585 1.77113,0.51394 7.58595,-7.11871 15.71168,-13.70923 22.85068,-21.25393 -10.11646,-11.06159 -22.40233,-20.30395 -36.34759,-25.93267 z m -36.91782,33.66852 c 4.32187,13.48593 12.1718,25.57007 21.57681,36.03286 l 22.88728,-20.78947 c 1.94371,-1.55853 0.90464,-1.6299 -0.63476,-2.19321 L 161.23898,810.7949 Z m 52.1086,14.12822 44.66065,15.90394 c -4.32392,-14.54689 -12.41384,-27.7685 -22.56898,-38.9523 l -23.01225,20.90297 c -1.67223,1.35326 -1.17245,1.5415 0.92058,2.14539 z m -28.6948,23.78756 c 9.97733,11.13211 22.30826,20.25112 36.19297,25.86994 l -11.81394,-47.01872 c -0.24809,-1.18978 -0.36986,-1.35104 -1.49167,-0.33205 l -23.22275,21.09417 0.32766,0.37768 0.008,0.009 z m 27.58834,-15.54751 c 3.45815,13.76858 6.91919,27.53657 10.37846,41.30493 9.14559,4.6969 21.26842,5.13014 29.7965,-1.27057 -13.33458,-14.60073 -26.5137,-29.34346 -39.93482,-43.86507 -0.84001,-1.17611 -1.91288,-2.39494 -1.22768,-0.34375 z m 3.75391,-2.44675 37.61326,41.40874 c 7.1511,-7.87583 7.83178,-19.8138 4.31773,-29.47142 -14.6668,-5.17311 -29.26398,-10.5241 -43.96637,-15.58822 -2.34003,-1.03127 -1.12256,0.0763 -0.0843,1.25149 0.7066,0.79977 1.41314,1.59962 2.11974,2.39938 z"
style="opacity:1;fill:#6bd625;fill-opacity:1;stroke:none;stroke-width:8;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> style="opacity:1;fill:#6bd625;fill-opacity:1;stroke:none;stroke-width:8;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

View file

@ -1,6 +1,4 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg <svg
xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#" xmlns:cc="http://creativecommons.org/ns#"
@ -8,22 +6,14 @@
xmlns:svg="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" width="640"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" height="640"
width="400" viewBox="0 0 640.00003 640"
height="400"
viewBox="0 0 400.00002 400"
id="svg2" id="svg2"
version="1.1" version="1.1">
inkscape:version="0.91 r13725"
sodipodi:docname="other.svg"
inkscape:export-filename="C:\Git\else\Etiquette\static\basic_thumbnails\other.png"
inkscape:export-xdpi="185.87027"
inkscape:export-ydpi="185.87027">
<defs <defs
id="defs4"> id="defs4">
<linearGradient <linearGradient
inkscape:collect="always"
id="linearGradient4361"> id="linearGradient4361">
<stop <stop
style="stop-color:#4d4d4d;stop-opacity:1" style="stop-color:#4d4d4d;stop-opacity:1"
@ -35,7 +25,6 @@
id="stop4365" /> id="stop4365" />
</linearGradient> </linearGradient>
<linearGradient <linearGradient
inkscape:collect="always"
xlink:href="#linearGradient4361" xlink:href="#linearGradient4361"
id="linearGradient4367" id="linearGradient4367"
x1="315.47162" x1="315.47162"
@ -45,25 +34,6 @@
gradientUnits="userSpaceOnUse" gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.8225722,0,0,1.3309496,35.485561,-282.08894)" /> gradientTransform="matrix(0.8225722,0,0,1.3309496,35.485561,-282.08894)" />
</defs> </defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.94929084"
inkscape:cx="51.384844"
inkscape:cy="169.83481"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1366"
inkscape:window-height="706"
inkscape:window-x="1432"
inkscape:window-y="204"
inkscape:window-maximized="1"
units="px" />
<metadata <metadata
id="metadata7"> id="metadata7">
<rdf:RDF> <rdf:RDF>
@ -77,10 +47,8 @@
</rdf:RDF> </rdf:RDF>
</metadata> </metadata>
<g <g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1" id="layer1"
transform="translate(0,-652.36221)"> transform="matrix(1.6,0,0,1.6,-2.4414063e-5,-1043.7795)">
<rect <rect
style="opacity:1;fill:url(#linearGradient4367);fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:4.18531179;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" style="opacity:1;fill:url(#linearGradient4367);fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:4.18531179;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="rect4359" id="rect4359"

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

View file

@ -0,0 +1,208 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
width="640"
height="640"
viewBox="0 0 640.00003 640"
id="svg2"
version="1.1">
<defs
id="defs4">
<linearGradient
id="linearGradient4160">
<stop
style="stop-color:#414141;stop-opacity:1"
offset="0"
id="stop4195" />
<stop
id="stop4197"
offset="0.44465271"
style="stop-color:#a5a5a5;stop-opacity:1" />
<stop
id="stop4199"
offset="0.51481694"
style="stop-color:#cccccc;stop-opacity:1" />
<stop
style="stop-color:#ffffff;stop-opacity:1"
offset="1"
id="stop4201" />
</linearGradient>
<linearGradient
id="linearGradient4203">
<stop
id="stop4179"
offset="0"
style="stop-color:#414141;stop-opacity:1" />
<stop
style="stop-color:#a5a5a5;stop-opacity:1"
offset="0.44465271"
id="stop4181" />
<stop
style="stop-color:#cccccc;stop-opacity:1"
offset="0.51481694"
id="stop4183" />
<stop
id="stop4185"
offset="1"
style="stop-color:#ffffff;stop-opacity:1" />
</linearGradient>
<linearGradient
id="linearGradient4187">
<stop
style="stop-color:#f5f5f5;stop-opacity:1"
offset="0"
id="stop4162" />
<stop
style="stop-color:#ffffff;stop-opacity:1"
offset="1"
id="stop4164" />
</linearGradient>
<linearGradient
xlink:href="#linearGradient4160"
id="linearGradient4166"
x1="218.28458"
y1="950.68616"
x2="213.38019"
y2="742.12012"
gradientUnits="userSpaceOnUse" />
<linearGradient
xlink:href="#linearGradient4187"
id="linearGradient4191"
x1="185.59323"
y1="394.32205"
x2="185.59323"
y2="123.97182"
gradientUnits="userSpaceOnUse" />
</defs>
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
transform="matrix(1.6,0,0,1.6,151.99998,-84.47952)"
id="g4352">
<g
id="g4324">
<g
id="g4280">
<path
style="opacity:1;fill:url(#linearGradient4191);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:28;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
d="m 0,119.99998 210.00002,0 0,280.00003 -210.00002,0 z"
id="rect4141" />
<g
id="g4254">
<path
id="path4143"
d="m 0,372 0,8 210,0 0,-8 -210,0 z"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#00ffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:8.00000095;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
<path
id="path4145"
d="m 0,352 0,8 210,0 0,-8 -210,0 z"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#00ffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:8.00000095;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
<path
id="path4147"
d="m 0,332 0,8 210,0 0,-8 -210,0 z"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#00ffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:8.00000095;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
<path
id="path4149"
d="m 0,312 0,8 210,0 0,-8 -210,0 z"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#00ffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:8.00000095;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
<path
id="path4151"
d="m 0,292 0,8 210,0 0,-8 -210,0 z"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#00ffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:8.00000095;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
<path
id="path4153"
d="m 0,272 0,8 210,0 0,-8 -210,0 z"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#00ffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:8.00000095;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
<path
id="path4155"
d="m 0,252 0,8 210,0 0,-8 -210,0 z"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#00ffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:8.00000095;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
<path
id="path4157"
d="m 0,232 0,8 210,0 0,-8 -210,0 z"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#00ffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:8.00000095;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
<path
id="path4159"
d="m 0,212 0,8 210,0 0,-8 -210,0 z"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#00ffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:8.00000095;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
<path
id="path4161"
d="m 0,192 0,8 210,0 0,-8 -210,0 z"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#00ffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:8.00000095;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
<path
id="path4163"
d="m 0,172 0,8 210,0 0,-8 -210,0 z"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#00ffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:8.00000095;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
<path
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#da6a8d;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:8.00000095;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="m 20.000001,400.00001 8,0 0,-280.00002 -8,0 0,280.00002 z"
id="path4226" />
</g>
</g>
<g
id="g4268">
<path
id="rect4239"
d="m 37.453492,122.77542 135.093038,0 c 1.35923,0 2.45349,1.09426 2.45349,2.45349 l 0,17.3176 c 0,1.35923 -1.09426,2.45349 -2.45349,2.45349 l -135.093038,0 c -1.359233,0 -2.453488,-1.09426 -2.453488,-2.45349 l 0,-17.3176 c 0,-1.35923 1.094255,-2.45349 2.453488,-2.45349 z"
style="opacity:1;fill:#d1d1d1;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:12;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
<g
id="g4244">
<path
id="path4171"
d="m 105.00001,134.83513 0,-30.47246"
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:9.39278793;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
id="rect4190"
d="m 104.41582,99.999977 1.16838,0 c 2.44636,0 4.41581,1.969453 4.41581,4.415813 l 0,31.16838 c 0,2.44636 -1.96945,4.41581 -4.41581,4.41581 l -1.16838,0 c -2.44636,0 -4.41581,-1.96945 -4.41581,-4.41581 l 0,-31.16838 c 0,-2.44636 1.96945,-4.415813 4.41581,-4.415813 z"
style="opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:12;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
<path
id="rect4192"
d="m 124.41582,99.999977 1.16838,0 c 2.44636,0 4.41581,1.969453 4.41581,4.415813 l 0,31.16838 c 0,2.44636 -1.96945,4.41581 -4.41581,4.41581 l -1.16838,0 c -2.44636,0 -4.41581,-1.96945 -4.41581,-4.41581 l 0,-31.16838 c 0,-2.44636 1.96945,-4.415813 4.41581,-4.415813 z"
style="opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:12;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
<path
id="rect4194"
d="m 144.41583,99.999977 1.16838,0 c 2.44635,0 4.41581,1.969453 4.41581,4.415813 l 0,31.16838 c 0,2.44636 -1.96946,4.41581 -4.41581,4.41581 l -1.16838,0 c -2.44636,0 -4.41581,-1.96945 -4.41581,-4.41581 l 0,-31.16838 c 0,-2.44636 1.96945,-4.415813 4.41581,-4.415813 z"
style="opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:12;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
<path
id="rect4196"
d="m 84.415818,99.999977 1.16838,0 c 2.446359,0 4.415811,1.969453 4.415811,4.415813 l 0,31.16838 c 0,2.44636 -1.969452,4.41581 -4.415811,4.41581 l -1.16838,0 c -2.446359,0 -4.41581,-1.96945 -4.41581,-4.41581 l 0,-31.16838 c 0,-2.44636 1.969451,-4.415813 4.41581,-4.415813 z"
style="opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:12;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
<path
id="rect4198"
d="m 64.415814,99.999977 1.16838,0 c 2.446359,0 4.415811,1.969453 4.415811,4.415813 l 0,31.16838 c 0,2.44636 -1.969452,4.41581 -4.415811,4.41581 l -1.16838,0 c -2.446359,0 -4.41581,-1.96945 -4.41581,-4.41581 l 0,-31.16838 c 0,-2.44636 1.969451,-4.415813 4.41581,-4.415813 z"
style="opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:12;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
<path
id="rect4200"
d="m 164.41583,99.999977 1.16838,0 c 2.44635,0 4.41581,1.969453 4.41581,4.415813 l 0,31.16838 c 0,2.44636 -1.96946,4.41581 -4.41581,4.41581 l -1.16838,0 c -2.44636,0 -4.41581,-1.96945 -4.41581,-4.41581 l 0,-31.16838 c 0,-2.44636 1.96945,-4.415813 4.41581,-4.415813 z"
style="opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:12;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
<path
id="rect4202"
d="m 44.415814,99.999977 1.16838,0 c 2.446359,0 4.415811,1.969453 4.415811,4.415813 l 0,31.16838 c 0,2.44636 -1.969452,4.41581 -4.415811,4.41581 l -1.16838,0 c -2.446359,0 -4.41581,-1.96945 -4.41581,-4.41581 l 0,-31.16838 c 0,-2.44636 1.969451,-4.415813 4.41581,-4.415813 z"
style="opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:12;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
</g>
</g>
</g>
<rect
y="119.99998"
x="0"
height="280.00003"
width="210.00002"
id="rect4169"
style="opacity:1;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:12.00000095;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 21 KiB

View file

@ -1,6 +1,4 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg <svg
xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#" xmlns:cc="http://creativecommons.org/ns#"
@ -8,22 +6,14 @@
xmlns:svg="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" width="640"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" height="640"
width="400" viewBox="0 0 640.00003 640"
height="400"
viewBox="0 0 400.00002 400"
id="svg2" id="svg2"
version="1.1" version="1.1">
inkscape:version="0.91 r13725"
sodipodi:docname="video.svg"
inkscape:export-filename="C:\Git\else\Etiquette\static\basic_thumbnails\video.png"
inkscape:export-xdpi="143.99998"
inkscape:export-ydpi="143.99998">
<defs <defs
id="defs4"> id="defs4">
<linearGradient <linearGradient
inkscape:collect="always"
id="linearGradient4160"> id="linearGradient4160">
<stop <stop
style="stop-color:#414141;stop-opacity:1" style="stop-color:#414141;stop-opacity:1"
@ -43,7 +33,6 @@
id="stop4164" /> id="stop4164" />
</linearGradient> </linearGradient>
<linearGradient <linearGradient
inkscape:collect="always"
xlink:href="#linearGradient4160" xlink:href="#linearGradient4160"
id="linearGradient4166" id="linearGradient4166"
x1="218.28458" x1="218.28458"
@ -52,25 +41,6 @@
y2="742.12012" y2="742.12012"
gradientUnits="userSpaceOnUse" /> gradientUnits="userSpaceOnUse" />
</defs> </defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.335625"
inkscape:cx="833.60008"
inkscape:cy="34.45227"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1366"
inkscape:window-height="706"
inkscape:window-x="1432"
inkscape:window-y="204"
inkscape:window-maximized="1"
units="px" />
<metadata <metadata
id="metadata7"> id="metadata7">
<rdf:RDF> <rdf:RDF>
@ -79,22 +49,16 @@
<dc:format>image/svg+xml</dc:format> <dc:format>image/svg+xml</dc:format>
<dc:type <dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title /> <dc:title></dc:title>
</cc:Work> </cc:Work>
</rdf:RDF> </rdf:RDF>
</metadata> </metadata>
<g <g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1" id="layer1"
transform="translate(0,-652.36221)"> transform="matrix(1.6,0,0,1.6,-2.4414063e-5,-1043.7795)">
<path <path
style="opacity:1;fill:url(#linearGradient4166);fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:4.00000024;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" style="opacity:1;fill:url(#linearGradient4166);fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:4.00000048;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
inkscape:transform-center-x="-38.11878"
inkscape:transform-center-y="-2.5060727e-005"
d="M 317.15321,852.36221 88.440457,984.40954 c 0,0 40.204773,-53.51798 40.204773,-132.04733 0,-78.52935 -40.204773,-132.0474 -40.204773,-132.0474 z" d="M 317.15321,852.36221 88.440457,984.40954 c 0,0 40.204773,-53.51798 40.204773,-132.04733 0,-78.52935 -40.204773,-132.0474 -40.204773,-132.0474 z"
id="path4157" id="path4157" />
inkscape:connector-curvature="0"
sodipodi:nodetypes="cczcc" />
</g> </g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 2 KiB

View file

@ -141,12 +141,18 @@ li
{ {
max-height: 100%; max-height: 100%;
} }
.photo_galleryview_info span .photo_galleryview_file_metadata
{ {
position: absolute; position: absolute;
bottom: 0; bottom: 0;
right: 0; right: 0;
} }
.photo_galleryview_tags
{
position: absolute;
bottom: 0;
left: 0;
}
.tag_object .tag_object
{ {
font-size: 0.9em; font-size: 0.9em;

View file

@ -81,4 +81,43 @@ function bind_box_to_button(box, button)
button.click(); button.click();
} }
}; };
}
function entry_with_history_hook(box, button)
{
//console.log(event.keyCode);
if (box.entry_history === undefined)
{box.entry_history = [];}
if (box.entry_history_pos === undefined)
{box.entry_history_pos = -1;}
if (event.keyCode == 13)
{
/* Enter */
box.entry_history.push(box.value);
button.click();
box.value = "";
}
else if (event.keyCode == 38)
{
/* Up arrow */
if (box.entry_history.length == 0)
{return}
if (box.entry_history_pos == -1)
{
box.entry_history_pos = box.entry_history.length - 1;
}
else if (box.entry_history_pos > 0)
{
box.entry_history_pos -= 1;
}
box.value = box.entry_history[box.entry_history_pos];
}
else if (event.keyCode == 27)
{
box.value = "";
}
else
{
box.entry_history_pos = -1;
}
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -0,0 +1,173 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
width="640"
height="640"
viewBox="0 0 640.00004 639.99999"
id="svg2"
version="1.1">
<defs
id="defs4">
<linearGradient
id="linearGradient4499">
<stop
style="stop-color:#f31010;stop-opacity:1"
offset="0"
id="stop4501" />
<stop
style="stop-color:#ef4646;stop-opacity:1"
offset="1"
id="stop4503" />
</linearGradient>
<linearGradient
id="linearGradient4489">
<stop
style="stop-color:#1d8f0e;stop-opacity:1"
offset="0"
id="stop4491" />
<stop
style="stop-color:#2fee15;stop-opacity:1"
offset="1"
id="stop4493" />
</linearGradient>
<marker
orient="auto"
refY="0"
refX="0"
id="Tail"
style="overflow:visible">
<g
id="g4180"
transform="scale(-1.2,-1.2)"
style="fill:#74440c;fill-opacity:1;stroke:#000000;stroke-opacity:1">
<path
id="path4182"
d="M -3.8048674,-3.9585227 0.54352094,0"
style="fill:#74440c;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.80000001;stroke-linecap:round;stroke-opacity:1" />
<path
id="path4184"
d="M -1.2866832,-3.9585227 3.0617053,0"
style="fill:#74440c;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.80000001;stroke-linecap:round;stroke-opacity:1" />
<path
id="path4186"
d="M 1.3053582,-3.9585227 5.6537466,0"
style="fill:#74440c;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.80000001;stroke-linecap:round;stroke-opacity:1" />
<path
id="path4188"
d="M -3.8048674,4.1775838 0.54352094,0.21974226"
style="fill:#74440c;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.80000001;stroke-linecap:round;stroke-opacity:1" />
<path
id="path4190"
d="M -1.2866832,4.1775838 3.0617053,0.21974226"
style="fill:#74440c;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.80000001;stroke-linecap:round;stroke-opacity:1" />
<path
id="path4192"
d="M 1.3053582,4.1775838 5.6537466,0.21974226"
style="fill:#74440c;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.80000001;stroke-linecap:round;stroke-opacity:1" />
</g>
</marker>
<linearGradient
xlink:href="#linearGradient4489"
id="linearGradient4495"
x1="280.82242"
y1="1056.675"
x2="36.084953"
y2="669.78662"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.3511769,0,0,1.3511769,73.997383,-786.40284)" />
<linearGradient
xlink:href="#linearGradient4499"
id="linearGradient4505"
x1="336.3808"
y1="1003.8561"
x2="105.84174"
y2="647.27222"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.3511769,0,0,1.3511769,73.996343,-786.40298)" />
<linearGradient
id="linearGradient4361">
<stop
id="stop4363"
offset="0"
style="stop-color:#4d4d4d;stop-opacity:1" />
<stop
id="stop4365"
offset="1"
style="stop-color:#000000;stop-opacity:0;" />
</linearGradient>
<linearGradient
gradientTransform="matrix(0.8225722,0,0,1.3309496,35.485561,-282.08894)"
gradientUnits="userSpaceOnUse"
y2="699.85175"
x2="204.01379"
y1="1037.2178"
x1="315.47162"
id="linearGradient4367"
xlink:href="#linearGradient4361" />
<linearGradient
xlink:href="#linearGradient4499"
id="linearGradient4505-3"
x1="336.3808"
y1="1003.8561"
x2="105.84174"
y2="647.27222"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.3511769,0,0,1.3511769,73.996343,-786.40298)" />
<linearGradient
xlink:href="#linearGradient4489"
id="linearGradient4495-0"
x1="280.82242"
y1="1056.675"
x2="36.084953"
y2="669.78662"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.3511769,0,0,1.3511769,73.997383,-786.40284)" />
<linearGradient
xlink:href="#linearGradient4499"
id="linearGradient4505-3-9"
x1="336.3808"
y1="1003.8561"
x2="105.84174"
y2="647.27222"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.220263,0,0,1.220263,66.826923,-710.20939)" />
<linearGradient
xlink:href="#linearGradient4489"
id="linearGradient4495-0-1"
x1="280.82242"
y1="1056.675"
x2="36.084953"
y2="669.78662"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.220263,0,0,1.220263,66.827863,-710.20926)" />
</defs>
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<path
id="path4480"
d="m 573.17315,444.37019 c -48.25919,77.78171 -134.42937,129.57907 -232.69753,129.57907 -151.13202,0 -273.648697,-122.51669 -273.648697,-273.64869 0,-90.63158 44.059897,-170.97205 111.928527,-220.771487 6.51438,-4.779996 13.24791,-9.279034 20.18309,-13.478366 z"
style="opacity:1;fill:url(#linearGradient4505-3-9);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:19.16129684;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
<path
id="circle4482"
d="M 178.24792,78.766505 C 107.30648,130.14717 66.452241,211.94323 66.319393,299.53686 66.319375,450.66888 189.34458,573.94828 340.47661,573.94828 428.2425,573.84456 511.5918,532.18966 562.97034,461.03416 c -46.64082,33.96568 -103.78292,51.78066 -161.48062,51.90097 -151.13198,0 -274.15719,-123.27939 -274.15718,-274.41142 0.095,-57.55121 17.12911,-113.1671 50.91538,-159.757205 z"
style="opacity:1;fill:url(#linearGradient4495-0-1);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:19.16129684;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
<path
style="opacity:1;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:16.25600243;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
d="m 573.17315,444.37019 c -48.25919,77.78171 -134.42937,129.57907 -232.69753,129.57907 -151.13202,0 -273.648697,-122.51669 -273.648697,-273.64869 0,-90.63158 44.059897,-170.97205 111.928527,-220.771487 6.51438,-4.779996 13.24791,-9.279034 20.18309,-13.478366 z"
id="path4252" />
</svg>

After

Width:  |  Height:  |  Size: 7 KiB

View file

@ -3,7 +3,7 @@
<head> <head>
{% import "photo_object.html" as photo_object %} {% import "photo_object.html" as photo_object %}
{% import "header.html" as header %} {% import "header.html" as header %}
<title>Album {{album.title}}</title> <title>Album {{album["title"]}}</title>
<meta charset="UTF-8"> <meta charset="UTF-8">
<link rel="stylesheet" href="/static/common.css"> <link rel="stylesheet" href="/static/common.css">
</head> </head>
@ -18,20 +18,20 @@
<body> <body>
{{header.make_header()}} {{header.make_header()}}
<div id="content_body"> <div id="content_body">
<h2>{{album.title}}</h2> <h2>{{album["title"]}}</h2>
{% set parent=album.parent() %} {% set parent=album["parent"] %}
{% if parent %} {% if parent %}
<h3>Parent: <a href="/album/{{parent.id}}">{{parent.title}}</a></h3> <h3>Parent: <a href="/album/{{parent["id"]}}">{{parent.title}}</a></h3>
{% endif %} {% endif %}
{% if child_albums %} {% if child_albums %}
<h3>Sub-albums</h3> <h3>Sub-albums</h3>
<ul> <ul>
{% for album in child_albums %} {% for album in child_albums %}
<li><a href="/album/{{album.id}}"> <li><a href="/album/{{album["id"]}}">
{% if album.title %} {% if album["title"] %}
{{album.title}} {{album["title"]}}
{% else %} {% else %}
{{album.id}} {{album["id"]}}
{% endif %}</a> {% endif %}</a>
</li> </li>
{% endfor %} {% endfor %}

View file

@ -13,6 +13,7 @@
/* Override common.css */ /* Override common.css */
flex: 1; flex: 1;
height: 100%; height: 100%;
width: 100%;
} }
#left #left
{ {
@ -45,20 +46,23 @@
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
max-height: 100%;
max-width: 100%;
height: 100%; height: 100%;
width: 100%;
} }
.photo_object a .photo_object a
{ {
max-height: 100%;
max-width: 100%;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
max-width: 100%;
max-height: 100%;
} }
.photo_object img .photo_object img
{ {
height: auto; max-height: 100%;
height: 100%; max-width: 100%;
} }
.photo_object audio .photo_object audio
{ {
@ -80,10 +84,10 @@
<!-- TAG INFO --> <!-- TAG INFO -->
<h4>Tags</h4> <h4>Tags</h4>
<ul id="this_tags"> <ul id="this_tags">
{% for tag in tags %} {% for tag in photo['tags'] %}
<li> <li>
<a class="tag_object" href="/search?tag_musts={{tag.name}}">{{tag.qualified_name()}}</a> <a class="tag_object" href="/search?tag_musts={{tag["name"]}}">{{tag["qualified_name"]}}</a>
<button class="remove_tag_button" onclick="remove_photo_tag('{{photo.id}}', '{{tag.name}}', receive_callback);"></button> <button class="remove_tag_button" onclick="remove_photo_tag('{{photo["id"]}}', '{{tag["name"]}}', receive_callback);"></button>
</li> </li>
{% endfor %} {% endfor %}
<li> <li>
@ -95,22 +99,24 @@
<!-- METADATA & DOWNLOAD --> <!-- METADATA & DOWNLOAD -->
<h4>File info</h4> <h4>File info</h4>
<ul id="metadata"> <ul id="metadata">
{% if photo.width %} {% if photo["width"] %}
<li>{{photo.width}}x{{photo.height}} px</li> <li>{{photo["width"]}}x{{photo["height"]}} px</li>
<li>{{photo.ratio}} aspect ratio</li> <li>{{photo["ratio"]}} aspect ratio</li>
<li>{{photo.bytestring()}}</li> <li>{{photo["bytestring"]}}</li>
{% endif %} {% endif %}
<li><a href="/file/{{photo.id}}.{{photo.extension}}?download=1">Download as {{photo.id}}.{{photo.extension}}</a></li> <li>{{photo["duration"]}}</li>
<li><a href="/file/{{photo.id}}.{{photo.extension}}?download=1&original_filename=1">Download as "{{photo.basename}}"</a></li> {% if photo["duration"] %}
{% 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>
</ul> </ul>
<!-- CONTAINING ALBUMS --> <!-- CONTAINING ALBUMS -->
{% set albums=photo.albums() %} {% if photo["albums"] %}
{% if albums %}
<h4>Albums containing this photo</h4> <h4>Albums containing this photo</h4>
<ul id="containing albums"> <ul id="containing albums">
{% for album in albums %} {% for album in photo["albums"] %}
<li><a href="/album/{{album.id}}">{{album.title}}</a></li> <li><a href="/album/{{album["id"]}}">{{album["title"]}}</a></li>
{% endfor %} {% endfor %}
{% endif %} {% endif %}
</ul> </ul>
@ -121,13 +127,14 @@
<div id="right"> <div id="right">
<div class="photo_object"> <div class="photo_object">
{% set filename = photo.id + "." + photo.extension %} {% set filename = photo["id"] + "." + photo["extension"] %}
{% set link = "/file/" + filename %} {% set link = "/file/" + filename %}
{% set mimetype=photo.mimetype() %} {% set mimetype=photo["mimetype"] %}
{% if mimetype == "image" %} {% if mimetype == "image" %}
<a target="_blank" href="{{link}}"><img src="{{link}}"></a> <!-- <a target="_blank" href="{{link}}"><img src="{{link}}"></a> -->
<img src="{{link}}">
{% elif mimetype == "video" %} {% elif mimetype == "video" %}
<video src="{{link}}" controls preload=none {%if photo.thumbnail%}poster="/thumbnail/{{photo.id}}.jpg"{%endif%}></video> <video src="{{link}}" controls preload=none {%if photo["has_thumbnail"]%}poster="/thumbnail/{{photo["id"]}}.jpg"{%endif%}></video>
{% elif mimetype == "audio" %} {% elif mimetype == "audio" %}
<audio src="{{link}}" controls></audio> <audio src="{{link}}" controls></audio>
{% else %} {% else %}
@ -140,10 +147,10 @@
</html> </html>
<script type="text/javascript"> <script type="text/javascript">
var box = document.getElementById('add_tag_textbox'); var add_tag_box = document.getElementById('add_tag_textbox');
var button = document.getElementById('add_tag_button'); var add_tag_button = document.getElementById('add_tag_button');
var message_area = document.getElementById('message_area'); var message_area = document.getElementById('message_area');
bind_box_to_button(box, button); add_tag_box.onkeydown = function(){entry_with_history_hook(add_tag_box, add_tag_button)};
function receive_callback(response) function receive_callback(response)
{ {
@ -170,7 +177,7 @@ function receive_callback(response)
} }
function submit_tag(callback) function submit_tag(callback)
{ {
add_photo_tag('{{photo.id}}', box.value, callback); add_photo_tag('{{photo["id"]}}', add_tag_box.value, callback);
box.value=''; add_tag_box.value = "";
} }
</script> </script>

View file

@ -1,29 +1,47 @@
{% set basics =
{
"audio": "audio",
"txt": "txt",
"video": "video",
}
%}
{% macro galleryview(photo) %} {% macro galleryview(photo) %}
<div class="photo_galleryview"> <div class="photo_galleryview">
<div class="photo_galleryview_thumb"> <div class="photo_galleryview_thumb">
<a target="_blank" href="/photo/{{photo.id}}"> <a target="_blank" href="/photo/{{photo["id"]}}">
<img height="150" <img height="150"
{% if photo.thumbnail %} {% if photo["has_thumbnail"] %}
src="/thumbnail/{{photo.id}}.jpg" src="/thumbnail/{{photo["id"]}}.jpg"
{% else %} {% else %}
{% set mimetype = photo.mimetype() %} {% set choice =
{% if mimetype == 'video' %} photo['extension'] if photo['extension'] in basics else
src="/static/basic_thumbnails/video.png" photo['mimetype'] if photo['mimetype'] in basics else
{% elif mimetype == 'audio' %} 'other'
src="/static/basic_thumbnails/audio.png" %}
{% else %} src="/static/basic_thumbnails/{{choice}}.png"
src="/static/basic_thumbnails/other.png"
{% endif %}
{% endif %} {% endif %}
</a> </a>
</div> </div>
<div class="photo_galleryview_info"> <div class="photo_galleryview_info">
<a target="_blank" href="/photo/{{photo.id}}">{{photo.basename}}</a> <a target="_blank" href="/photo/{{photo["id"]}}">{{photo["filename"]}}</a>
<span> <span class="photo_galleryview_file_metadata">
{% if photo.width %} {% if photo["width"] %}
{{photo.width}}x{{photo.height}} {{photo["width"]}}x{{photo["height"]}},
{% endif %} {% endif %}
{{photo.bytestring()}} {% if photo["duration"] %}
{{photo["duration"]}},
{% endif %}
{{photo["bytestring"]}}
</span>
<span class="photo_galleryview_tags">
{% if photo["tags"] %}
{% set tags=[] %}
{% for tag in photo["tags"] %}
{% do tags.append(tag["name"]) %}
{% endfor %}
<span title="{{", ".join(tags)}}">T</span>
{% endif %}
</span> </span>
</div> </div>
</div> </div>

View file

@ -13,9 +13,13 @@ a
{ {
width: 50%; width: 50%;
height: 40px; height: 40px;
background-color: #ffffd4; background-color: rgba(0, 0, 0, 0.1);
margin: 8px; margin: 8px;
} }
a:hover
{
background-color: #ffffd4;
}
</style> </style>
<head> <head>

View file

@ -94,14 +94,14 @@ form
{% macro create_orderby_li(selected_column, selected_sorter) %} {% macro create_orderby_li(selected_column, selected_sorter) %}
<li class="search_builder_orderby_li"> <li class="search_builder_orderby_li">
<select> <select>
<option value="created" {%if selected_column=="created"%}selected{%endif%} >Creation date</option> <option value="created" {%if selected_column=="created"%}selected{%endif%} >Creation date</option>
<option value="area" {%if selected_column=="area"%}selected{%endif%} >Area</option> <option value="area" {%if selected_column=="area"%}selected{%endif%} >Area</option>
<option value="width" {%if selected_column=="width"%}selected{%endif%} >Width</option> <option value="width" {%if selected_column=="width"%}selected{%endif%} >Width</option>
<option value="height" {%if selected_column=="height"%}selected{%endif%} >Height</option> <option value="height" {%if selected_column=="height"%}selected{%endif%} >Height</option>
<option value="ratio" {%if selected_column=="ratio"%}selected{%endif%} >Aspect Ratio</option> <option value="ratio" {%if selected_column=="ratio"%}selected{%endif%} >Aspect Ratio</option>
<option value="bytes" {%if selected_column=="bytes"%}selected{%endif%} >File size</option> <option value="bytes" {%if selected_column=="bytes"%}selected{%endif%} >File size</option>
<option value="length" {%if selected_column=="length"%}selected{%endif%} >Duration</option> <option value="duration" {%if selected_column=="duration"%}selected{%endif%} >Duration</option>
<option value="random" {%if selected_column=="random"%}selected{%endif%} >Random</option> <option value="random" {%if selected_column=="random"%}selected{%endif%} >Random</option>
</select> </select>
<select> <select>
<option value="desc" {%if selected_sorter=="desc"%}selected{%endif%} >Descending</option> <option value="desc" {%if selected_sorter=="desc"%}selected{%endif%} >Descending</option>
@ -149,8 +149,7 @@ form
<ul id="search_builder_orderby_ul"> <ul id="search_builder_orderby_ul">
{% if "orderby" in search_kwargs and search_kwargs["orderby"] %} {% if "orderby" in search_kwargs and search_kwargs["orderby"] %}
{% for orderby in search_kwargs["orderby"] %} {% for orderby in search_kwargs["orderby"] %}
{% set column=orderby.split(" ")[0] %} {% set column, sorter=orderby.split(" ") %}
{% set sorter=orderby.split(" ")[1] %}
{{ create_orderby_li(selected_column=column, selected_sorter=sorter) }} {{ create_orderby_li(selected_column=column, selected_sorter=sorter) }}
{% endfor %} {% endfor %}
{% else %} {% else %}
@ -161,24 +160,53 @@ form
</div> </div>
<br> <br>
<form id="search_builder_form" action="" onsubmit="return submit_search();"> <form id="search_builder_form" action="" onsubmit="return submit_search();">
<input type="text"
<span>Min-max values</span>
<input type="text" class="basic_param"
value="{%if search_kwargs['area']%}{{search_kwargs['area']}}{%endif%}"
name="area" placeholder="Area: 1m-2m">
<input type="text" class="basic_param"
value="{%if search_kwargs['width']%}{{search_kwargs['width']}}{%endif%}"
name="width" placeholder="Width: 1k-2k">
<input type="text" class="basic_param"
value="{%if search_kwargs['height']%}{{search_kwargs['height']}}{%endif%}"
name="height" placeholder="Height: 1k-2k">
<input type="text" class="basic_param"
value="{%if search_kwargs['ratio']%}{{search_kwargs['ratio']}}{%endif%}"
name="ratio" placeholder="Aspect Ratio: 1.7-2">
<input type="text" class="basic_param"
value="{%if search_kwargs['bytes']%}{{search_kwargs['bytes']}}{%endif%}"
name="bytes" placeholder="File Size: 1mb-2mb">
<input type="text" class="basic_param"
value="{%if search_kwargs['duration']%}{{search_kwargs['duration']}}{%endif%}"
name="duration" placeholder="Duration: 10:00-20:00">
<br>
<span>Other filters</span>
<input type="text" class="basic_param"
value="{%if search_kwargs['mimetype']%}{{search_kwargs['mimetype']}}{%endif%}" value="{%if search_kwargs['mimetype']%}{{search_kwargs['mimetype']}}{%endif%}"
name="mimetype" placeholder="Mimetype(s)"> name="mimetype" placeholder="Mimetype(s)">
<input type="text" <input type="text" class="basic_param"
value="{%if search_kwargs['extension']%}{{search_kwargs['extension']}}{%endif%}" value="{%if search_kwargs['extension']%}{{search_kwargs['extension']}}{%endif%}"
name="extension" placeholder="Extension(s)"> name="extension" placeholder="Extension(s)">
<input type="text" <input type="text" class="basic_param"
value="{%if search_kwargs['extension_not']%}{{search_kwargs['extension_not']}}{%endif%}" value="{%if search_kwargs['extension_not']%}{{search_kwargs['extension_not']}}{%endif%}"
name="extension_not" placeholder="Forbid extension(s)"> name="extension_not" placeholder="Forbid extension(s)">
<select name="limit"> <select name="limit" class="basic_param">
<option value="20" {%if search_kwargs['limit'] == 20%}selected{%endif%}>20 items</option> <option value="20" {%if search_kwargs['limit'] == 20%}selected{%endif%}>20 items</option>
<option value="50" {%if search_kwargs['limit'] == 50%}selected{%endif%}>50 items</option> <option value="50" {%if search_kwargs['limit'] == 50%}selected{%endif%}>50 items</option>
<option value="100" {%if search_kwargs['limit'] == 100%}selected{%endif%}>100 items</option> <option value="100" {%if search_kwargs['limit'] == 100%}selected{%endif%}>100 items</option>
</select> </select>
<select name="has_tags"> <select name="has_tags" class="basic_param">
<option value="" {%if search_kwargs['has_tags'] == None %}selected{%endif%}>Tagged and untagged</option> <option value="" {%if search_kwargs['has_tags'] == None %}selected{%endif%}>Tagged and untagged</option>
<option value="yes"{%if search_kwargs['has_tags'] == True %}selected{%endif%}>Tagged only</option> <option value="yes"{%if search_kwargs['has_tags'] == True %}selected{%endif%}>Tagged only</option>
<option value="no" {%if search_kwargs['has_tags'] == False %}selected{%endif%}>Untagged only</option> <option value="no" {%if search_kwargs['has_tags'] == False %}selected{%endif%}>Untagged only</option>
@ -189,7 +217,7 @@ form
<span>Tags on this page (click to join query):</span> <span>Tags on this page (click to join query):</span>
<ul> <ul>
{% for tag in total_tags %} {% for tag in total_tags %}
<li><a href="" class="tag_object">{{tag._cached_qualname}}</a></li> <li><a href="" class="tag_object">{{tag}}</a></li>
{% endfor %} {% endfor %}
</ul> </ul>
{% endif %} {% endif %}
@ -314,6 +342,18 @@ function orderby_remove_hook(button)
ul.removeChild(li); ul.removeChild(li);
} }
} }
function simplify_tagnames(tags)
{
var new_tags = [];
for (var index = 0; index < tags.length; index += 1)
{
var tag = tags[index];
tag = tag.split(".");
tag = tag[tag.length - 1];
new_tags.push(tag);
}
return new_tags;
}
function submit_search() function submit_search()
{ {
/* /*
@ -322,13 +362,13 @@ function submit_search()
var url = window.location.origin + "/search"; var url = window.location.origin + "/search";
var parameters = []; var parameters = [];
var has_tag_params = false; var has_tag_params = false;
var musts = inputted_musts.join(","); var musts = simplify_tagnames(inputted_musts).join(",");
if (musts) {parameters.push("tag_musts=" + musts); has_tag_params=true;} if (musts) {parameters.push("tag_musts=" + musts); has_tag_params=true;}
var mays = inputted_mays.join(","); var mays = simplify_tagnames(inputted_mays).join(",");
if (mays) {parameters.push("tag_mays=" + mays); has_tag_params=true;} if (mays) {parameters.push("tag_mays=" + mays); has_tag_params=true;}
var forbids = inputted_forbids.join(","); var forbids = simplify_tagnames(inputted_forbids).join(",");
if (forbids) {parameters.push("tag_forbids=" + forbids); has_tag_params=true;} if (forbids) {parameters.push("tag_forbids=" + forbids); has_tag_params=true;}
var expression = document.getElementsByName("tag_expression")[0].value; var expression = document.getElementsByName("tag_expression")[0].value;
@ -339,10 +379,10 @@ function submit_search()
has_tag_params=true; has_tag_params=true;
} }
var basic_inputs = ["mimetype", "extension", "extension_not", "limit", "has_tags"]; var basic_inputs = document.getElementsByClassName("basic_param");
for (var index = 0; index < basic_inputs.length; index += 1) for (var index = 0; index < basic_inputs.length; index += 1)
{ {
var boxname = basic_inputs[index]; var boxname = basic_inputs[index].name;
var box = document.getElementsByName(boxname)[0]; var box = document.getElementsByName(boxname)[0];
var value = box.value; var value = box.value;
if (boxname == "has_tags" && has_tag_params && value == "no") if (boxname == "has_tags" && has_tag_params && value == "no")
@ -414,8 +454,8 @@ function tag_input_hook(box, inputted_list, li_class)
value = value.split("."); value = value.split(".");
value = value[value.length-1]; value = value[value.length-1];
value = value.split("+")[0]; value = value.split("+")[0];
value = value.replace(" ", "_"); value = value.replace(new RegExp(" ", 'g'), "_");
value = value.replace("-", "_"); value = value.replace(new RegExp("-", 'g'), "_");
if (!(value in QUALNAME_MAP)) if (!(value in QUALNAME_MAP))
{ {
return; return;

View file

@ -91,7 +91,7 @@ body
var box = document.getElementById('add_tag_textbox'); var box = document.getElementById('add_tag_textbox');
var button = document.getElementById('add_tag_button'); var button = document.getElementById('add_tag_button');
var message_area = document.getElementById('message_area'); var message_area = document.getElementById('message_area');
bind_box_to_button(box, button); box.onkeydown = function(){entry_with_history_hook(box, button)};
function receive_callback(responses) function receive_callback(responses)
{ {
@ -115,6 +115,9 @@ function receive_callback(responses)
if (action == "new_tag") if (action == "new_tag")
{message_text = "Created tag " + tagname;} {message_text = "Created tag " + tagname;}
else if (action == "new_synonym")
{message_text = "New synonym " + tagname;}
else if (action == "existing_tag") else if (action == "existing_tag")
{message_text = "Existing tag " + tagname;} {message_text = "Existing tag " + tagname;}
@ -137,6 +140,6 @@ function receive_callback(responses)
function submit_tag(callback) function submit_tag(callback)
{ {
create_tag(box.value, callback); create_tag(box.value, callback);
box.value=''; box.value = "";
} }
</script> </script>

View file

@ -1,12 +1,13 @@
import os import os
import phototagger import phototagger
import unittest import unittest
import random
class PhotoDBTest(unittest.TestCase): class PhotoDBTest(unittest.TestCase):
def setUp(self): def setUp(self):
self.P = phototagger.PhotoDB(':memory:') self.P = phototagger.PhotoDB(':memory:')
class AlbumTest(PhotoDBTest): class AlbumTest(PhotoDBTest):
''' '''
Test the creation and properties of albums Test the creation and properties of albums
@ -55,6 +56,7 @@ class PhotoTest(PhotoDBTest):
def test_reload_metadata(self): def test_reload_metadata(self):
pass pass
class TagTest(PhotoDBTest): class TagTest(PhotoDBTest):
''' '''
Test the creation and properties of tags Test the creation and properties of tags
@ -89,6 +91,7 @@ class TagTest(PhotoDBTest):
self.assertRaises(phototagger.TagTooShort, tag.rename, '??') self.assertRaises(phototagger.TagTooShort, tag.rename, '??')
tag.rename(tag.name) # does nothing tag.rename(tag.name) # does nothing
class SearchTest(PhotoDBTest): class SearchTest(PhotoDBTest):
def search_extension(self): def search_extension(self):
pass pass
@ -99,6 +102,7 @@ class SearchTest(PhotoDBTest):
def search_tags(self): def search_tags(self):
pass pass
class SynonymTest(PhotoDBTest): class SynonymTest(PhotoDBTest):
''' '''
Test the creation and management of synonyms Test the creation and management of synonyms
@ -136,6 +140,7 @@ class SynonymTest(PhotoDBTest):
tag.add_synonym('test get syns3') tag.add_synonym('test get syns3')
self.assertEqual(len(tag.synonyms()), 3) self.assertEqual(len(tag.synonyms()), 3)
class AlbumGroupTest(PhotoDBTest): class AlbumGroupTest(PhotoDBTest):
''' '''
Test the relationships between albums as they form and leave groups Test the relationships between albums as they form and leave groups
@ -155,6 +160,7 @@ class AlbumGroupTest(PhotoDBTest):
def test_album_parents(self): def test_album_parents(self):
pass pass
class TagGroupTest(PhotoDBTest): class TagGroupTest(PhotoDBTest):
''' '''
Test the relationships between tags as they form and leave groups Test the relationships between tags as they form and leave groups
@ -188,6 +194,7 @@ class AlbumPhotoTest(PhotoDBTest):
def test_remove_photo(self): def test_remove_photo(self):
pass pass
class PhotoTagTest(PhotoDBTest): class PhotoTagTest(PhotoDBTest):
''' '''
Test the relationships between photos and tags Test the relationships between photos and tags