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