Bug labels ventura 816 (#823)

* Partial fix for #816, labels on Ventura / macOS 13.0

* Version bump
This commit is contained in:
Rhet Turnbull
2022-11-12 11:22:44 -08:00
committed by GitHub
parent 6dbeaae541
commit ae560d24cb
9 changed files with 125 additions and 113 deletions

View File

@@ -1,5 +1,5 @@
[bumpversion] [bumpversion]
current_version = 0.52.0 current_version = 0.53.0
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+) parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)
serialize = {major}.{minor}.{patch} serialize = {major}.{minor}.{patch}

View File

@@ -2012,7 +2012,7 @@ cog.out(get_template_field_table())
|{cr}|A carriage return: '\r'| |{cr}|A carriage return: '\r'|
|{crlf}|A carriage return + line feed: '\r\n'| |{crlf}|A carriage return + line feed: '\r\n'|
|{tab}|:A tab: '\t'| |{tab}|:A tab: '\t'|
|{osxphotos_version}|The osxphotos version, e.g. '0.52.0'| |{osxphotos_version}|The osxphotos version, e.g. '0.53.0'|
|{osxphotos_cmd_line}|The full command line used to run osxphotos| |{osxphotos_cmd_line}|The full command line used to run osxphotos|
|{album}|Album(s) photo is contained in| |{album}|Album(s) photo is contained in|
|{folder_album}|Folder path + album photo is contained in. e.g. 'Folder/Subfolder/Album' or just 'Album' if no enclosing folder| |{folder_album}|Folder path + album photo is contained in. e.g. 'Folder/Subfolder/Album' or just 'Album' if no enclosing folder|

View File

@@ -2009,7 +2009,7 @@ Substitution Description
{cr} A carriage return: '\r' {cr} A carriage return: '\r'
{crlf} A carriage return + line feed: '\r\n' {crlf} A carriage return + line feed: '\r\n'
{tab} :A tab: '\t' {tab} :A tab: '\t'
{osxphotos_version} The osxphotos version, e.g. '0.52.0' {osxphotos_version} The osxphotos version, e.g. '0.53.0'
{osxphotos_cmd_line} The full command line used to run osxphotos {osxphotos_cmd_line} The full command line used to run osxphotos
The following substitutions may result in multiple values. Thus if specified The following substitutions may result in multiple values. Thus if specified
@@ -2493,7 +2493,7 @@ The following template field substitutions are availabe for use the templating s
|{cr}|A carriage return: '\r'| |{cr}|A carriage return: '\r'|
|{crlf}|A carriage return + line feed: '\r\n'| |{crlf}|A carriage return + line feed: '\r\n'|
|{tab}|:A tab: '\t'| |{tab}|:A tab: '\t'|
|{osxphotos_version}|The osxphotos version, e.g. '0.52.0'| |{osxphotos_version}|The osxphotos version, e.g. '0.53.0'|
|{osxphotos_cmd_line}|The full command line used to run osxphotos| |{osxphotos_cmd_line}|The full command line used to run osxphotos|
|{album}|Album(s) photo is contained in| |{album}|Album(s) photo is contained in|
|{folder_album}|Folder path + album photo is contained in. e.g. 'Folder/Subfolder/Album' or just 'Album' if no enclosing folder| |{folder_album}|Folder path + album photo is contained in. e.g. 'Folder/Subfolder/Album' or just 'Album' if no enclosing folder|

View File

