512 lines
17 KiB
Python
512 lines
17 KiB
Python
""" Constants used by osxphotos """
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import os.path
|
|
import sqlite3
|
|
from datetime import datetime
|
|
from enum import Enum
|
|
|
|
logger: logging.Logger = logging.getLogger("osxphotos")
|
|
|
|
APP_NAME = "osxphotos"
|
|
|
|
OSXPHOTOS_URL = "https://github.com/RhetTbull/osxphotos"
|
|
|
|
# Time delta: add this to Photos times to get unix time
|
|
# Apple Epoch is Jan 1, 2001
|
|
TIME_DELTA = (datetime(2001, 1, 1, 0, 0) - datetime(1970, 1, 1, 0, 0)).total_seconds()
|
|
|
|
# which Photos library database versions have been tested
|
|
# Photos 2.0 (10.12.6) == 2622
|
|
# Photos 3.0 (10.13.6) == 3301
|
|
# Photos 4.0 (10.14.5) == 4016
|
|
# Photos 4.0 (10.14.6) == 4025
|
|
# Photos 5.0+ (10.15.0) == 6000 or 5001
|
|
_TESTED_DB_VERSIONS = ["6000", "5001", "4025", "4016", "3301", "2622"]
|
|
|
|
# database model versions (applies to Photos 5+)
|
|
# these come from PLModelVersion key in binary plist in Z_METADATA.Z_PLIST
|
|
# Photos 5 (10.15.1) == 13537
|
|
# Photos 5 (10.15.4, 10.15.5, 10.15.6) == 13703
|
|
# Photos 6 (10.16.0 Beta) == 14104
|
|
# Photos 7 (12.0.1) == 15323
|
|
# Photos 8 (13.0.0) == 16320
|
|
# Photos 9 (14.0.0 dev preview) = 17120
|
|
_TEST_MODEL_VERSIONS = ["13537", "13703", "14104", "15323", "16320", "17120"]
|
|
|
|
_PHOTOS_2_VERSION = "2622"
|
|
|
|
# only version 3 - 4 have RKVersion.selfPortrait
|
|
_PHOTOS_3_VERSION = "3301"
|
|
|
|
# versions 5.0 and later have a different database structure
|
|
_PHOTOS_4_VERSION = "4025" # latest Mojave version on 10.14.6
|
|
_PHOTOS_5_VERSION = "5000" # I've seen both 5001 and 6000. 6000 is most common on Catalina and up but there are some version 5001 database in the wild
|
|
|
|
# Ranges for model version by Photos version
|
|
_PHOTOS_5_MODEL_VERSION = [13000, 13999]
|
|
_PHOTOS_6_MODEL_VERSION = [14000, 14999]
|
|
_PHOTOS_7_MODEL_VERSION = [15000, 15999] # Dev preview: 15134, 12.1: 15331
|
|
_PHOTOS_8_MODEL_VERSION = [16000, 16999] # Ventura dev preview: 16119
|
|
_PHOTOS_9_MODEL_VERSION = [17000, 17999] # Sonoma dev preview: 17120
|
|
|
|
# the preview versions of 12.0.0 had a difference schema for syndication info so need to check model version before processing
|
|
_PHOTOS_SYNDICATION_MODEL_VERSION = 15323 # 12.0.1
|
|
|
|
# shared iCloud library versions; dev preview doesn't contain same columns as release version
|
|
_PHOTOS_SHARED_LIBRARY_VERSION = 16320 # 13.0
|
|
|
|
# some table names differ between Photos 5 and later versions
|
|
_DB_TABLE_NAMES = {
|
|
5: {
|
|
"ASSET": "ZGENERICASSET",
|
|
"KEYWORD_JOIN": "Z_1KEYWORDS.Z_37KEYWORDS",
|
|
"ALBUM_JOIN": "Z_26ASSETS.Z_34ASSETS",
|
|
"ALBUM_SORT_ORDER": "Z_26ASSETS.Z_FOK_34ASSETS",
|
|
"IMPORT_FOK": "ZGENERICASSET.Z_FOK_IMPORTSESSION",
|
|
"DEPTH_STATE": "ZGENERICASSET.ZDEPTHSTATES",
|
|
"UTI_ORIGINAL": "ZINTERNALRESOURCE.ZUNIFORMTYPEIDENTIFIER",
|
|
"ASSET_ALBUM_JOIN": "Z_26ASSETS.Z_26ALBUMS",
|
|
"ASSET_ALBUM_TABLE": "Z_26ASSETS",
|
|
"HDR_TYPE": "ZCUSTOMRENDEREDVALUE",
|
|
"DETECTED_FACE_PERSON_FK": "ZDETECTEDFACE.ZPERSON",
|
|
"DETECTED_FACE_ASSET_FK": "ZDETECTEDFACE.ZASSET",
|
|
},
|
|
6: {
|
|
"ASSET": "ZASSET",
|
|
"KEYWORD_JOIN": "Z_1KEYWORDS.Z_36KEYWORDS",
|
|
"ALBUM_JOIN": "Z_26ASSETS.Z_3ASSETS",
|
|
"ALBUM_SORT_ORDER": "Z_26ASSETS.Z_FOK_3ASSETS",
|
|
"IMPORT_FOK": "null",
|
|
"DEPTH_STATE": "ZASSET.ZDEPTHTYPE",
|
|
"UTI_ORIGINAL": "ZINTERNALRESOURCE.ZUNIFORMTYPEIDENTIFIER",
|
|
"ASSET_ALBUM_JOIN": "Z_26ASSETS.Z_26ALBUMS",
|
|
"ASSET_ALBUM_TABLE": "Z_26ASSETS",
|
|
"HDR_TYPE": "ZCUSTOMRENDEREDVALUE",
|
|
"DETECTED_FACE_PERSON_FK": "ZDETECTEDFACE.ZPERSON",
|
|
"DETECTED_FACE_ASSET_FK": "ZDETECTEDFACE.ZASSET",
|
|
},
|
|
7: {
|
|
"ASSET": "ZASSET",
|
|
"KEYWORD_JOIN": "Z_1KEYWORDS.Z_38KEYWORDS",
|
|
"ALBUM_JOIN": "Z_27ASSETS.Z_3ASSETS",
|
|
"ALBUM_SORT_ORDER": "Z_27ASSETS.Z_FOK_3ASSETS",
|
|
"IMPORT_FOK": "null",
|
|
"DEPTH_STATE": "ZASSET.ZDEPTHTYPE",
|
|
"UTI_ORIGINAL": "ZINTERNALRESOURCE.ZCOMPACTUTI",
|
|
"ASSET_ALBUM_JOIN": "Z_27ASSETS.Z_27ALBUMS",
|
|
"ASSET_ALBUM_TABLE": "Z_27ASSETS",
|
|
"HDR_TYPE": "ZHDRTYPE",
|
|
"DETECTED_FACE_PERSON_FK": "ZDETECTEDFACE.ZPERSON",
|
|
"DETECTED_FACE_ASSET_FK": "ZDETECTEDFACE.ZASSET",
|
|
},
|
|
8: {
|
|
"ASSET": "ZASSET",
|
|
"KEYWORD_JOIN": "Z_1KEYWORDS.Z_40KEYWORDS",
|
|
"ALBUM_JOIN": "Z_28ASSETS.Z_3ASSETS",
|
|
"ALBUM_SORT_ORDER": "Z_28ASSETS.Z_FOK_3ASSETS",
|
|
"IMPORT_FOK": "null",
|
|
"DEPTH_STATE": "ZASSET.ZDEPTHTYPE",
|
|
"UTI_ORIGINAL": "ZINTERNALRESOURCE.ZCOMPACTUTI",
|
|
"ASSET_ALBUM_JOIN": "Z_28ASSETS.Z_28ALBUMS",
|
|
"ASSET_ALBUM_TABLE": "Z_28ASSETS",
|
|
"HDR_TYPE": "ZHDRTYPE",
|
|
"DETECTED_FACE_PERSON_FK": "ZDETECTEDFACE.ZPERSON",
|
|
"DETECTED_FACE_ASSET_FK": "ZDETECTEDFACE.ZASSET",
|
|
},
|
|
9: {
|
|
"ASSET": "ZASSET",
|
|
"KEYWORD_JOIN": "Z_1KEYWORDS.Z_40KEYWORDS",
|
|
"ALBUM_JOIN": "Z_28ASSETS.Z_3ASSETS",
|
|
"ALBUM_SORT_ORDER": "Z_28ASSETS.Z_FOK_3ASSETS",
|
|
"IMPORT_FOK": "null",
|
|
"DEPTH_STATE": "ZASSET.ZDEPTHTYPE",
|
|
"UTI_ORIGINAL": "ZINTERNALRESOURCE.ZCOMPACTUTI",
|
|
"ASSET_ALBUM_JOIN": "Z_28ASSETS.Z_28ALBUMS",
|
|
"ASSET_ALBUM_TABLE": "Z_28ASSETS",
|
|
"HDR_TYPE": "ZHDRTYPE",
|
|
"DETECTED_FACE_PERSON_FK": "ZDETECTEDFACE.ZPERSONFORFACE",
|
|
"DETECTED_FACE_ASSET_FK": "ZDETECTEDFACE.ZASSETFORFACE",
|
|
},
|
|
}
|
|
|
|
# which version operating systems have been tested
|
|
_TESTED_OS_VERSIONS = [
|
|
("10", "12"),
|
|
("10", "13"),
|
|
("10", "14"),
|
|
("10", "15"),
|
|
("10", "16"),
|
|
("11", "0"),
|
|
("11", "1"),
|
|
("11", "2"),
|
|
("11", "3"),
|
|
("11", "4"),
|
|
("11", "5"),
|
|
("11", "6"),
|
|
("11", "7"),
|
|
("12", "0"),
|
|
("12", "1"),
|
|
("12", "2"),
|
|
("12", "3"),
|
|
("12", "4"),
|
|
("12", "5"),
|
|
("12", "6"),
|
|
("13", "0"),
|
|
("13", "1"),
|
|
("13", "2"),
|
|
("13", "3"),
|
|
("13", "4"),
|
|
("14", "0"),
|
|
]
|
|
|
|
# Photos 5 has persons who are empty string if unidentified face
|
|
_UNKNOWN_PERSON = "_UNKNOWN_"
|
|
|
|
# photos with no reverse geolocation info (place)
|
|
_UNKNOWN_PLACE = "_UNKNOWN_"
|
|
|
|
_EXIF_TOOL_URL = "https://exiftool.org/"
|
|
|
|
# Where are shared iCloud photos located?
|
|
_PHOTOS_5_SHARED_PHOTO_PATH = "resources/cloudsharing/data"
|
|
_PHOTOS_8_SHARED_PHOTO_PATH = "scopes/cloudsharing/data"
|
|
|
|
# Where are shared iCloud derivatives located?
|
|
_PHOTOS_5_SHARED_DERIVATIVE_PATH = (
|
|
"resources/cloudsharing/resources/derivatives/masters"
|
|
)
|
|
_PHOTOS_8_SHARED_DERIVATIVE_PATH = "scopes/cloudsharing/resources/derivatives/masters"
|
|
|
|
# What type of file? Based on ZGENERICASSET.ZKIND in Photos 5 database
|
|
_PHOTO_TYPE = 0
|
|
_MOVIE_TYPE = 1
|
|
|
|
# Name of XMP template file
|
|
_TEMPLATE_DIR = os.path.join(os.path.dirname(__file__), "templates")
|
|
_XMP_TEMPLATE_NAME = "xmp_sidecar.mako"
|
|
_XMP_TEMPLATE_NAME_BETA = "xmp_sidecar_beta.mako"
|
|
|
|
# Constants used for processing folders and albums
|
|
_PHOTOS_5_ALBUM_KIND = 2 # normal user album
|
|
_PHOTOS_5_SHARED_ALBUM_KIND = 1505 # shared album
|
|
_PHOTOS_5_PROJECT_ALBUM_KIND = 1508 # My Projects (e.g. Calendar, Card, Slideshow)
|
|
_PHOTOS_5_FOLDER_KIND = 4000 # user folder
|
|
_PHOTOS_5_ROOT_FOLDER_KIND = 3999 # root folder
|
|
_PHOTOS_5_IMPORT_SESSION_ALBUM_KIND = 1506 # import session
|
|
|
|
_PHOTOS_4_ALBUM_KIND = 3 # RKAlbum.albumSubclass
|
|
_PHOTOS_4_ALBUM_TYPE_ALBUM = 1 # RKAlbum.albumType
|
|
_PHOTOS_4_ALBUM_TYPE_PROJECT = 9 # RKAlbum.albumType
|
|
_PHOTOS_4_ALBUM_TYPE_SLIDESHOW = 8 # RKAlbum.albumType
|
|
_PHOTOS_4_TOP_LEVEL_ALBUMS = [
|
|
"TopLevelAlbums",
|
|
"TopLevelKeepsakes",
|
|
"TopLevelSlideshows",
|
|
]
|
|
_PHOTOS_4_ROOT_FOLDER = "LibraryFolder"
|
|
|
|
# EXIF related constants
|
|
# max keyword length for IPTC:Keyword, reference
|
|
# https://www.iptc.org/std/photometadata/documentation/userguide/
|
|
_MAX_IPTC_KEYWORD_LEN = 64
|
|
|
|
# Sentinel value for detecting if a template in keyword_template doesn't match
|
|
# If anyone has a keyword matching this, then too bad...
|
|
_OSXPHOTOS_NONE_SENTINEL = "OSXPhotosXYZZY42_Sentinel$"
|
|
|
|
# Lock file extension for reserving filenames when exporting
|
|
_OSXPHOTOS_LOCK_EXTENSION = ".osxphotos.lock"
|
|
|
|
|
|
class SearchCategory:
|
|
"""SearchInfo categories for Photos 5+; corresponds to categories in database/search/psi.sqlite:groups.category
|
|
|
|
Note: This is a simple enum class; the values are not meant to be changed.
|
|
Would be great if Python enums actually let you access the value directly.
|
|
"""
|
|
|
|
LABEL = 2024
|
|
PLACE_NAME = 1
|
|
STREET = 2
|
|
NEIGHBORHOOD = 3
|
|
LOCALITY_4 = 4
|
|
SUB_LOCALITY_5 = 5
|
|
SUB_LOCALITY_6 = 6
|
|
CITY = 7
|
|
LOCALITY_8 = 8
|
|
NAMED_AREA = 9
|
|
ALL_LOCALITY = [
|
|
LOCALITY_4,
|
|
SUB_LOCALITY_5,
|
|
SUB_LOCALITY_6,
|
|
LOCALITY_8,
|
|
NAMED_AREA,
|
|
]
|
|
STATE = 10
|
|
STATE_ABBREVIATION = 11
|
|
COUNTRY = 12
|
|
BODY_OF_WATER = 14
|
|
MONTH = 1014
|
|
YEAR = 1015
|
|
KEYWORDS = 2016
|
|
TITLE = 2017
|
|
DESCRIPTION = 2018
|
|
HOME = 2020
|
|
WORK = 2036
|
|
PERSON = 2021
|
|
ACTIVITY = 2027
|
|
HOLIDAY = 2029
|
|
SEASON = 2030
|
|
VENUE = 2038
|
|
VENUE_TYPE = 2039
|
|
PHOTO_TYPE_VIDEO = 2044
|
|
PHOTO_TYPE_SLOMO = 2045
|
|
PHOTO_TYPE_LIVE = 2046
|
|
PHOTO_TYPE_SCREENSHOT = 2047
|
|
PHOTO_TYPE_PANORAMA = 2048
|
|
PHOTO_TYPE_TIMELAPSE = 2049
|
|
PHOTO_TYPE_BURSTS = 2052
|
|
PHOTO_TYPE_PORTRAIT = 2053
|
|
PHOTO_TYPE_SELFIES = 2054
|
|
PHOTO_TYPE_FAVORITES = 2055
|
|
PHOTO_TYPE_ANIMATED = None # Photos 8+ only
|
|
MEDIA_TYPES = [
|
|
PHOTO_TYPE_VIDEO,
|
|
PHOTO_TYPE_SLOMO,
|
|
PHOTO_TYPE_LIVE,
|
|
PHOTO_TYPE_SCREENSHOT,
|
|
PHOTO_TYPE_PANORAMA,
|
|
PHOTO_TYPE_TIMELAPSE,
|
|
PHOTO_TYPE_BURSTS,
|
|
PHOTO_TYPE_PORTRAIT,
|
|
PHOTO_TYPE_SELFIES,
|
|
PHOTO_TYPE_FAVORITES,
|
|
]
|
|
PHOTO_NAME = 2056
|
|
CAMERA = None # Photos 8+ only
|
|
TEXT_FOUND = None # Photos 8+ only
|
|
DETECTED_TEXT = None # Photos 8+ only
|
|
SOURCE = None # Photos 8+ only
|
|
|
|
@classmethod
|
|
def categories(cls) -> dict[int, str]:
|
|
"""Return categories as dict of value: name"""
|
|
# a bit of a hack to basically reverse the enum
|
|
return {
|
|
value: name
|
|
for name, value in cls.__dict__.items()
|
|
if name is not None
|
|
and not name.startswith("__")
|
|
and not callable(name)
|
|
and name.isupper()
|
|
and not isinstance(value, (list, dict, tuple))
|
|
}
|
|
|
|
|
|
class SearchCategory_Photos8(SearchCategory):
|
|
"""Search categories for Photos 8"""
|
|
|
|
# Many of the category values changed in Ventura / Photos 8
|
|
# and some new categories were added
|
|
CITY = 5
|
|
LOCALITY_4 = 4
|
|
SUB_LOCALITY_5 = None
|
|
SUB_LOCALITY_6 = 6
|
|
LOCALITY_8 = 8
|
|
NAMED_AREA = 7
|
|
ALL_LOCALITY = [
|
|
LOCALITY_4,
|
|
SUB_LOCALITY_6,
|
|
LOCALITY_8,
|
|
NAMED_AREA,
|
|
]
|
|
HOME = 1000
|
|
WORK = 1001
|
|
LABEL = 1500
|
|
MONTH = 1100
|
|
YEAR = 1101
|
|
HOLIDAY = 1103
|
|
SEASON = 1104
|
|
KEYWORDS = 1200
|
|
TITLE = 1201
|
|
DESCRIPTION = 1202
|
|
DETECTED_TEXT = 1203 # new in Photos 8
|
|
TEXT_FOUND = 1205 # new in Photos 8
|
|
PERSON = 1300
|
|
ACTIVITY = 1600
|
|
VENUE = 1700
|
|
VENUE_TYPE = 1701
|
|
PHOTO_TYPE_VIDEO = 1901
|
|
PHOTO_TYPE_SELFIES = 1915
|
|
PHOTO_TYPE_LIVE = 1906
|
|
PHOTO_TYPE_PORTRAIT = 1914
|
|
PHOTO_TYPE_FAVORITES = 2000
|
|
PHOTO_TYPE_PANORAMA = 1908
|
|
PHOTO_TYPE_TIMELAPSE = 1909
|
|
PHOTO_TYPE_SLOMO = 1905
|
|
PHOTO_TYPE_BURSTS = 1913
|
|
PHOTO_TYPE_SCREENSHOT = 1907
|
|
PHOTO_TYPE_ANIMATED = 1912
|
|
PHOTO_TYPE_RAW = 1902
|
|
MEDIA_TYPES = [
|
|
PHOTO_TYPE_VIDEO,
|
|
PHOTO_TYPE_SLOMO,
|
|
PHOTO_TYPE_LIVE,
|
|
PHOTO_TYPE_SCREENSHOT,
|
|
PHOTO_TYPE_PANORAMA,
|
|
PHOTO_TYPE_TIMELAPSE,
|
|
PHOTO_TYPE_BURSTS,
|
|
PHOTO_TYPE_PORTRAIT,
|
|
PHOTO_TYPE_SELFIES,
|
|
PHOTO_TYPE_FAVORITES,
|
|
PHOTO_TYPE_ANIMATED,
|
|
]
|
|
PHOTO_NAME = 2100
|
|
CAMERA = 2300 # new in Photos 8
|
|
SOURCE = 2200 # new in Photos 8, shows the app/software source for the photo, e.g. Messages, Safari, etc.
|
|
|
|
@classmethod
|
|
def categories(cls) -> dict[int, str]:
|
|
"""Return categories as dict of value: name"""
|
|
# need to get the categories from the base class and update with the new values
|
|
classdict = SearchCategory.__dict__.copy()
|
|
classdict |= cls.__dict__.copy()
|
|
return {
|
|
value: name
|
|
for name, value in classdict.items()
|
|
if name is not None
|
|
and not name.startswith("__")
|
|
and not callable(name)
|
|
and name.isupper()
|
|
and not isinstance(value, (list, dict, tuple))
|
|
}
|
|
|
|
|
|
def search_category_factory(version: int) -> SearchCategory:
|
|
"""Return SearchCategory class for Photos version"""
|
|
return SearchCategory_Photos8 if version >= 8 else SearchCategory
|
|
|
|
|
|
# Max filename length on MacOS
|
|
MAX_FILENAME_LEN = 255 - len(_OSXPHOTOS_LOCK_EXTENSION)
|
|
|
|
# Max directory name length on MacOS
|
|
MAX_DIRNAME_LEN = 255
|
|
|
|
# Default JPEG quality when converting to JPEG
|
|
DEFAULT_JPEG_QUALITY = 1.0
|
|
|
|
# Default suffix to add to edited images
|
|
DEFAULT_EDITED_SUFFIX = "_edited"
|
|
|
|
# Default suffix to add to original images
|
|
DEFAULT_ORIGINAL_SUFFIX = ""
|
|
|
|
# Default suffix to add to preview images
|
|
DEFAULT_PREVIEW_SUFFIX = "_preview"
|
|
|
|
# Bit masks for --sidecar
|
|
SIDECAR_JSON = 0x1
|
|
SIDECAR_EXIFTOOL = 0x2
|
|
SIDECAR_XMP = 0x4
|
|
|
|
# supported attributes for --xattr-template
|
|
EXTENDED_ATTRIBUTE_NAMES = [
|
|
"authors",
|
|
"comment",
|
|
"copyright",
|
|
"creator",
|
|
"description",
|
|
"findercomment",
|
|
"headline",
|
|
"participants",
|
|
"projects",
|
|
"starrating",
|
|
"subject",
|
|
"title",
|
|
"version",
|
|
]
|
|
EXTENDED_ATTRIBUTE_NAMES_QUOTED = [f"'{x}'" for x in EXTENDED_ATTRIBUTE_NAMES]
|
|
|
|
|
|
# name of export DB
|
|
OSXPHOTOS_EXPORT_DB = ".osxphotos_export.db"
|
|
|
|
# bit flags for burst images ("burstPickType")
|
|
BURST_PICK_TYPE_NONE = 0b0 # 0: sometimes used for single images with a burst UUID
|
|
BURST_NOT_SELECTED = 0b10 # 2: burst image is not selected
|
|
BURST_DEFAULT_PICK = 0b100 # 4: burst image is the one Photos picked to be key image before any selections made
|
|
BURST_SELECTED = 0b1000 # 8: burst image is selected
|
|
BURST_KEY = 0b10000 # 16: burst image is the key photo (top of burst stack)
|
|
BURST_UNKNOWN = 0b100000 # 32: this is almost always set with BURST_DEFAULT_PICK and never if BURST_DEFAULT_PICK is not set. I think this has something to do with what algorithm Photos used to pick the default image
|
|
|
|
LIVE_VIDEO_EXTENSIONS = [".mov"]
|
|
|
|
# categories that --post-command can be used with; these map to ExportResults fields
|
|
POST_COMMAND_CATEGORIES = {
|
|
"exported": "All exported files",
|
|
"new": "When used with '--update', all newly exported files",
|
|
"updated": "When used with '--update', all files which were previously exported but updated this time",
|
|
"skipped": "When used with '--update', all files which were skipped (because they were previously exported and didn't change)",
|
|
"missing": "All files which were not exported because they were missing from the Photos library",
|
|
"exif_updated": "When used with '--exiftool', all files on which exiftool updated the metadata",
|
|
"touched": "When used with '--touch-file', all files where the date was touched",
|
|
"converted_to_jpeg": "When used with '--convert-to-jpeg', all files which were converted to jpeg",
|
|
"sidecar_json_written": "When used with '--sidecar json', all JSON sidecar files which were written",
|
|
"sidecar_json_skipped": "When used with '--sidecar json' and '--update', all JSON sidecar files which were skipped",
|
|
"sidecar_exiftool_written": "When used with '--sidecar exiftool', all exiftool sidecar files which were written",
|
|
"sidecar_exiftool_skipped": "When used with '--sidecar exiftool' and '--update, all exiftool sidecar files which were skipped",
|
|
"sidecar_xmp_written": "When used with '--sidecar xmp', all XMP sidecar files which were written",
|
|
"sidecar_xmp_skipped": "When used with '--sidecar xmp' and '--update', all XMP sidecar files which were skipped",
|
|
"error": "All files which produced an error during export",
|
|
# "deleted_files": "When used with '--cleanup', all files deleted during the export",
|
|
# "deleted_directories": "When used with '--cleanup', all directories deleted during the export",
|
|
}
|
|
|
|
|
|
class AlbumSortOrder(Enum):
|
|
"""Album Sort Order"""
|
|
|
|
UNKNOWN = 0
|
|
MANUAL = 1
|
|
NEWEST_FIRST = 2
|
|
OLDEST_FIRST = 3
|
|
TITLE = 5
|
|
|
|
|
|
TEXT_DETECTION_CONFIDENCE_THRESHOLD = 0.75
|
|
|
|
# stat sort order for cProfile: https://docs.python.org/3/library/profile.html#pstats.Stats.sort_stats
|
|
PROFILE_SORT_KEYS = [
|
|
"calls",
|
|
"cumulative",
|
|
"cumtime",
|
|
"file",
|
|
"filename",
|
|
"module",
|
|
"ncalls",
|
|
"pcalls",
|
|
"line",
|
|
"name",
|
|
"nfl",
|
|
"stdname",
|
|
"time",
|
|
"tottime",
|
|
]
|
|
|
|
UUID_PATTERN = (
|
|
r"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}"
|
|
)
|
|
# Reference: https://docs.python.org/3/library/sqlite3.html?highlight=sqlite3%20threadsafety#sqlite3.threadsafety
|
|
# and https://docs.python.org/3/library/sqlite3.html?highlight=sqlite3%20threadsafety#sqlite3.connect
|
|
# 3: serialized mode; Threads may share the module, connections and cursors
|
|
# 3 is the default in the python.org python 3.11 distribution
|
|
# earlier versions of python.org python 3.x default to 1 which means threads may not share
|
|
# sqlite3 connections and thus PhotoInfo.export() cannot be used in a multithreaded environment
|
|
# pass SQLITE_CHECK_SAME_THREAD to sqlite3.connect() to enable multithreaded access on systems that support it
|
|
SQLITE_CHECK_SAME_THREAD = not sqlite3.threadsafety == 3
|
|
logger.debug(f"{SQLITE_CHECK_SAME_THREAD=}, {sqlite3.threadsafety=}")
|