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
|
||||
|
||||
ALLOWED_ORDERBY_COLUMNS = [
|
||||
'extension',
|
||||
'width',
|
||||
'height',
|
||||
'ratio',
|
||||
'area',
|
||||
'duration',
|
||||
'bytes',
|
||||
'created',
|
||||
'tagged_at',
|
||||
'random',
|
||||
]
|
||||
|
||||
# Errors and warnings
|
||||
ERROR_DATABASE_OUTOFDATE = 'Database is out-of-date. {current} should be {new}. Please use etiquette_upgrader.py'
|
||||
ERROR_INVALID_ACTION = 'Invalid action'
|
||||
|
@ -21,7 +34,7 @@ VALID_TAG_CHARS = string.ascii_lowercase + string.digits + '_'
|
|||
|
||||
DEFAULT_ID_LENGTH = 12
|
||||
DEFAULT_DBNAME = 'phototagger.db'
|
||||
DEFAULT_THUMBDIR = '_etiquette\\site_thumbnails'
|
||||
DEFAULT_DATADIR = '.\\_etiquette'
|
||||
DEFAULT_DIGEST_EXCLUDE_FILES = [
|
||||
DEFAULT_DBNAME,
|
||||
'desktop.ini',
|
||||
|
|
35
etiquette.py
35
etiquette.py
|
@ -12,20 +12,15 @@ import warnings
|
|||
|
||||
import constants
|
||||
import decorators
|
||||
import exceptions
|
||||
import helpers
|
||||
import jsonify
|
||||
import phototagger
|
||||
|
||||
try:
|
||||
sys.path.append('C:\\git\\else\\Bytestring')
|
||||
sys.path.append('C:\\git\\else\\WebstreamZip')
|
||||
import bytestring
|
||||
import webstreamzip
|
||||
except ImportError:
|
||||
# pip install
|
||||
# https://raw.githubusercontent.com/voussoir/else/master/_voussoirkit/voussoirkit.zip
|
||||
from vousoirkit import bytestring
|
||||
from vousoirkit import webstreamzip
|
||||
# pip install
|
||||
# https://raw.githubusercontent.com/voussoir/else/master/_voussoirkit/voussoirkit.zip
|
||||
from voussoirkit import bytestring
|
||||
from voussoirkit import webstreamzip
|
||||
|
||||
site = flask.Flask(__name__)
|
||||
site.config.update(
|
||||
|
@ -61,7 +56,7 @@ def delete_synonym(synonym):
|
|||
synonym = phototagger.normalize_tagname(synonym)
|
||||
try:
|
||||
master_tag = P.get_tag(synonym)
|
||||
except phototagger.NoSuchTag:
|
||||
except exceptions.NoSuchTag:
|
||||
flask.abort(404, 'That synonym doesnt exist')
|
||||
|
||||
if synonym not in master_tag.synonyms():
|
||||
|
@ -79,19 +74,19 @@ def make_json_response(j, *args, **kwargs):
|
|||
def P_album(albumid):
|
||||
try:
|
||||
return P.get_album(albumid)
|
||||
except phototagger.NoSuchAlbum:
|
||||
except exceptions.NoSuchAlbum:
|
||||
flask.abort(404, 'That album doesnt exist')
|
||||
|
||||
def P_photo(photoid):
|
||||
try:
|
||||
return P.get_photo(photoid)
|
||||
except phototagger.NoSuchPhoto:
|
||||
except exceptions.NoSuchPhoto:
|
||||
flask.abort(404, 'That photo doesnt exist')
|
||||
|
||||
def P_tag(tagname):
|
||||
try:
|
||||
return P.get_tag(tagname)
|
||||
except phototagger.NoSuchTag as e:
|
||||
except exceptions.NoSuchTag as e:
|
||||
flask.abort(404, 'That tag doesnt exist: %s' % e)
|
||||
|
||||
def send_file(filepath):
|
||||
|
@ -465,7 +460,7 @@ def get_static(filename):
|
|||
def get_tags_core(specific_tag=None):
|
||||
try:
|
||||
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')
|
||||
tags = tags.split('\n')
|
||||
tags = [t for t in tags if t != '']
|
||||
|
@ -516,7 +511,7 @@ def post_edit_album(albumid):
|
|||
tag = request.form[action].strip()
|
||||
try:
|
||||
tag = P_tag(tag)
|
||||
except phototagger.NoSuchTag:
|
||||
except exceptions.NoSuchTag:
|
||||
response = {'error': 'That tag doesnt exist', 'tagname': tag}
|
||||
return make_json_response(response, status=404)
|
||||
recursive = request.form.get('recursive', False)
|
||||
|
@ -552,7 +547,7 @@ def post_edit_photo(photoid):
|
|||
|
||||
try:
|
||||
tag = P.get_tag(tag)
|
||||
except phototagger.NoSuchTag:
|
||||
except exceptions.NoSuchTag:
|
||||
response = {'error': 'That tag doesnt exist', 'tagname': tag}
|
||||
return make_json_response(response, status=404)
|
||||
|
||||
|
@ -595,11 +590,11 @@ def post_edit_tags():
|
|||
status = 400
|
||||
try:
|
||||
response = method(tag)
|
||||
except phototagger.TagTooShort:
|
||||
except exceptions.TagTooShort:
|
||||
response = {'error': constants.ERROR_TAG_TOO_SHORT, 'tagname': tag}
|
||||
except phototagger.CantSynonymSelf:
|
||||
except exceptions.CantSynonymSelf:
|
||||
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}
|
||||
except ValueError as e:
|
||||
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()
|
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 mimetypes
|
||||
import os
|
||||
|
||||
import exceptions
|
||||
import constants
|
||||
import warnings
|
||||
|
||||
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)
|
||||
|
||||
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):
|
||||
'''
|
||||
Convert hh:mm:ss string to an integer seconds.
|
||||
|
@ -133,3 +173,114 @@ def truthystring(s):
|
|||
if s in {'null', 'none'}:
|
||||
return None
|
||||
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)
|
||||
|
|
369
phototagger.py
369
phototagger.py
|
@ -17,21 +17,14 @@ import warnings
|
|||
|
||||
import constants
|
||||
import decorators
|
||||
import exceptions
|
||||
import helpers
|
||||
|
||||
try:
|
||||
sys.path.append('C:\\git\\else\\Bytestring')
|
||||
sys.path.append('C:\\git\\else\\Pathclass')
|
||||
sys.path.append('C:\\git\\else\\SpinalTap')
|
||||
import bytestring
|
||||
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
|
||||
# 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:
|
||||
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);
|
||||
'''.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):
|
||||
basename = subject.lower()
|
||||
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):
|
||||
'''
|
||||
Return the current UTC timestamp or datetime object.
|
||||
|
@ -376,9 +216,9 @@ def normalize_tagname(tagname):
|
|||
tagname = ''.join(tagname)
|
||||
|
||||
if len(tagname) < constants.MIN_TAG_NAME_LENGTH:
|
||||
raise TagTooShort(tagname)
|
||||
raise exceptions.TagTooShort(tagname)
|
||||
if len(tagname) > constants.MAX_TAG_NAME_LENGTH:
|
||||
raise TagTooLong(tagname)
|
||||
raise exceptions.TagTooLong(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])
|
||||
except KeyError:
|
||||
if warn_bad_tags:
|
||||
warnings.warn(constants.NO_SUCH_TAG.format(tag=token))
|
||||
warnings.warn(constants.WARNING_NO_SUCH_TAG.format(tag=token))
|
||||
else:
|
||||
raise NoSuchTag(token)
|
||||
raise exceptions.NoSuchTag(token)
|
||||
return False
|
||||
operand_stack.append(value)
|
||||
if has_operand:
|
||||
|
@ -573,50 +413,6 @@ def tag_export_totally_flat(tags):
|
|||
result[synonym] = children
|
||||
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)
|
||||
self.cur.execute('SELECT * FROM albums WHERE associated_directory == ?', [filepath])
|
||||
f = self.cur.fetchone()
|
||||
if f is None:
|
||||
raise NoSuchAlbum(filepath)
|
||||
return self.get_album(f[SQL_ALBUM['id']])
|
||||
fetch = self.cur.fetchone()
|
||||
if fetch is None:
|
||||
raise exceptions.NoSuchAlbum(filepath)
|
||||
return self.get_album(fetch[SQL_ALBUM['id']])
|
||||
|
||||
def get_albums(self):
|
||||
yield from self.get_things(thing_type='album')
|
||||
|
||||
def new_album(self,
|
||||
def new_album(
|
||||
self,
|
||||
associated_directory=None,
|
||||
commit=True,
|
||||
description=None,
|
||||
|
@ -691,7 +488,7 @@ class PDBPhotoMixin:
|
|||
self.cur.execute('SELECT * FROM photos WHERE filepath == ?', [filepath])
|
||||
fetch = self.cur.fetchone()
|
||||
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)
|
||||
return photo
|
||||
|
||||
|
@ -706,10 +503,10 @@ class PDBPhotoMixin:
|
|||
temp_cur = self.sql.cursor()
|
||||
temp_cur.execute('SELECT * FROM photos ORDER BY created DESC')
|
||||
while True:
|
||||
f = temp_cur.fetchone()
|
||||
if f is None:
|
||||
fetch = temp_cur.fetchone()
|
||||
if fetch is None:
|
||||
break
|
||||
photo = Photo(self, f)
|
||||
photo = Photo(self, fetch)
|
||||
|
||||
yield photo
|
||||
|
||||
|
@ -750,7 +547,7 @@ class PDBPhotoMixin:
|
|||
database. Tags may be applied now or later.
|
||||
|
||||
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.
|
||||
'''
|
||||
|
@ -759,10 +556,10 @@ class PDBPhotoMixin:
|
|||
if not allow_duplicates:
|
||||
try:
|
||||
existing = self.get_photo_by_path(filename)
|
||||
except NoSuchPhoto:
|
||||
except exceptions.NoSuchPhoto:
|
||||
pass
|
||||
else:
|
||||
exc = PhotoExists(filename, existing)
|
||||
exc = exceptions.PhotoExists(filename, existing)
|
||||
exc.photo = existing
|
||||
raise exc
|
||||
|
||||
|
@ -874,7 +671,7 @@ class PDBPhotoMixin:
|
|||
QUERY OPTIONS
|
||||
warn_bad_tags:
|
||||
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:
|
||||
The maximum number of *successful* results to yield.
|
||||
|
@ -890,18 +687,18 @@ class PDBPhotoMixin:
|
|||
start_time = time.time()
|
||||
maximums = {}
|
||||
minimums = {}
|
||||
_helper_minmax('area', area, minimums, maximums)
|
||||
_helper_minmax('created', created, minimums, maximums)
|
||||
_helper_minmax('width', width, minimums, maximums)
|
||||
_helper_minmax('height', height, minimums, maximums)
|
||||
_helper_minmax('ratio', ratio, minimums, maximums)
|
||||
_helper_minmax('bytes', bytes, minimums, maximums)
|
||||
_helper_minmax('duration', duration, minimums, maximums)
|
||||
helpers._minmax('area', area, minimums, maximums)
|
||||
helpers._minmax('created', created, minimums, maximums)
|
||||
helpers._minmax('width', width, minimums, maximums)
|
||||
helpers._minmax('height', height, minimums, maximums)
|
||||
helpers._minmax('ratio', ratio, minimums, maximums)
|
||||
helpers._minmax('bytes', bytes, minimums, maximums)
|
||||
helpers._minmax('duration', duration, minimums, maximums)
|
||||
orderby = orderby or []
|
||||
|
||||
extension = _helper_extension(extension)
|
||||
extension_not = _helper_extension(extension_not)
|
||||
mimetype = _helper_extension(mimetype)
|
||||
extension = helpers._normalize_extensions(extension)
|
||||
extension_not = helpers._normalize_extensions(extension_not)
|
||||
mimetype = helpers._normalize_extensions(mimetype)
|
||||
|
||||
if filename is not None:
|
||||
if not isinstance(filename, str):
|
||||
|
@ -909,14 +706,14 @@ class PDBPhotoMixin:
|
|||
filename = set(term.lower() for term in filename.strip().split(' '))
|
||||
|
||||
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_mays = _helper_setify(self, tag_mays, warn_bad_tags=warn_bad_tags)
|
||||
tag_forbids = _helper_setify(self, tag_forbids, warn_bad_tags=warn_bad_tags)
|
||||
tag_musts = helpers._setify_tags(photodb=self, tags=tag_musts, warn_bad_tags=warn_bad_tags)
|
||||
tag_mays = helpers._setify_tags(photodb=self, tags=tag_mays, 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'
|
||||
orderby = [_helper_orderby(o) for o in orderby]
|
||||
orderby = [helpers._orderby(o) for o in orderby]
|
||||
orderby = [o for o in orderby if o]
|
||||
if orderby:
|
||||
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.
|
||||
'''
|
||||
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:
|
||||
return self.get_tag_by_id(id)
|
||||
elif name is not None:
|
||||
return self.get_tag_by_name(name)
|
||||
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):
|
||||
return self.get_thing_by_id('tag', thing_id=id)
|
||||
|
@ -1055,7 +852,7 @@ class PDBTagMixin:
|
|||
fetch = self.cur.fetchone()
|
||||
if fetch is None:
|
||||
# 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']]
|
||||
|
||||
def get_tags(self):
|
||||
|
@ -1068,10 +865,10 @@ class PDBTagMixin:
|
|||
tagname = normalize_tagname(tagname)
|
||||
try:
|
||||
self.get_tag_by_name(tagname)
|
||||
except NoSuchTag:
|
||||
except exceptions.NoSuchTag:
|
||||
pass
|
||||
else:
|
||||
raise TagExists(tagname)
|
||||
raise exceptions.TagExists(tagname)
|
||||
|
||||
tagid = self.generate_id('tags')
|
||||
self._cached_frozen_children = None
|
||||
|
@ -1121,10 +918,17 @@ class PhotoDB(PDBAlbumMixin, PDBPhotoMixin, PDBTagMixin):
|
|||
'''
|
||||
def __init__(
|
||||
self,
|
||||
databasename=constants.DEFAULT_DBNAME,
|
||||
thumbnail_folder=constants.DEFAULT_THUMBDIR,
|
||||
id_length=constants.DEFAULT_ID_LENGTH,
|
||||
databasename=None,
|
||||
data_directory=None,
|
||||
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.database_abspath = os.path.abspath(databasename)
|
||||
existing_database = os.path.exists(databasename)
|
||||
|
@ -1143,8 +947,11 @@ class PhotoDB(PDBAlbumMixin, PDBPhotoMixin, PDBTagMixin):
|
|||
for statement in statements:
|
||||
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
|
||||
|
||||
|
@ -1189,7 +996,7 @@ class PhotoDB(PDBAlbumMixin, PDBPhotoMixin, PDBTagMixin):
|
|||
)
|
||||
try:
|
||||
album = self.get_album_by_path(directory.absolute_path)
|
||||
except NoSuchAlbum:
|
||||
except exceptions.NoSuchAlbum:
|
||||
album = self.new_album(
|
||||
associated_directory=directory.absolute_path,
|
||||
commit=False,
|
||||
|
@ -1202,7 +1009,7 @@ class PhotoDB(PDBAlbumMixin, PDBPhotoMixin, PDBTagMixin):
|
|||
if current_album is None:
|
||||
try:
|
||||
current_album = self.get_album_by_path(current_location.absolute_path)
|
||||
except NoSuchAlbum:
|
||||
except exceptions.NoSuchAlbum:
|
||||
current_album = self.new_album(
|
||||
associated_directory=current_location.absolute_path,
|
||||
commit=False,
|
||||
|
@ -1213,13 +1020,13 @@ class PhotoDB(PDBAlbumMixin, PDBPhotoMixin, PDBTagMixin):
|
|||
parent = albums[current_location.parent.absolute_path]
|
||||
try:
|
||||
parent.add(current_album, commit=False)
|
||||
except GroupExists:
|
||||
except exceptions.GroupExists:
|
||||
pass
|
||||
#print('Added to %s' % parent.title)
|
||||
for filepath in files:
|
||||
try:
|
||||
photo = self.new_photo(filepath.absolute_path, commit=False)
|
||||
except PhotoExists as e:
|
||||
except exceptions.PhotoExists as e:
|
||||
photo = e.photo
|
||||
current_album.add_photo(photo, commit=False)
|
||||
|
||||
|
@ -1259,7 +1066,7 @@ class PhotoDB(PDBAlbumMixin, PDBPhotoMixin, PDBTagMixin):
|
|||
filepath = filepath.absolute_path
|
||||
try:
|
||||
photo = self.get_photo_by_path(filepath)
|
||||
except NoSuchPhoto:
|
||||
except exceptions.NoSuchPhoto:
|
||||
pass
|
||||
else:
|
||||
continue
|
||||
|
@ -1282,7 +1089,7 @@ class PhotoDB(PDBAlbumMixin, PDBPhotoMixin, PDBTagMixin):
|
|||
try:
|
||||
item = self.get_tag(name)
|
||||
note = ('existing_tag', item.qualified_name())
|
||||
except NoSuchTag:
|
||||
except exceptions.NoSuchTag:
|
||||
item = self.new_tag(name)
|
||||
note = ('new_tag', item.qualified_name())
|
||||
output_notes.append(note)
|
||||
|
@ -1330,7 +1137,7 @@ class PhotoDB(PDBAlbumMixin, PDBPhotoMixin, PDBTagMixin):
|
|||
lower.join_group(higher)
|
||||
note = ('join_group', '%s.%s' % (higher.name, lower.name))
|
||||
output_notes.append(note)
|
||||
except GroupExists:
|
||||
except exceptions.GroupExists:
|
||||
pass
|
||||
tag = tags[-1]
|
||||
|
||||
|
@ -1340,7 +1147,7 @@ class PhotoDB(PDBAlbumMixin, PDBPhotoMixin, PDBTagMixin):
|
|||
note = ('new_synonym', '%s+%s' % (tag.name, synonym))
|
||||
output_notes.append(note)
|
||||
print('New syn %s' % synonym)
|
||||
except TagExists:
|
||||
except exceptions.TagExists:
|
||||
pass
|
||||
return output_notes
|
||||
|
||||
|
@ -1405,19 +1212,19 @@ class PhotoDB(PDBAlbumMixin, PDBPhotoMixin, PDBTagMixin):
|
|||
'album':
|
||||
{
|
||||
'class': Album,
|
||||
'exception': NoSuchAlbum,
|
||||
'exception': exceptions.NoSuchAlbum,
|
||||
'table': 'albums',
|
||||
},
|
||||
'tag':
|
||||
{
|
||||
'class': Tag,
|
||||
'exception': NoSuchTag,
|
||||
'exception': exceptions.NoSuchTag,
|
||||
'table': 'tags',
|
||||
},
|
||||
'photo':
|
||||
{
|
||||
'class': Photo,
|
||||
'exception': NoSuchPhoto,
|
||||
'exception': exceptions.NoSuchPhoto,
|
||||
'table': 'photos',
|
||||
},
|
||||
}[thing_type]
|
||||
|
@ -1447,7 +1254,7 @@ class GroupableMixin:
|
|||
'''
|
||||
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)):
|
||||
raise TypeError('Member must be of type %s' % type(self))
|
||||
|
@ -1459,7 +1266,7 @@ class GroupableMixin:
|
|||
that_group = self
|
||||
else:
|
||||
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.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():
|
||||
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.
|
||||
'''
|
||||
|
@ -1748,6 +1555,14 @@ class Photo(ObjectBase):
|
|||
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 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:
|
||||
log.debug('Committing - delete photo')
|
||||
self.photodb.commit()
|
||||
|
@ -1856,7 +1671,7 @@ class Photo(ObjectBase):
|
|||
return hopeful_filepath
|
||||
|
||||
def mimetype(self):
|
||||
return get_mimetype(self.real_filepath)
|
||||
return helpers.get_mimetype(self.real_filepath)
|
||||
|
||||
@decorators.time_me
|
||||
def reload_metadata(self, commit=True):
|
||||
|
@ -1979,7 +1794,7 @@ class Photo(ObjectBase):
|
|||
else:
|
||||
queue_action = {'action': os.remove, 'args': [old_path.absolute_path]}
|
||||
self.photodb.on_commit_queue.append(queue_action)
|
||||
|
||||
|
||||
self.__reinit__()
|
||||
|
||||
def tags(self):
|
||||
|
@ -2036,11 +1851,11 @@ class Tag(ObjectBase, GroupableMixin):
|
|||
raise ValueError('Cannot assign synonym to itself.')
|
||||
|
||||
try:
|
||||
tag = self.photodb.get_tag_by_name(synname)
|
||||
except NoSuchTag:
|
||||
self.photodb.get_tag_by_name(synname)
|
||||
except exceptions.NoSuchTag:
|
||||
pass
|
||||
else:
|
||||
raise TagExists(synname)
|
||||
raise exceptions.TagExists(synname)
|
||||
|
||||
self.photodb._cached_frozen_children = None
|
||||
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:
|
||||
return self._cached_qualified_name
|
||||
string = self.name
|
||||
qualname = self.name
|
||||
for parent in self.walk_parents():
|
||||
string = parent.name + '.' + string
|
||||
self._cached_qualified_name = string
|
||||
return string
|
||||
qualname = parent.name + '.' + qualname
|
||||
self._cached_qualified_name = qualname
|
||||
return qualname
|
||||
|
||||
def remove_synonym(self, synname, commit=True):
|
||||
'''
|
||||
|
@ -2137,10 +1952,10 @@ class Tag(ObjectBase, GroupableMixin):
|
|||
|
||||
try:
|
||||
self.photodb.get_tag(new_name)
|
||||
except NoSuchTag:
|
||||
except exceptions.NoSuchTag:
|
||||
pass
|
||||
else:
|
||||
raise TagExists(new_name)
|
||||
raise exceptions.TagExists(new_name)
|
||||
|
||||
self._cached_qualified_name = None
|
||||
self.photodb._cached_frozen_children = None
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
flask
|
||||
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