@@ -171,64 +171,91 @@ _MAX_IPTC_KEYWORD_LEN = 64
# If anyone has a keyword matching this, then too bad... # If anyone has a keyword matching this, then too bad...
_OSXPHOTOS_NONE_SENTINEL = "OSXPhotosXYZZY42_Sentinel$" _OSXPHOTOS_NONE_SENTINEL = "OSXPhotosXYZZY42_Sentinel$"
# SearchInfo categories for Photos 5, corresponds to categories in database/search/psi.sqlite
SEARCH_CATEGORY_LABEL = 2024 class SearchCategory:
SEARCH_CATEGORY_PLACE_NAME = 1 """SearchInfo categories for Photos 5+; corresponds to categories in database/search/psi.sqlite:groups.category
SEARCH_CATEGORY_STREET = 2
SEARCH_CATEGORY_NEIGHBORHOOD = 3 Note: This is a simple enum class; the values are not meant to be changed.
SEARCH_CATEGORY_LOCALITY_4 = 4 Would be great if Python enums actually let you access the value directly.
SEARCH_CATEGORY_SUB_LOCALITY_5 = 5 """
SEARCH_CATEGORY_SUB_LOCALITY_6 = 6
SEARCH_CATEGORY_CITY = 7 LABEL = 2024
SEARCH_CATEGORY_LOCALITY_8 = 8 PLACE_NAME = 1
SEARCH_CATEGORY_NAMED_AREA = 9 STREET = 2
SEARCH_CATEGORY_ALL_LOCALITY = [ NEIGHBORHOOD = 3
SEARCH_CATEGORY_LOCALITY_4, LOCALITY_4 = 4
SEARCH_CATEGORY_SUB_LOCALITY_5, SUB_LOCALITY_5 = 5
SEARCH_CATEGORY_SUB_LOCALITY_6, SUB_LOCALITY_6 = 6
SEARCH_CATEGORY_LOCALITY_8, CITY = 7
SEARCH_CATEGORY_NAMED_AREA, LOCALITY_8 = 8
] NAMED_AREA = 9
SEARCH_CATEGORY_STATE = 10 ALL_LOCALITY = [
SEARCH_CATEGORY_STATE_ABBREVIATION = 11 LOCALITY_4,
SEARCH_CATEGORY_COUNTRY = 12 SUB_LOCALITY_5,
SEARCH_CATEGORY_BODY_OF_WATER = 14 SUB_LOCALITY_6,
SEARCH_CATEGORY_MONTH = 1014 LOCALITY_8,
SEARCH_CATEGORY_YEAR = 1015 NAMED_AREA,
SEARCH_CATEGORY_KEYWORDS = 2016 ]
SEARCH_CATEGORY_TITLE = 2017 STATE = 10
SEARCH_CATEGORY_DESCRIPTION = 2018 STATE_ABBREVIATION = 11
SEARCH_CATEGORY_HOME = 2020 COUNTRY = 12
SEARCH_CATEGORY_PERSON = 2021 BODY_OF_WATER = 14
SEARCH_CATEGORY_ACTIVITY = 2027 MONTH = 1014
SEARCH_CATEGORY_HOLIDAY = 2029 YEAR = 1015
SEARCH_CATEGORY_SEASON = 2030 KEYWORDS = 2016
SEARCH_CATEGORY_WORK = 2036 TITLE = 2017
SEARCH_CATEGORY_VENUE = 2038 DESCRIPTION = 2018
SEARCH_CATEGORY_VENUE_TYPE = 2039 HOME = 2020
SEARCH_CATEGORY_PHOTO_TYPE_VIDEO = 2044 PERSON = 2021
SEARCH_CATEGORY_PHOTO_TYPE_SLOMO = 2045 ACTIVITY = 2027
SEARCH_CATEGORY_PHOTO_TYPE_LIVE = 2046 HOLIDAY = 2029
SEARCH_CATEGORY_PHOTO_TYPE_SCREENSHOT = 2047 SEASON = 2030
SEARCH_CATEGORY_PHOTO_TYPE_PANORAMA = 2048 WORK = 2036
SEARCH_CATEGORY_PHOTO_TYPE_TIMELAPSE = 2049 VENUE = 2038
SEARCH_CATEGORY_PHOTO_TYPE_BURSTS = 2052 VENUE_TYPE = 2039
SEARCH_CATEGORY_PHOTO_TYPE_PORTRAIT = 2053 PHOTO_TYPE_VIDEO = 2044
SEARCH_CATEGORY_PHOTO_TYPE_SELFIES = 2054 PHOTO_TYPE_SLOMO = 2045
SEARCH_CATEGORY_PHOTO_TYPE_FAVORITES = 2055 PHOTO_TYPE_LIVE = 2046
SEARCH_CATEGORY_MEDIA_TYPES = [ PHOTO_TYPE_SCREENSHOT = 2047
SEARCH_CATEGORY_PHOTO_TYPE_VIDEO, PHOTO_TYPE_PANORAMA = 2048
SEARCH_CATEGORY_PHOTO_TYPE_SLOMO, PHOTO_TYPE_TIMELAPSE = 2049
SEARCH_CATEGORY_PHOTO_TYPE_LIVE, PHOTO_TYPE_BURSTS = 2052
SEARCH_CATEGORY_PHOTO_TYPE_SCREENSHOT, PHOTO_TYPE_PORTRAIT = 2053
SEARCH_CATEGORY_PHOTO_TYPE_PANORAMA, PHOTO_TYPE_SELFIES = 2054
SEARCH_CATEGORY_PHOTO_TYPE_TIMELAPSE, PHOTO_TYPE_FAVORITES = 2055
SEARCH_CATEGORY_PHOTO_TYPE_BURSTS, MEDIA_TYPES = [
SEARCH_CATEGORY_PHOTO_TYPE_PORTRAIT, PHOTO_TYPE_VIDEO,
SEARCH_CATEGORY_PHOTO_TYPE_SELFIES, PHOTO_TYPE_SLOMO,
SEARCH_CATEGORY_PHOTO_TYPE_FAVORITES, PHOTO_TYPE_LIVE,
] PHOTO_TYPE_SCREENSHOT,
SEARCH_CATEGORY_PHOTO_NAME = 2056 PHOTO_TYPE_PANORAMA,
PHOTO_TYPE_TIMELAPSE,
PHOTO_TYPE_BURSTS,
PHOTO_TYPE_PORTRAIT,
PHOTO_TYPE_SELFIES,
PHOTO_TYPE_FAVORITES,
]
PHOTO_NAME = 2056
class SearchCategory_Photos8(SearchCategory):
"""Search categories for Photos 8"""
# NOTE: This list is incomplete;
# until I get a test library that's been processed by photoanalysisd on Ventura,
# I can't verify all these are correct
LABEL = 1500
MONTH = 1100
YEAR = 1101
SEASON = 1104
ACTIVITY = 1600
KEYWORDS = 1200
PHOTO_NAME = 2100
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 length on MacOS

