create exceptions.py and move more constants
This commit is contained in:
parent
2b34854910
commit
1ecd1f979e
8 changed files with 343 additions and 300 deletions
21
README.md
Normal file
21
README.md
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
Etiquette
|
||||||
|
=========
|
||||||
|
|
||||||
|
This is the readme file.
|
||||||
|
|
||||||
|
### Changelog
|
||||||
|
|
||||||
|
- **[addition]** A new feature was added.
|
||||||
|
- **[bugfix]** Incorrect behavior was fixed.
|
||||||
|
- **[change]** An existing feature was slightly modified or parameters were renamed.
|
||||||
|
- **[cleanup]** Code was improved, comments were added, or other changes with minor impact on the interface.
|
||||||
|
- **[removal]** An old feature was removed.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
- 2016 11 28
|
||||||
|
- **[addition]** Added `etiquette_upgrader.py`. When an update causes the anatomy of the etiquette database to change, I will increment the `phototagger.DATABASE_VERSION` variable, and add a new function to this script that should automatically make all the necessary changes. Until the database is upgraded, phototagger will not start. Don't forget to make backups just in case.
|
||||||
|
|
||||||
|
- 2016 11 05
|
||||||
|
- **[addition]** Added the ability to download an album as a `.tar` file. No compression is used. I still need to do more experiments to make sure this is working perfectly.
|
||||||
|
|
15
constants.py
15
constants.py
|
@ -1,5 +1,18 @@
|
||||||
import string
|
import string
|
||||||
|
|
||||||
|
ALLOWED_ORDERBY_COLUMNS = [
|
||||||
|
'extension',
|
||||||
|
'width',
|
||||||
|
'height',
|
||||||
|
'ratio',
|
||||||
|
'area',
|
||||||
|
'duration',
|
||||||
|
'bytes',
|
||||||
|
'created',
|
||||||
|
'tagged_at',
|
||||||
|
'random',
|
||||||
|
]
|
||||||
|
|
||||||
# Errors and warnings
|
# Errors and warnings
|
||||||
ERROR_DATABASE_OUTOFDATE = 'Database is out-of-date. {current} should be {new}. Please use etiquette_upgrader.py'
|
ERROR_DATABASE_OUTOFDATE = 'Database is out-of-date. {current} should be {new}. Please use etiquette_upgrader.py'
|
||||||
ERROR_INVALID_ACTION = 'Invalid action'
|
ERROR_INVALID_ACTION = 'Invalid action'
|
||||||
|
@ -21,7 +34,7 @@ VALID_TAG_CHARS = string.ascii_lowercase + string.digits + '_'
|
||||||
|
|
||||||
DEFAULT_ID_LENGTH = 12
|
DEFAULT_ID_LENGTH = 12
|
||||||
DEFAULT_DBNAME = 'phototagger.db'
|
DEFAULT_DBNAME = 'phototagger.db'
|
||||||
DEFAULT_THUMBDIR = '_etiquette\\site_thumbnails'
|
DEFAULT_DATADIR = '.\\_etiquette'
|
||||||
DEFAULT_DIGEST_EXCLUDE_FILES = [
|
DEFAULT_DIGEST_EXCLUDE_FILES = [
|
||||||
DEFAULT_DBNAME,
|
DEFAULT_DBNAME,
|
||||||
'desktop.ini',
|
'desktop.ini',
|
||||||
|
|
35
etiquette.py
35
etiquette.py
|
@ -12,20 +12,15 @@ import warnings
|
||||||
|
|
||||||
import constants
|
import constants
|
||||||
import decorators
|
import decorators
|
||||||
|
import exceptions
|
||||||
import helpers
|
import helpers
|
||||||
import jsonify
|
import jsonify
|
||||||
import phototagger
|
import phototagger
|
||||||
|
|
||||||
try:
|
# pip install
|
||||||
sys.path.append('C:\\git\\else\\Bytestring')
|
# https://raw.githubusercontent.com/voussoir/else/master/_voussoirkit/voussoirkit.zip
|
||||||
sys.path.append('C:\\git\\else\\WebstreamZip')
|
from voussoirkit import bytestring
|
||||||
import bytestring
|
from voussoirkit import webstreamzip
|
||||||
import webstreamzip
|
|
||||||
except ImportError:
|
|
||||||
# pip install
|
|
||||||
# https://raw.githubusercontent.com/voussoir/else/master/_voussoirkit/voussoirkit.zip
|
|
||||||
from vousoirkit import bytestring
|
|
||||||
from vousoirkit import webstreamzip
|
|
||||||
|
|
||||||
site = flask.Flask(__name__)
|
site = flask.Flask(__name__)
|
||||||
site.config.update(
|
site.config.update(
|
||||||
|
@ -61,7 +56,7 @@ def delete_synonym(synonym):
|
||||||
synonym = phototagger.normalize_tagname(synonym)
|
synonym = phototagger.normalize_tagname(synonym)
|
||||||
try:
|
try:
|
||||||
master_tag = P.get_tag(synonym)
|
master_tag = P.get_tag(synonym)
|
||||||
except phototagger.NoSuchTag:
|
except exceptions.NoSuchTag:
|
||||||
flask.abort(404, 'That synonym doesnt exist')
|
flask.abort(404, 'That synonym doesnt exist')
|
||||||
|
|
||||||
if synonym not in master_tag.synonyms():
|
if synonym not in master_tag.synonyms():
|
||||||
|
@ -79,19 +74,19 @@ def make_json_response(j, *args, **kwargs):
|
||||||
def P_album(albumid):
|
def P_album(albumid):
|
||||||
try:
|
try:
|
||||||
return P.get_album(albumid)
|
return P.get_album(albumid)
|
||||||
except phototagger.NoSuchAlbum:
|
except exceptions.NoSuchAlbum:
|
||||||
flask.abort(404, 'That album doesnt exist')
|
flask.abort(404, 'That album doesnt exist')
|
||||||
|
|
||||||
def P_photo(photoid):
|
def P_photo(photoid):
|
||||||
try:
|
try:
|
||||||
return P.get_photo(photoid)
|
return P.get_photo(photoid)
|
||||||
except phototagger.NoSuchPhoto:
|
except exceptions.NoSuchPhoto:
|
||||||
flask.abort(404, 'That photo doesnt exist')
|
flask.abort(404, 'That photo doesnt exist')
|
||||||
|
|
||||||
def P_tag(tagname):
|
def P_tag(tagname):
|
||||||
try:
|
try:
|
||||||
return P.get_tag(tagname)
|
return P.get_tag(tagname)
|
||||||
except phototagger.NoSuchTag as e:
|
except exceptions.NoSuchTag as e:
|
||||||
flask.abort(404, 'That tag doesnt exist: %s' % e)
|
flask.abort(404, 'That tag doesnt exist: %s' % e)
|
||||||
|
|
||||||
def send_file(filepath):
|
def send_file(filepath):
|
||||||
|
@ -465,7 +460,7 @@ def get_static(filename):
|
||||||
def get_tags_core(specific_tag=None):
|
def get_tags_core(specific_tag=None):
|
||||||
try:
|
try:
|
||||||
tags = P.export_tags(phototagger.tag_export_easybake, specific_tag=specific_tag)
|
tags = P.export_tags(phototagger.tag_export_easybake, specific_tag=specific_tag)
|
||||||
except phototagger.NoSuchTag:
|
except exceptions.NoSuchTag:
|
||||||
flask.abort(404, 'That tag doesnt exist')
|
flask.abort(404, 'That tag doesnt exist')
|
||||||
tags = tags.split('\n')
|
tags = tags.split('\n')
|
||||||
tags = [t for t in tags if t != '']
|
tags = [t for t in tags if t != '']
|
||||||
|
@ -516,7 +511,7 @@ def post_edit_album(albumid):
|
||||||
tag = request.form[action].strip()
|
tag = request.form[action].strip()
|
||||||
try:
|
try:
|
||||||
tag = P_tag(tag)
|
tag = P_tag(tag)
|
||||||
except phototagger.NoSuchTag:
|
except exceptions.NoSuchTag:
|
||||||
response = {'error': 'That tag doesnt exist', 'tagname': tag}
|
response = {'error': 'That tag doesnt exist', 'tagname': tag}
|
||||||
return make_json_response(response, status=404)
|
return make_json_response(response, status=404)
|
||||||
recursive = request.form.get('recursive', False)
|
recursive = request.form.get('recursive', False)
|
||||||
|
@ -552,7 +547,7 @@ def post_edit_photo(photoid):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
tag = P.get_tag(tag)
|
tag = P.get_tag(tag)
|
||||||
except phototagger.NoSuchTag:
|
except exceptions.NoSuchTag:
|
||||||
response = {'error': 'That tag doesnt exist', 'tagname': tag}
|
response = {'error': 'That tag doesnt exist', 'tagname': tag}
|
||||||
return make_json_response(response, status=404)
|
return make_json_response(response, status=404)
|
||||||
|
|
||||||
|
@ -595,11 +590,11 @@ def post_edit_tags():
|
||||||
status = 400
|
status = 400
|
||||||
try:
|
try:
|
||||||
response = method(tag)
|
response = method(tag)
|
||||||
except phototagger.TagTooShort:
|
except exceptions.TagTooShort:
|
||||||
response = {'error': constants.ERROR_TAG_TOO_SHORT, 'tagname': tag}
|
response = {'error': constants.ERROR_TAG_TOO_SHORT, 'tagname': tag}
|
||||||
except phototagger.CantSynonymSelf:
|
except exceptions.CantSynonymSelf:
|
||||||
response = {'error': constants.ERROR_SYNONYM_ITSELF, 'tagname': tag}
|
response = {'error': constants.ERROR_SYNONYM_ITSELF, 'tagname': tag}
|
||||||
except phototagger.NoSuchTag as e:
|
except exceptions.NoSuchTag as e:
|
||||||
response = {'error': constants.ERROR_NO_SUCH_TAG, 'tagname': tag}
|
response = {'error': constants.ERROR_NO_SUCH_TAG, 'tagname': tag}
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
response = {'error': e.args[0], 'tagname': tag}
|
response = {'error': e.args[0], 'tagname': tag}
|
||||||
|
|
|
@ -25,5 +25,5 @@ else:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
print('Starting server')
|
print('Starting server on port %d' % port)
|
||||||
http.serve_forever()
|
http.serve_forever()
|
46
exceptions.py
Normal file
46
exceptions.py
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
class CantSynonymSelf(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class NoSuchAlbum(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class NoSuchGroup(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class NoSuchPhoto(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class NoSuchSynonym(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class NoSuchTag(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class PhotoExists(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class TagExists(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class GroupExists(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class TagTooLong(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class TagTooShort(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class NotExclusive(Exception):
|
||||||
|
'''
|
||||||
|
For when two or more mutually exclusive actions have been requested.
|
||||||
|
'''
|
||||||
|
pass
|
||||||
|
|
||||||
|
class OutOfOrder(Exception):
|
||||||
|
'''
|
||||||
|
For when a requested range (a, b) has b > a
|
||||||
|
'''
|
||||||
|
pass
|
151
helpers.py
151
helpers.py
|
@ -1,6 +1,10 @@
|
||||||
import math
|
import math
|
||||||
|
import mimetypes
|
||||||
|
import os
|
||||||
|
|
||||||
|
import exceptions
|
||||||
import constants
|
import constants
|
||||||
|
import warnings
|
||||||
|
|
||||||
def chunk_sequence(sequence, chunk_length, allow_incomplete=True):
|
def chunk_sequence(sequence, chunk_length, allow_incomplete=True):
|
||||||
'''
|
'''
|
||||||
|
@ -67,6 +71,42 @@ def fit_into_bounds(image_width, image_height, frame_width, frame_height):
|
||||||
|
|
||||||
return (new_width, new_height)
|
return (new_width, new_height)
|
||||||
|
|
||||||
|
def get_mimetype(filepath):
|
||||||
|
extension = os.path.splitext(filepath)[1].replace('.', '')
|
||||||
|
if extension in constants.ADDITIONAL_MIMETYPES:
|
||||||
|
return constants.ADDITIONAL_MIMETYPES[extension]
|
||||||
|
mimetype = mimetypes.guess_type(filepath)[0]
|
||||||
|
if mimetype is not None:
|
||||||
|
mimetype = mimetype.split('/')[0]
|
||||||
|
return mimetype
|
||||||
|
|
||||||
|
def hyphen_range(s):
|
||||||
|
'''
|
||||||
|
Given a string like '1-3', return ints (1, 3) representing lower
|
||||||
|
and upper bounds.
|
||||||
|
|
||||||
|
Supports bytestring.parsebytes and hh:mm:ss format.
|
||||||
|
'''
|
||||||
|
s = s.strip()
|
||||||
|
s = s.replace(' ', '')
|
||||||
|
if not s:
|
||||||
|
return (None, None)
|
||||||
|
parts = s.split('-')
|
||||||
|
parts = [part.strip() or None for part in parts]
|
||||||
|
if len(parts) == 1:
|
||||||
|
low = parts[0]
|
||||||
|
high = None
|
||||||
|
elif len(parts) == 2:
|
||||||
|
(low, high) = parts
|
||||||
|
else:
|
||||||
|
raise ValueError('Too many hyphens')
|
||||||
|
|
||||||
|
low = _unitconvert(low)
|
||||||
|
high = _unitconvert(high)
|
||||||
|
if low is not None and high is not None and low > high:
|
||||||
|
raise exceptions.OutOfOrder(s, low, high)
|
||||||
|
return low, high
|
||||||
|
|
||||||
def hms_to_seconds(hms):
|
def hms_to_seconds(hms):
|
||||||
'''
|
'''
|
||||||
Convert hh:mm:ss string to an integer seconds.
|
Convert hh:mm:ss string to an integer seconds.
|
||||||
|
@ -133,3 +173,114 @@ def truthystring(s):
|
||||||
if s in {'null', 'none'}:
|
if s in {'null', 'none'}:
|
||||||
return None
|
return None
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
#===============================================================================
|
||||||
|
|
||||||
|
def _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.
|
||||||
|
('area', '100-200', {}, {}) --> {'area': 100}, {'area': 200} (MODIFIED IN PLACE)
|
||||||
|
'''
|
||||||
|
if value is None:
|
||||||
|
return
|
||||||
|
if isinstance(value, (int, float)):
|
||||||
|
minimums[key] = value
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
(low, high) = hyphen_range(value)
|
||||||
|
except ValueError:
|
||||||
|
warnings.warn(constants.WARNING_MINMAX_INVALID.format(field=key, value=value))
|
||||||
|
return
|
||||||
|
except exceptions.OutOfOrder as e:
|
||||||
|
warnings.warn(constants.WARNING_MINMAX_OOO.format(field=key, min=e.args[1], max=e.args[2]))
|
||||||
|
return
|
||||||
|
if low is not None:
|
||||||
|
minimums[key] = low
|
||||||
|
if high is not None:
|
||||||
|
maximums[key] = high
|
||||||
|
|
||||||
|
def _normalize_extensions(extensions):
|
||||||
|
'''
|
||||||
|
When searching, this function normalizes the list of inputted extensions.
|
||||||
|
'''
|
||||||
|
if isinstance(extensions, str):
|
||||||
|
extensions = extensions.split()
|
||||||
|
if extensions is None:
|
||||||
|
return set()
|
||||||
|
extensions = [e.lower().strip('.').strip() for e in extensions]
|
||||||
|
extensions = set(e for e in extensions if e)
|
||||||
|
return extensions
|
||||||
|
|
||||||
|
def _orderby(orderby):
|
||||||
|
'''
|
||||||
|
When searching, this function ensures that the user has entered a valid orderby
|
||||||
|
query, and normalizes the query text.
|
||||||
|
|
||||||
|
'random asc' --> ('random', 'asc')
|
||||||
|
'area' --> ('area', 'desc')
|
||||||
|
'''
|
||||||
|
orderby = orderby.lower().strip()
|
||||||
|
if orderby == '':
|
||||||
|
return None
|
||||||
|
|
||||||
|
orderby = orderby.split(' ')
|
||||||
|
if len(orderby) == 2:
|
||||||
|
(column, sorter) = orderby
|
||||||
|
elif len(orderby) == 1:
|
||||||
|
column = orderby[0]
|
||||||
|
sorter = 'desc'
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
#print(column, sorter)
|
||||||
|
if column not in constants.ALLOWED_ORDERBY_COLUMNS:
|
||||||
|
warnings.warn(constants.WARNING_ORDERBY_BADCOL.format(column=column))
|
||||||
|
return None
|
||||||
|
if column == 'random':
|
||||||
|
column = 'RANDOM()'
|
||||||
|
|
||||||
|
if sorter not in ['desc', 'asc']:
|
||||||
|
warnings.warn(constants.WARNING_ORDERBY_BADSORTER.format(column=column, sorter=sorter))
|
||||||
|
sorter = 'desc'
|
||||||
|
return (column, sorter)
|
||||||
|
|
||||||
|
def _setify_tags(photodb, tags, 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 tags is None:
|
||||||
|
return set()
|
||||||
|
|
||||||
|
tagset = set()
|
||||||
|
for tag in tags:
|
||||||
|
tag = tag.strip()
|
||||||
|
if tag == '':
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
tag = photodb.get_tag(tag)
|
||||||
|
tagset.add(tag)
|
||||||
|
except NoSuchTag:
|
||||||
|
if warn_bad_tags:
|
||||||
|
warnings.warn(constants.WARNING_NO_SUCH_TAG.format(tag=tag))
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
|
return tagset
|
||||||
|
|
||||||
|
def _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 helpers.hms_to_seconds(value)
|
||||||
|
elif all(c in '0123456789.' for c in value):
|
||||||
|
return float(value)
|
||||||
|
else:
|
||||||
|
return bytestring.parsebytes(value)
|
||||||
|
|
367
phototagger.py
367
phototagger.py
|
@ -17,21 +17,14 @@ import warnings
|
||||||
|
|
||||||
import constants
|
import constants
|
||||||
import decorators
|
import decorators
|
||||||
|
import exceptions
|
||||||
import helpers
|
import helpers
|
||||||
|
|
||||||
try:
|
# pip install
|
||||||
sys.path.append('C:\\git\\else\\Bytestring')
|
# https://raw.githubusercontent.com/voussoir/else/master/_voussoirkit/voussoirkit.zip
|
||||||
sys.path.append('C:\\git\\else\\Pathclass')
|
from voussoirkit import bytestring
|
||||||
sys.path.append('C:\\git\\else\\SpinalTap')
|
from voussoirkit import pathclass
|
||||||
import bytestring
|
from voussoirkit import spinal
|
||||||
import pathclass
|
|
||||||
import spinal
|
|
||||||
except ImportError:
|
|
||||||
# pip install
|
|
||||||
# https://raw.githubusercontent.com/voussoir/else/master/_voussoirkit/voussoirkit.zip
|
|
||||||
from voussoirkit import bytestring
|
|
||||||
from voussoirkit import pathclass
|
|
||||||
from voussoirkit import spinal
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
ffmpeg = converter.Converter(
|
ffmpeg = converter.Converter(
|
||||||
|
@ -185,164 +178,11 @@ CREATE INDEX IF NOT EXISTS index_grouprel_parentid on tag_group_rel(parentid);
|
||||||
CREATE INDEX IF NOT EXISTS index_grouprel_memberid on tag_group_rel(memberid);
|
CREATE INDEX IF NOT EXISTS index_grouprel_memberid on tag_group_rel(memberid);
|
||||||
'''.format(user_version=DATABASE_VERSION)
|
'''.format(user_version=DATABASE_VERSION)
|
||||||
|
|
||||||
ALLOWED_ORDERBY_COLUMNS = [
|
|
||||||
'extension',
|
|
||||||
'width',
|
|
||||||
'height',
|
|
||||||
'ratio',
|
|
||||||
'area',
|
|
||||||
'duration',
|
|
||||||
'bytes',
|
|
||||||
'created',
|
|
||||||
'tagged_at',
|
|
||||||
'random',
|
|
||||||
]
|
|
||||||
|
|
||||||
def _helper_extension(ext):
|
|
||||||
'''
|
|
||||||
When searching, this function normalizes the list of permissible extensions.
|
|
||||||
'''
|
|
||||||
if isinstance(ext, str):
|
|
||||||
ext = [ext]
|
|
||||||
if ext is None:
|
|
||||||
return set()
|
|
||||||
ext = [e.lower().strip('.') for e in ext]
|
|
||||||
ext = [e for e in ext if e]
|
|
||||||
ext = set(ext)
|
|
||||||
return ext
|
|
||||||
|
|
||||||
def _helper_filenamefilter(subject, terms):
|
def _helper_filenamefilter(subject, terms):
|
||||||
basename = subject.lower()
|
basename = subject.lower()
|
||||||
return all(term in basename for term in terms)
|
return all(term in basename for term in terms)
|
||||||
|
|
||||||
def _helper_minmax(key, value, minimums, maximums):
|
|
||||||
'''
|
|
||||||
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:
|
|
||||||
return
|
|
||||||
if isinstance(value, (int, float)):
|
|
||||||
minimums[key] = value
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
(low, high) = hyphen_range(value)
|
|
||||||
except ValueError:
|
|
||||||
warnings.warn(constants.WARNING_MINMAX_INVALID.format(field=key, value=value))
|
|
||||||
return
|
|
||||||
except OutOfOrder as e:
|
|
||||||
warnings.warn(constants.WARNING_MINMAX_OOO.format(field=key, min=e.args[1], max=e.args[2]))
|
|
||||||
return
|
|
||||||
if low is not None:
|
|
||||||
minimums[key] = low
|
|
||||||
if high is not None:
|
|
||||||
maximums[key] = high
|
|
||||||
|
|
||||||
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()
|
|
||||||
if orderby == '':
|
|
||||||
return None
|
|
||||||
|
|
||||||
orderby = orderby.split(' ')
|
|
||||||
if len(orderby) == 2:
|
|
||||||
(column, sorter) = orderby
|
|
||||||
elif len(orderby) == 1:
|
|
||||||
column = orderby[0]
|
|
||||||
sorter = 'desc'
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
#print(column, sorter)
|
|
||||||
if column not in ALLOWED_ORDERBY_COLUMNS:
|
|
||||||
warnings.warn(constants.WARNING_ORDERBY_BADCOL.format(column=column))
|
|
||||||
return None
|
|
||||||
if column == 'random':
|
|
||||||
column = 'RANDOM()'
|
|
||||||
|
|
||||||
if sorter not in ['desc', 'asc']:
|
|
||||||
warnings.warn(constants.WARNING_ORDERBY_BADSORTER.format(column=column, sorter=sorter))
|
|
||||||
sorter = 'desc'
|
|
||||||
return (column, sorter)
|
|
||||||
|
|
||||||
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:
|
|
||||||
return set()
|
|
||||||
|
|
||||||
s = set()
|
|
||||||
for tag in l:
|
|
||||||
tag = tag.strip()
|
|
||||||
if tag == '':
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
tag = photodb.get_tag(tag)
|
|
||||||
except NoSuchTag:
|
|
||||||
if not warn_bad_tags:
|
|
||||||
raise
|
|
||||||
warnings.warn(constants.WARNING_NO_SUCH_TAG.format(tag=tag))
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
s.add(tag)
|
|
||||||
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 helpers.hms_to_seconds(value)
|
|
||||||
elif all(c in '0123456789.' for c in value):
|
|
||||||
return float(value)
|
|
||||||
else:
|
|
||||||
return bytestring.parsebytes(value)
|
|
||||||
|
|
||||||
def hyphen_range(s):
|
|
||||||
'''
|
|
||||||
Given a string like '1-3', return ints (1, 3) representing lower
|
|
||||||
and upper bounds.
|
|
||||||
|
|
||||||
Supports bytestring.parsebytes and hh:mm:ss format.
|
|
||||||
'''
|
|
||||||
s = s.strip()
|
|
||||||
s = s.replace(' ', '')
|
|
||||||
if not s:
|
|
||||||
return (None, None)
|
|
||||||
parts = s.split('-')
|
|
||||||
parts = [part.strip() or None for part in parts]
|
|
||||||
if len(parts) == 1:
|
|
||||||
low = parts[0]
|
|
||||||
high = None
|
|
||||||
elif len(parts) == 2:
|
|
||||||
(low, high) = parts
|
|
||||||
else:
|
|
||||||
raise ValueError('Too many hyphens')
|
|
||||||
|
|
||||||
low = _helper_unitconvert(low)
|
|
||||||
high = _helper_unitconvert(high)
|
|
||||||
if low is not None and high is not None and low > high:
|
|
||||||
raise OutOfOrder(s, low, high)
|
|
||||||
return low, high
|
|
||||||
|
|
||||||
def get_mimetype(filepath):
|
|
||||||
extension = os.path.splitext(filepath)[1].replace('.', '')
|
|
||||||
if extension in constants.ADDITIONAL_MIMETYPES:
|
|
||||||
return constants.ADDITIONAL_MIMETYPES[extension]
|
|
||||||
mimetype = mimetypes.guess_type(filepath)[0]
|
|
||||||
if mimetype is not None:
|
|
||||||
mimetype = mimetype.split('/')[0]
|
|
||||||
return mimetype
|
|
||||||
|
|
||||||
def getnow(timestamp=True):
|
def getnow(timestamp=True):
|
||||||
'''
|
'''
|
||||||
Return the current UTC timestamp or datetime object.
|
Return the current UTC timestamp or datetime object.
|
||||||
|
@ -376,9 +216,9 @@ def normalize_tagname(tagname):
|
||||||
tagname = ''.join(tagname)
|
tagname = ''.join(tagname)
|
||||||
|
|
||||||
if len(tagname) < constants.MIN_TAG_NAME_LENGTH:
|
if len(tagname) < constants.MIN_TAG_NAME_LENGTH:
|
||||||
raise TagTooShort(tagname)
|
raise exceptions.TagTooShort(tagname)
|
||||||
if len(tagname) > constants.MAX_TAG_NAME_LENGTH:
|
if len(tagname) > constants.MAX_TAG_NAME_LENGTH:
|
||||||
raise TagTooLong(tagname)
|
raise exceptions.TagTooLong(tagname)
|
||||||
|
|
||||||
return tagname
|
return tagname
|
||||||
|
|
||||||
|
@ -437,9 +277,9 @@ def searchfilter_expression(photo_tags, expression, frozen_children, warn_bad_ta
|
||||||
value = any(option in photo_tags for option in frozen_children[token])
|
value = any(option in photo_tags for option in frozen_children[token])
|
||||||
except KeyError:
|
except KeyError:
|
||||||
if warn_bad_tags:
|
if warn_bad_tags:
|
||||||
warnings.warn(constants.NO_SUCH_TAG.format(tag=token))
|
warnings.warn(constants.WARNING_NO_SUCH_TAG.format(tag=token))
|
||||||
else:
|
else:
|
||||||
raise NoSuchTag(token)
|
raise exceptions.NoSuchTag(token)
|
||||||
return False
|
return False
|
||||||
operand_stack.append(value)
|
operand_stack.append(value)
|
||||||
if has_operand:
|
if has_operand:
|
||||||
|
@ -573,50 +413,6 @@ def tag_export_totally_flat(tags):
|
||||||
result[synonym] = children
|
result[synonym] = children
|
||||||
return result
|
return result
|
||||||
|
|
||||||
####################################################################################################
|
|
||||||
####################################################################################################
|
|
||||||
|
|
||||||
class CantSynonymSelf(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
class NoSuchAlbum(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
class NoSuchGroup(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
class NoSuchPhoto(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
class NoSuchSynonym(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
class NoSuchTag(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class PhotoExists(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
class TagExists(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
class GroupExists(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class TagTooLong(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
class TagTooShort(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
class XORException(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
class OutOfOrder(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
####################################################################################################
|
####################################################################################################
|
||||||
####################################################################################################
|
####################################################################################################
|
||||||
|
@ -632,15 +428,16 @@ class PDBAlbumMixin:
|
||||||
'''
|
'''
|
||||||
filepath = os.path.abspath(filepath)
|
filepath = os.path.abspath(filepath)
|
||||||
self.cur.execute('SELECT * FROM albums WHERE associated_directory == ?', [filepath])
|
self.cur.execute('SELECT * FROM albums WHERE associated_directory == ?', [filepath])
|
||||||
f = self.cur.fetchone()
|
fetch = self.cur.fetchone()
|
||||||
if f is None:
|
if fetch is None:
|
||||||
raise NoSuchAlbum(filepath)
|
raise exceptions.NoSuchAlbum(filepath)
|
||||||
return self.get_album(f[SQL_ALBUM['id']])
|
return self.get_album(fetch[SQL_ALBUM['id']])
|
||||||
|
|
||||||
def get_albums(self):
|
def get_albums(self):
|
||||||
yield from self.get_things(thing_type='album')
|
yield from self.get_things(thing_type='album')
|
||||||
|
|
||||||
def new_album(self,
|
def new_album(
|
||||||
|
self,
|
||||||
associated_directory=None,
|
associated_directory=None,
|
||||||
commit=True,
|
commit=True,
|
||||||
description=None,
|
description=None,
|
||||||
|
@ -691,7 +488,7 @@ class PDBPhotoMixin:
|
||||||
self.cur.execute('SELECT * FROM photos WHERE filepath == ?', [filepath])
|
self.cur.execute('SELECT * FROM photos WHERE filepath == ?', [filepath])
|
||||||
fetch = self.cur.fetchone()
|
fetch = self.cur.fetchone()
|
||||||
if fetch is None:
|
if fetch is None:
|
||||||
raise_no_such_thing(NoSuchPhoto, thing_name=filepath)
|
raise_no_such_thing(exceptions.NoSuchPhoto, thing_name=filepath)
|
||||||
photo = Photo(self, fetch)
|
photo = Photo(self, fetch)
|
||||||
return photo
|
return photo
|
||||||
|
|
||||||
|
@ -706,10 +503,10 @@ class PDBPhotoMixin:
|
||||||
temp_cur = self.sql.cursor()
|
temp_cur = self.sql.cursor()
|
||||||
temp_cur.execute('SELECT * FROM photos ORDER BY created DESC')
|
temp_cur.execute('SELECT * FROM photos ORDER BY created DESC')
|
||||||
while True:
|
while True:
|
||||||
f = temp_cur.fetchone()
|
fetch = temp_cur.fetchone()
|
||||||
if f is None:
|
if fetch is None:
|
||||||
break
|
break
|
||||||
photo = Photo(self, f)
|
photo = Photo(self, fetch)
|
||||||
|
|
||||||
yield photo
|
yield photo
|
||||||
|
|
||||||
|
@ -750,7 +547,7 @@ class PDBPhotoMixin:
|
||||||
database. Tags may be applied now or later.
|
database. Tags may be applied now or later.
|
||||||
|
|
||||||
If `allow_duplicates` is False, we will first check the database for any files
|
If `allow_duplicates` is False, we will first check the database for any files
|
||||||
with the same path and raise PhotoExists if found.
|
with the same path and raise exceptions.PhotoExists if found.
|
||||||
|
|
||||||
Returns the Photo object.
|
Returns the Photo object.
|
||||||
'''
|
'''
|
||||||
|
@ -759,10 +556,10 @@ class PDBPhotoMixin:
|
||||||
if not allow_duplicates:
|
if not allow_duplicates:
|
||||||
try:
|
try:
|
||||||
existing = self.get_photo_by_path(filename)
|
existing = self.get_photo_by_path(filename)
|
||||||
except NoSuchPhoto:
|
except exceptions.NoSuchPhoto:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
exc = PhotoExists(filename, existing)
|
exc = exceptions.PhotoExists(filename, existing)
|
||||||
exc.photo = existing
|
exc.photo = existing
|
||||||
raise exc
|
raise exc
|
||||||
|
|
||||||
|
@ -874,7 +671,7 @@ class PDBPhotoMixin:
|
||||||
QUERY OPTIONS
|
QUERY OPTIONS
|
||||||
warn_bad_tags:
|
warn_bad_tags:
|
||||||
If a tag is not found, issue a warning but continue the search.
|
If a tag is not found, issue a warning but continue the search.
|
||||||
Otherwise, a NoSuchTag exception would be raised.
|
Otherwise, a exceptions.NoSuchTag exception would be raised.
|
||||||
|
|
||||||
limit:
|
limit:
|
||||||
The maximum number of *successful* results to yield.
|
The maximum number of *successful* results to yield.
|
||||||
|
@ -890,18 +687,18 @@ class PDBPhotoMixin:
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
maximums = {}
|
maximums = {}
|
||||||
minimums = {}
|
minimums = {}
|
||||||
_helper_minmax('area', area, minimums, maximums)
|
helpers._minmax('area', area, minimums, maximums)
|
||||||
_helper_minmax('created', created, minimums, maximums)
|
helpers._minmax('created', created, minimums, maximums)
|
||||||
_helper_minmax('width', width, minimums, maximums)
|
helpers._minmax('width', width, minimums, maximums)
|
||||||
_helper_minmax('height', height, minimums, maximums)
|
helpers._minmax('height', height, minimums, maximums)
|
||||||
_helper_minmax('ratio', ratio, minimums, maximums)
|
helpers._minmax('ratio', ratio, minimums, maximums)
|
||||||
_helper_minmax('bytes', bytes, minimums, maximums)
|
helpers._minmax('bytes', bytes, minimums, maximums)
|
||||||
_helper_minmax('duration', duration, minimums, maximums)
|
helpers._minmax('duration', duration, minimums, maximums)
|
||||||
orderby = orderby or []
|
orderby = orderby or []
|
||||||
|
|
||||||
extension = _helper_extension(extension)
|
extension = helpers._normalize_extensions(extension)
|
||||||
extension_not = _helper_extension(extension_not)
|
extension_not = helpers._normalize_extensions(extension_not)
|
||||||
mimetype = _helper_extension(mimetype)
|
mimetype = helpers._normalize_extensions(mimetype)
|
||||||
|
|
||||||
if filename is not None:
|
if filename is not None:
|
||||||
if not isinstance(filename, str):
|
if not isinstance(filename, str):
|
||||||
|
@ -909,14 +706,14 @@ class PDBPhotoMixin:
|
||||||
filename = set(term.lower() for term in filename.strip().split(' '))
|
filename = set(term.lower() for term in filename.strip().split(' '))
|
||||||
|
|
||||||
if (tag_musts or tag_mays or tag_forbids) and tag_expression:
|
if (tag_musts or tag_mays or tag_forbids) and tag_expression:
|
||||||
raise XORException('Expression filter cannot be used with musts, mays, forbids')
|
raise exceptions.NotExclusive('Expression filter cannot be used with musts, mays, forbids')
|
||||||
|
|
||||||
tag_musts = _helper_setify(self, tag_musts, warn_bad_tags=warn_bad_tags)
|
tag_musts = helpers._setify_tags(photodb=self, tags=tag_musts, warn_bad_tags=warn_bad_tags)
|
||||||
tag_mays = _helper_setify(self, tag_mays, warn_bad_tags=warn_bad_tags)
|
tag_mays = helpers._setify_tags(photodb=self, tags=tag_mays, warn_bad_tags=warn_bad_tags)
|
||||||
tag_forbids = _helper_setify(self, tag_forbids, warn_bad_tags=warn_bad_tags)
|
tag_forbids = helpers._setify_tags(photodb=self, tags=tag_forbids, warn_bad_tags=warn_bad_tags)
|
||||||
|
|
||||||
query = 'SELECT * FROM photos'
|
query = 'SELECT * FROM photos'
|
||||||
orderby = [_helper_orderby(o) for o in orderby]
|
orderby = [helpers._orderby(o) for o in orderby]
|
||||||
orderby = [o for o in orderby if o]
|
orderby = [o for o in orderby if o]
|
||||||
if orderby:
|
if orderby:
|
||||||
whereable_columns = [o[0] for o in orderby if o[0] != 'RANDOM()']
|
whereable_columns = [o[0] for o in orderby if o[0] != 'RANDOM()']
|
||||||
|
@ -1025,14 +822,14 @@ class PDBTagMixin:
|
||||||
Redirect to get_tag_by_id or get_tag_by_name after xor-checking the parameters.
|
Redirect to get_tag_by_id or get_tag_by_name after xor-checking the parameters.
|
||||||
'''
|
'''
|
||||||
if not helpers.is_xor(id, name):
|
if not helpers.is_xor(id, name):
|
||||||
raise XORException('One and only one of `id`, `name` can be passed.')
|
raise exceptions.NotExclusive('One and only one of `id`, `name` can be passed.')
|
||||||
|
|
||||||
if id is not None:
|
if id is not None:
|
||||||
return self.get_tag_by_id(id)
|
return self.get_tag_by_id(id)
|
||||||
elif name is not None:
|
elif name is not None:
|
||||||
return self.get_tag_by_name(name)
|
return self.get_tag_by_name(name)
|
||||||
else:
|
else:
|
||||||
raise_no_such_thing(NoSuchTag, thing_id=id, thing_name=name)
|
raise_no_such_thing(exceptions.NoSuchTag, thing_id=id, thing_name=name)
|
||||||
|
|
||||||
def get_tag_by_id(self, id):
|
def get_tag_by_id(self, id):
|
||||||
return self.get_thing_by_id('tag', thing_id=id)
|
return self.get_thing_by_id('tag', thing_id=id)
|
||||||
|
@ -1055,7 +852,7 @@ class PDBTagMixin:
|
||||||
fetch = self.cur.fetchone()
|
fetch = self.cur.fetchone()
|
||||||
if fetch is None:
|
if fetch is None:
|
||||||
# was not a top tag or synonym
|
# was not a top tag or synonym
|
||||||
raise_no_such_thing(NoSuchTag, thing_name=tagname)
|
raise_no_such_thing(exceptions.NoSuchTag, thing_name=tagname)
|
||||||
tagname = fetch[SQL_SYN['master']]
|
tagname = fetch[SQL_SYN['master']]
|
||||||
|
|
||||||
def get_tags(self):
|
def get_tags(self):
|
||||||
|
@ -1068,10 +865,10 @@ class PDBTagMixin:
|
||||||
tagname = normalize_tagname(tagname)
|
tagname = normalize_tagname(tagname)
|
||||||
try:
|
try:
|
||||||
self.get_tag_by_name(tagname)
|
self.get_tag_by_name(tagname)
|
||||||
except NoSuchTag:
|
except exceptions.NoSuchTag:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
raise TagExists(tagname)
|
raise exceptions.TagExists(tagname)
|
||||||
|
|
||||||
tagid = self.generate_id('tags')
|
tagid = self.generate_id('tags')
|
||||||
self._cached_frozen_children = None
|
self._cached_frozen_children = None
|
||||||
|
@ -1121,10 +918,17 @@ class PhotoDB(PDBAlbumMixin, PDBPhotoMixin, PDBTagMixin):
|
||||||
'''
|
'''
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
databasename=constants.DEFAULT_DBNAME,
|
databasename=None,
|
||||||
thumbnail_folder=constants.DEFAULT_THUMBDIR,
|
data_directory=None,
|
||||||
id_length=constants.DEFAULT_ID_LENGTH,
|
id_length=None,
|
||||||
):
|
):
|
||||||
|
if databasename is None:
|
||||||
|
databasename = constants.DEFAULT_DBNAME
|
||||||
|
if data_directory is None:
|
||||||
|
data_directory = constants.DEFAULT_DATADIR
|
||||||
|
if id_length is None:
|
||||||
|
id_length = constants.DEFAULT_ID_LENGTH
|
||||||
|
|
||||||
self.databasename = databasename
|
self.databasename = databasename
|
||||||
self.database_abspath = os.path.abspath(databasename)
|
self.database_abspath = os.path.abspath(databasename)
|
||||||
existing_database = os.path.exists(databasename)
|
existing_database = os.path.exists(databasename)
|
||||||
|
@ -1143,8 +947,11 @@ class PhotoDB(PDBAlbumMixin, PDBPhotoMixin, PDBTagMixin):
|
||||||
for statement in statements:
|
for statement in statements:
|
||||||
self.cur.execute(statement)
|
self.cur.execute(statement)
|
||||||
|
|
||||||
self.thumbnail_folder = os.path.abspath(thumbnail_folder)
|
|
||||||
os.makedirs(thumbnail_folder, exist_ok=True)
|
self.data_directory = data_directory
|
||||||
|
self.thumbnail_folder = os.path.join(data_directory, 'site_thumbnails')
|
||||||
|
self.thumbnail_folder = os.path.abspath(self.thumbnail_folder)
|
||||||
|
os.makedirs(self.thumbnail_folder, exist_ok=True)
|
||||||
|
|
||||||
self.id_length = id_length
|
self.id_length = id_length
|
||||||
|
|
||||||
|
@ -1189,7 +996,7 @@ class PhotoDB(PDBAlbumMixin, PDBPhotoMixin, PDBTagMixin):
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
album = self.get_album_by_path(directory.absolute_path)
|
album = self.get_album_by_path(directory.absolute_path)
|
||||||
except NoSuchAlbum:
|
except exceptions.NoSuchAlbum:
|
||||||
album = self.new_album(
|
album = self.new_album(
|
||||||
associated_directory=directory.absolute_path,
|
associated_directory=directory.absolute_path,
|
||||||
commit=False,
|
commit=False,
|
||||||
|
@ -1202,7 +1009,7 @@ class PhotoDB(PDBAlbumMixin, PDBPhotoMixin, PDBTagMixin):
|
||||||
if current_album is None:
|
if current_album is None:
|
||||||
try:
|
try:
|
||||||
current_album = self.get_album_by_path(current_location.absolute_path)
|
current_album = self.get_album_by_path(current_location.absolute_path)
|
||||||
except NoSuchAlbum:
|
except exceptions.NoSuchAlbum:
|
||||||
current_album = self.new_album(
|
current_album = self.new_album(
|
||||||
associated_directory=current_location.absolute_path,
|
associated_directory=current_location.absolute_path,
|
||||||
commit=False,
|
commit=False,
|
||||||
|
@ -1213,13 +1020,13 @@ class PhotoDB(PDBAlbumMixin, PDBPhotoMixin, PDBTagMixin):
|
||||||
parent = albums[current_location.parent.absolute_path]
|
parent = albums[current_location.parent.absolute_path]
|
||||||
try:
|
try:
|
||||||
parent.add(current_album, commit=False)
|
parent.add(current_album, commit=False)
|
||||||
except GroupExists:
|
except exceptions.GroupExists:
|
||||||
pass
|
pass
|
||||||
#print('Added to %s' % parent.title)
|
#print('Added to %s' % parent.title)
|
||||||
for filepath in files:
|
for filepath in files:
|
||||||
try:
|
try:
|
||||||
photo = self.new_photo(filepath.absolute_path, commit=False)
|
photo = self.new_photo(filepath.absolute_path, commit=False)
|
||||||
except PhotoExists as e:
|
except exceptions.PhotoExists as e:
|
||||||
photo = e.photo
|
photo = e.photo
|
||||||
current_album.add_photo(photo, commit=False)
|
current_album.add_photo(photo, commit=False)
|
||||||
|
|
||||||
|
@ -1259,7 +1066,7 @@ class PhotoDB(PDBAlbumMixin, PDBPhotoMixin, PDBTagMixin):
|
||||||
filepath = filepath.absolute_path
|
filepath = filepath.absolute_path
|
||||||
try:
|
try:
|
||||||
photo = self.get_photo_by_path(filepath)
|
photo = self.get_photo_by_path(filepath)
|
||||||
except NoSuchPhoto:
|
except exceptions.NoSuchPhoto:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
continue
|
continue
|
||||||
|
@ -1282,7 +1089,7 @@ class PhotoDB(PDBAlbumMixin, PDBPhotoMixin, PDBTagMixin):
|
||||||
try:
|
try:
|
||||||
item = self.get_tag(name)
|
item = self.get_tag(name)
|
||||||
note = ('existing_tag', item.qualified_name())
|
note = ('existing_tag', item.qualified_name())
|
||||||
except NoSuchTag:
|
except exceptions.NoSuchTag:
|
||||||
item = self.new_tag(name)
|
item = self.new_tag(name)
|
||||||
note = ('new_tag', item.qualified_name())
|
note = ('new_tag', item.qualified_name())
|
||||||
output_notes.append(note)
|
output_notes.append(note)
|
||||||
|
@ -1330,7 +1137,7 @@ class PhotoDB(PDBAlbumMixin, PDBPhotoMixin, PDBTagMixin):
|
||||||
lower.join_group(higher)
|
lower.join_group(higher)
|
||||||
note = ('join_group', '%s.%s' % (higher.name, lower.name))
|
note = ('join_group', '%s.%s' % (higher.name, lower.name))
|
||||||
output_notes.append(note)
|
output_notes.append(note)
|
||||||
except GroupExists:
|
except exceptions.GroupExists:
|
||||||
pass
|
pass
|
||||||
tag = tags[-1]
|
tag = tags[-1]
|
||||||
|
|
||||||
|
@ -1340,7 +1147,7 @@ class PhotoDB(PDBAlbumMixin, PDBPhotoMixin, PDBTagMixin):
|
||||||
note = ('new_synonym', '%s+%s' % (tag.name, synonym))
|
note = ('new_synonym', '%s+%s' % (tag.name, synonym))
|
||||||
output_notes.append(note)
|
output_notes.append(note)
|
||||||
print('New syn %s' % synonym)
|
print('New syn %s' % synonym)
|
||||||
except TagExists:
|
except exceptions.TagExists:
|
||||||
pass
|
pass
|
||||||
return output_notes
|
return output_notes
|
||||||
|
|
||||||
|
@ -1405,19 +1212,19 @@ class PhotoDB(PDBAlbumMixin, PDBPhotoMixin, PDBTagMixin):
|
||||||
'album':
|
'album':
|
||||||
{
|
{
|
||||||
'class': Album,
|
'class': Album,
|
||||||
'exception': NoSuchAlbum,
|
'exception': exceptions.NoSuchAlbum,
|
||||||
'table': 'albums',
|
'table': 'albums',
|
||||||
},
|
},
|
||||||
'tag':
|
'tag':
|
||||||
{
|
{
|
||||||
'class': Tag,
|
'class': Tag,
|
||||||
'exception': NoSuchTag,
|
'exception': exceptions.NoSuchTag,
|
||||||
'table': 'tags',
|
'table': 'tags',
|
||||||
},
|
},
|
||||||
'photo':
|
'photo':
|
||||||
{
|
{
|
||||||
'class': Photo,
|
'class': Photo,
|
||||||
'exception': NoSuchPhoto,
|
'exception': exceptions.NoSuchPhoto,
|
||||||
'table': 'photos',
|
'table': 'photos',
|
||||||
},
|
},
|
||||||
}[thing_type]
|
}[thing_type]
|
||||||
|
@ -1447,7 +1254,7 @@ class GroupableMixin:
|
||||||
'''
|
'''
|
||||||
Add a Tag object to this group.
|
Add a Tag object to this group.
|
||||||
|
|
||||||
If that object is already a member of another group, a GroupExists is raised.
|
If that object is already a member of another group, a exceptions.GroupExists is raised.
|
||||||
'''
|
'''
|
||||||
if not isinstance(member, type(self)):
|
if not isinstance(member, type(self)):
|
||||||
raise TypeError('Member must be of type %s' % type(self))
|
raise TypeError('Member must be of type %s' % type(self))
|
||||||
|
@ -1459,7 +1266,7 @@ class GroupableMixin:
|
||||||
that_group = self
|
that_group = self
|
||||||
else:
|
else:
|
||||||
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 exceptions.GroupExists('%s already in group %s' % (member.name, that_group.name))
|
||||||
|
|
||||||
self.photodb._cached_frozen_children = None
|
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])
|
||||||
|
@ -1740,7 +1547,7 @@ class Photo(ObjectBase):
|
||||||
for tag in other_photo.tags():
|
for tag in other_photo.tags():
|
||||||
self.add_tag(tag)
|
self.add_tag(tag)
|
||||||
|
|
||||||
def delete(self, commit=True):
|
def delete(self, delete_file=False, commit=True):
|
||||||
'''
|
'''
|
||||||
Delete the Photo and its relation to any tags and albums.
|
Delete the Photo and its relation to any tags and albums.
|
||||||
'''
|
'''
|
||||||
|
@ -1748,6 +1555,14 @@ class Photo(ObjectBase):
|
||||||
self.photodb.cur.execute('DELETE FROM photos WHERE id == ?', [self.id])
|
self.photodb.cur.execute('DELETE FROM photos WHERE id == ?', [self.id])
|
||||||
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 delete_file:
|
||||||
|
path = self.real_path.absolute_path
|
||||||
|
if commit:
|
||||||
|
os.remove(path)
|
||||||
|
else:
|
||||||
|
queue_action = {'action': os.remove, 'args': [path]}
|
||||||
|
self.photodb.on_commit_queue.append(queue_action)
|
||||||
if commit:
|
if commit:
|
||||||
log.debug('Committing - delete photo')
|
log.debug('Committing - delete photo')
|
||||||
self.photodb.commit()
|
self.photodb.commit()
|
||||||
|
@ -1856,7 +1671,7 @@ class Photo(ObjectBase):
|
||||||
return hopeful_filepath
|
return hopeful_filepath
|
||||||
|
|
||||||
def mimetype(self):
|
def mimetype(self):
|
||||||
return get_mimetype(self.real_filepath)
|
return helpers.get_mimetype(self.real_filepath)
|
||||||
|
|
||||||
@decorators.time_me
|
@decorators.time_me
|
||||||
def reload_metadata(self, commit=True):
|
def reload_metadata(self, commit=True):
|
||||||
|
@ -2036,11 +1851,11 @@ class Tag(ObjectBase, GroupableMixin):
|
||||||
raise ValueError('Cannot assign synonym to itself.')
|
raise ValueError('Cannot assign synonym to itself.')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
tag = self.photodb.get_tag_by_name(synname)
|
self.photodb.get_tag_by_name(synname)
|
||||||
except NoSuchTag:
|
except exceptions.NoSuchTag:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
raise TagExists(synname)
|
raise exceptions.TagExists(synname)
|
||||||
|
|
||||||
self.photodb._cached_frozen_children = None
|
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])
|
||||||
|
@ -2103,11 +1918,11 @@ class Tag(ObjectBase, GroupableMixin):
|
||||||
'''
|
'''
|
||||||
if self._cached_qualified_name:
|
if self._cached_qualified_name:
|
||||||
return self._cached_qualified_name
|
return self._cached_qualified_name
|
||||||
string = self.name
|
qualname = self.name
|
||||||
for parent in self.walk_parents():
|
for parent in self.walk_parents():
|
||||||
string = parent.name + '.' + string
|
qualname = parent.name + '.' + qualname
|
||||||
self._cached_qualified_name = string
|
self._cached_qualified_name = qualname
|
||||||
return string
|
return qualname
|
||||||
|
|
||||||
def remove_synonym(self, synname, commit=True):
|
def remove_synonym(self, synname, commit=True):
|
||||||
'''
|
'''
|
||||||
|
@ -2137,10 +1952,10 @@ class Tag(ObjectBase, GroupableMixin):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.photodb.get_tag(new_name)
|
self.photodb.get_tag(new_name)
|
||||||
except NoSuchTag:
|
except exceptions.NoSuchTag:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
raise TagExists(new_name)
|
raise exceptions.TagExists(new_name)
|
||||||
|
|
||||||
self._cached_qualified_name = None
|
self._cached_qualified_name = None
|
||||||
self.photodb._cached_frozen_children = None
|
self.photodb._cached_frozen_children = None
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
flask
|
flask
|
||||||
gevent
|
gevent
|
||||||
pillow
|
pillow
|
||||||
|
https://raw.githubusercontent.com/voussoir/else/master/_voussoirkit/voussoirkit.zip
|
||||||
|
git+https://github.com/senko/python-video-converter.git
|
Loading…
Reference in a new issue