View File

@@ -1,3 +1,3 @@
""" version info """ """ version info """
__version__ = "0.52.0" __version__ = "0.53.0"

View File

@@ -9,8 +9,8 @@ import uuid as uuidlib
from functools import lru_cache from functools import lru_cache
from pprint import pformat from pprint import pformat
from .._constants import _PHOTOS_4_VERSION, SEARCH_CATEGORY_LABEL from .._constants import _PHOTOS_4_VERSION, search_category_factory
from ..sqlite_utils import sqlite_open_ro, sqlite_db_is_locked from ..sqlite_utils import sqlite_db_is_locked, sqlite_open_ro
from ..utils import normalize_unicode from ..utils import normalize_unicode
""" """
@@ -134,7 +134,8 @@ def _process_searchinfo(self):
except KeyError: except KeyError:
_db_searchinfo_categories[category] = [record["normalized_string"]] _db_searchinfo_categories[category] = [record["normalized_string"]]
if category == SEARCH_CATEGORY_LABEL: categories = search_category_factory(self._photos_ver)
if category == categories.LABEL:
label = record["content_string"] label = record["content_string"]
label_norm = record["normalized_string"] label_norm = record["normalized_string"]
try: try:

View File

@@ -57,7 +57,7 @@ from ..photoinfo import PhotoInfo
from ..phototemplate import RenderOptions from ..phototemplate import RenderOptions
from ..queryoptions import QueryOptions from ..queryoptions import QueryOptions
from ..rich_utils import add_rich_markup_tag from ..rich_utils import add_rich_markup_tag
from ..sqlite_utils import sqlite_open_ro, sqlite_db_is_locked from ..sqlite_utils import sqlite_db_is_locked, sqlite_open_ro
from ..utils import ( from ..utils import (
_check_file_exists, _check_file_exists,
_get_os_version, _get_os_version,
@@ -321,7 +321,10 @@ class PhotosDB:
verbose(f"Database locked, creating temporary copy.") verbose(f"Database locked, creating temporary copy.")
self._tmp_db = self._copy_db_file(self._dbfile) self._tmp_db = self._copy_db_file(self._dbfile)
# _db_version is set from photos.db
self._db_version = get_db_version(self._tmp_db) self._db_version = get_db_version(self._tmp_db)
# _photos_version is set from Photos.sqlite which only exists for Photos 5+
self._photos_ver = 4 if self._db_version == 4 else 5
# If Photos >= 5, actual data isn't in photos.db but in Photos.sqlite # If Photos >= 5, actual data isn't in photos.db but in Photos.sqlite
if int(self._db_version) > int(_PHOTOS_4_VERSION): if int(self._db_version) > int(_PHOTOS_4_VERSION):
@@ -336,6 +339,8 @@ class PhotosDB:
if sqlite_db_is_locked(self._dbfile_actual): if sqlite_db_is_locked(self._dbfile_actual):
verbose(f"Database locked, creating temporary copy.") verbose(f"Database locked, creating temporary copy.")
self._tmp_db = self._copy_db_file(self._dbfile_actual) self._tmp_db = self._copy_db_file(self._dbfile_actual)
# set the photos version to actual value based on Photos.sqlite
self._photos_ver = get_db_model_version(self._tmp_db)
if is_debug(): if is_debug():
logging.debug( logging.debug(
@@ -588,7 +593,7 @@ class PhotosDB:
""" """
return sqlite_open_ro(self._tmp_db) return sqlite_open_ro(self._tmp_db)
def _copy_db_file(self, fname): def _copy_db_file(self, fname: str) -> str:
"""copies the sqlite database file to a temp file""" """copies the sqlite database file to a temp file"""
""" returns the name of the temp file """ """ returns the name of the temp file """
""" If sqlite shared memory and write-ahead log files exist, those are copied too """ """ If sqlite shared memory and write-ahead log files exist, those are copied too """
@@ -648,8 +653,6 @@ class PhotosDB:
verbose("Processing database.") verbose("Processing database.")
verbose(f"Database version: {self._num(self._db_version)}.") verbose(f"Database version: {self._num(self._db_version)}.")
self._photos_ver = 4 # only used in Photos 5+
(conn, c) = sqlite_open_ro(self._tmp_db) (conn, c) = sqlite_open_ro(self._tmp_db)
# get info to associate persons with photos # get info to associate persons with photos
@@ -1604,8 +1607,8 @@ class PhotosDB:
(conn, c) = sqlite_open_ro(self._tmp_db) (conn, c) = sqlite_open_ro(self._tmp_db)
# some of the tables/columns have different names in different versions of Photos # some of the tables/columns have different names in different versions of Photos
photos_ver = get_db_model_version(self._tmp_db) # set local var for readability
self._photos_ver = photos_ver photos_ver = self._photos_ver
verbose( verbose(
f"Database version: {self._num(self._db_version)}, {self._num(photos_ver)}." f"Database version: {self._num(self._db_version)}, {self._num(photos_ver)}."
) )

View File

@@ -69,7 +69,7 @@ def get_db_version(db_file):
return version return version
def get_model_version(db_file): def get_model_version(db_file: str) -> str:
"""Returns the database model version from Z_METADATA """Returns the database model version from Z_METADATA
Args: Args:
@@ -90,7 +90,7 @@ def get_model_version(db_file):
return plist["PLModelVersion"] return plist["PLModelVersion"]
def get_db_model_version(db_file): def get_db_model_version(db_file: str) -> int:
"""Returns Photos version based on model version found in db_file """Returns Photos version based on model version found in db_file
Args: Args:

View File

@@ -1,27 +1,7 @@
""" class for PhotoInfo exposing SearchInfo data such as labels """ class for PhotoInfo exposing SearchInfo data such as labels
""" """
from ._constants import ( from ._constants import _PHOTOS_4_VERSION, search_category_factory
_PHOTOS_4_VERSION,
SEARCH_CATEGORY_ACTIVITY,
SEARCH_CATEGORY_ALL_LOCALITY,
SEARCH_CATEGORY_BODY_OF_WATER,
SEARCH_CATEGORY_CITY,
SEARCH_CATEGORY_COUNTRY,
SEARCH_CATEGORY_HOLIDAY,
SEARCH_CATEGORY_LABEL,
SEARCH_CATEGORY_MEDIA_TYPES,
SEARCH_CATEGORY_MONTH,
SEARCH_CATEGORY_NEIGHBORHOOD,
SEARCH_CATEGORY_PLACE_NAME,
SEARCH_CATEGORY_SEASON,
SEARCH_CATEGORY_STATE,
SEARCH_CATEGORY_STATE_ABBREVIATION,
SEARCH_CATEGORY_STREET,
SEARCH_CATEGORY_VENUE,
SEARCH_CATEGORY_VENUE_TYPE,
SEARCH_CATEGORY_YEAR,
)
__all__ = ["SearchInfo"] __all__ = ["SearchInfo"]
@@ -38,6 +18,7 @@ class SearchInfo:
"search info not implemented for this database version" "search info not implemented for this database version"
) )
self._categories = search_category_factory(photo._db._photos_ver)
self._photo = photo self._photo = photo
self._normalized = normalized self._normalized = normalized
self.uuid = photo.uuid self.uuid = photo.uuid
@@ -51,103 +32,103 @@ class SearchInfo:
@property @property
def labels(self): def labels(self):
"""return list of labels associated with Photo""" """return list of labels associated with Photo"""
return self._get_text_for_category(SEARCH_CATEGORY_LABEL) return self._get_text_for_category(self._categories.LABEL)
@property @property
def place_names(self): def place_names(self):
"""returns list of place names""" """returns list of place names"""
return self._get_text_for_category(SEARCH_CATEGORY_PLACE_NAME) return self._get_text_for_category(self._categories.PLACE_NAME)
@property @property
def streets(self): def streets(self):
"""returns list of street names""" """returns list of street names"""
return self._get_text_for_category(SEARCH_CATEGORY_STREET) return self._get_text_for_category(self._categories.STREET)
@property @property
def neighborhoods(self): def neighborhoods(self):
"""returns list of neighborhoods""" """returns list of neighborhoods"""
return self._get_text_for_category(SEARCH_CATEGORY_NEIGHBORHOOD) return self._get_text_for_category(self._categories.NEIGHBORHOOD)
@property @property
def locality_names(self): def locality_names(self):
"""returns list of other locality names""" """returns list of other locality names"""
locality = [] locality = []
for category in SEARCH_CATEGORY_ALL_LOCALITY: for category in self._categories.ALL_LOCALITY:
locality += self._get_text_for_category(category) locality += self._get_text_for_category(category)
return locality return locality
@property @property
def city(self): def city(self):
"""returns city/town""" """returns city/town"""
city = self._get_text_for_category(SEARCH_CATEGORY_CITY) city = self._get_text_for_category(self._categories.CITY)
return city[0] if city else "" return city[0] if city else ""
@property @property
def state(self): def state(self):
"""returns state name""" """returns state name"""
state = self._get_text_for_category(SEARCH_CATEGORY_STATE) state = self._get_text_for_category(self._categories.STATE)
return state[0] if state else "" return state[0] if state else ""
@property @property
def state_abbreviation(self): def state_abbreviation(self):
"""returns state abbreviation""" """returns state abbreviation"""
abbrev = self._get_text_for_category(SEARCH_CATEGORY_STATE_ABBREVIATION) abbrev = self._get_text_for_category(self._categories.STATE_ABBREVIATION)
return abbrev[0] if abbrev else "" return abbrev[0] if abbrev else ""
@property @property
def country(self): def country(self):
"""returns country name""" """returns country name"""
country = self._get_text_for_category(SEARCH_CATEGORY_COUNTRY) country = self._get_text_for_category(self._categories.COUNTRY)
return country[0] if country else "" return country[0] if country else ""
@property @property
def month(self): def month(self):
"""returns month name""" """returns month name"""
month = self._get_text_for_category(SEARCH_CATEGORY_MONTH) month = self._get_text_for_category(self._categories.MONTH)
return month[0] if month else "" return month[0] if month else ""
@property @property
def year(self): def year(self):
"""returns year""" """returns year"""
year = self._get_text_for_category(SEARCH_CATEGORY_YEAR) year = self._get_text_for_category(self._categories.YEAR)
return year[0] if year else "" return year[0] if year else ""
@property @property
def bodies_of_water(self): def bodies_of_water(self):
"""returns list of body of water names""" """returns list of body of water names"""
return self._get_text_for_category(SEARCH_CATEGORY_BODY_OF_WATER) return self._get_text_for_category(self._categories.BODY_OF_WATER)
@property @property
def holidays(self): def holidays(self):
"""returns list of holiday names""" """returns list of holiday names"""
return self._get_text_for_category(SEARCH_CATEGORY_HOLIDAY) return self._get_text_for_category(self._categories.HOLIDAY)
@property @property
def activities(self): def activities(self):
"""returns list of activity names""" """returns list of activity names"""
return self._get_text_for_category(SEARCH_CATEGORY_ACTIVITY) return self._get_text_for_category(self._categories.ACTIVITY)
@property @property
def season(self): def season(self):
"""returns season name""" """returns season name"""
season = self._get_text_for_category(SEARCH_CATEGORY_SEASON) season = self._get_text_for_category(self._categories.SEASON)
return season[0] if season else "" return season[0] if season else ""
@property @property
def venues(self): def venues(self):
"""returns list of venue names""" """returns list of venue names"""
return self._get_text_for_category(SEARCH_CATEGORY_VENUE) return self._get_text_for_category(self._categories.VENUE)
@property @property
def venue_types(self): def venue_types(self):
"""returns list of venue types""" """returns list of venue types"""
return self._get_text_for_category(SEARCH_CATEGORY_VENUE_TYPE) return self._get_text_for_category(self._categories.VENUE_TYPE)
@property @property
def media_types(self): def media_types(self):
"""returns list of media types (photo, video, panorama, etc)""" """returns list of media types (photo, video, panorama, etc)"""
types = [] types = []
for category in SEARCH_CATEGORY_MEDIA_TYPES: for category in self._categories.MEDIA_TYPES:
types += self._get_text_for_category(category) types += self._get_text_for_category(category)
return types return types