diff --git a/API_README.md b/API_README.md index 44da92e9..7b9d3d1f 100644 --- a/API_README.md +++ b/API_README.md @@ -1181,12 +1181,12 @@ Returns True if photo is a [cloud asset](#iscloudasset) and is synched to iCloud #### `syndicated` -Return true if photo was shared via syndication (e.g. via Messages, etc.); these are photos that appear in "Shared with you" album. Photos 8+ only; returns None if not Photos 8+. +Return true if photo was shared via syndication (e.g. via Messages, etc.); these are photos that appear in "Shared with you" album. Photos 7+ only; returns None if not Photos 7+. #### `saved_to_library` Return True if syndicated photo has been saved to library; returns False if photo is not syndicated or has not been saved to the library. -Syndicated photos are photos that appear in "Shared with you" album. Photos 8+ only; returns None if not Photos 8+. +Syndicated photos are photos that appear in "Shared with you" album. Photos 7+ only; returns None if not Photos 7+. ### `shared_moment` diff --git a/osxphotos/_constants.py b/osxphotos/_constants.py index 91b9e1eb..2ba85398 100644 --- a/osxphotos/_constants.py +++ b/osxphotos/_constants.py @@ -52,6 +52,9 @@ _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 + # some table names differ between Photos 5 and later versions _DB_TABLE_NAMES = { 5: { diff --git a/osxphotos/photoinfo.py b/osxphotos/photoinfo.py index 667d5379..31d741b8 100644 --- a/osxphotos/photoinfo.py +++ b/osxphotos/photoinfo.py @@ -173,7 +173,11 @@ class PhotoInfo: """Returns candidate path for original photo on Photos >= version 5""" if self._info["shared"]: return self._path_5_shared() - if self.shared_moment and self._path_shared_moment(): + if ( + self.shared_moment + and self._db.photos_version >= 7 + and self._path_shared_moment() + ): # path for photos in shared moments if it's in the shared moment folder # the file may also be in the originals folder which the next check will catch # check shared_moment first as a photo can be both a shared moment and syndicated @@ -222,8 +226,8 @@ class PhotoInfo: ) def _path_syndication(self): - """Return path for syndicated photo on Photos >= version 8""" - # Photos 8+ stores syndicated photos in a separate directory + """Return path for syndicated photo on Photos >= version 7""" + # Photos 7+ stores syndicated photos in a separate directory # in ~/Photos Library.photoslibrary/scopes/syndication/originals/X/UUID.ext # where X is first digit of UUID syndication_path = "scopes/syndication/originals" @@ -237,8 +241,8 @@ class PhotoInfo: return path if os.path.isfile(path) else None def _path_shared_moment(self): - """Return path for shared moment photo on Photos >= version 8""" - # Photos 8+ stores shared moment photos in a separate directory + """Return path for shared moment photo on Photos >= version 7""" + # Photos 7+ stores shared moment photos in a separate directory # in ~/Photos Library.photoslibrary/scopes/momentshared/originals/X/UUID.ext # where X is first digit of UUID momentshared_path = "scopes/momentshared/originals" @@ -371,7 +375,7 @@ class PhotoInfo: ) def _path_edited_4(self) -> str | None: - """return path_edited for Photos <= 4; modified version of code in PhotoInfo to debug #859""" + """return path_edited for Photos <= 4; #859""" if not self._info["hasAdjustments"]: return None @@ -476,7 +480,7 @@ class PhotoInfo: # In Photos 5, raw is in same folder as original but with _4.ext # Unless "Copy Items to the Photos Library" is not checked - # then RAW image is not renamed but has same name is jpeg buth with raw extension + # then RAW image is not renamed but has same name is jpeg but with raw extension # Current implementation finds images with the correct raw UTI extension # in same folder as the original and with same stem as original in form: original_stem*.raw_ext # TODO: I don't like this -- would prefer a more deterministic approach but until I have more @@ -931,7 +935,7 @@ class PhotoInfo: elif self.live_photo and self.path and not self.ismissing: if self.shared: return self._path_live_photo_shared_5() - if self.shared_moment: + if self.shared_moment and self._db.photos_version >= 7: return self._path_live_shared_moment() if self.syndicated and not self.saved_to_library: # syndicated ("Shared with you") photos not yet saved to library @@ -995,8 +999,8 @@ class PhotoInfo: return photopath def _path_live_syndicated(self): - """Return path for live syndicated photo on Photos >= version 8""" - # Photos 8+ stores live syndicated photos in a separate directory + """Return path for live syndicated photo on Photos >= version 7""" + # Photos 7+ stores live syndicated photos in a separate directory # in ~/Photos Library.photoslibrary/scopes/syndication/originals/X/UUID_3.mov # where X is first digit of UUID syndication_path = "scopes/syndication/originals" @@ -1011,8 +1015,8 @@ class PhotoInfo: return live_photo if os.path.isfile(live_photo) else None def _path_live_shared_moment(self): - """Return path for live shared moment photo on Photos >= version 8""" - # Photos 8+ stores live shared moment photos in a separate directory + """Return path for live shared moment photo on Photos >= version 7""" + # Photos 7+ stores live shared moment photos in a separate directory # in ~/Photos Library.photoslibrary/scopes/momentshared/originals/X/UUID_3.mov # where X is first digit of UUID shared_moment_path = "scopes/momentshared/originals" @@ -1036,7 +1040,7 @@ class PhotoInfo: return self._path_derivatives_5_shared() directory = self._uuid[0] # first char of uuid - if self.shared_moment: + if self.shared_moment and self._db.photos_version >= 7: # shared moments derivative_path = "scopes/momentshared/resources/derivatives" thumb_path = ( @@ -1398,9 +1402,9 @@ class PhotoInfo: def syndicated(self) -> bool | None: """Return true if photo was shared via syndication (e.g. via Messages, etc.); these are photos that appear in "Shared with you" album. - Photos 8+ only; returns None if not Photos 8+. + Photos 7+ only; returns None if not Photos 7+. """ - if self._db.photos_version < 8: + if self._db.photos_version < 7: return None try: @@ -1415,10 +1419,10 @@ class PhotoInfo: def saved_to_library(self) -> bool | None: """Return True if syndicated photo has been saved to library; returns False if photo is not syndicated or has not been saved to the library. - Returns None if not Photos 8+. - Syndicated photos are photos that appear in "Shared with you" album; Photos 8+ only. + Returns None if not Photos 7+. + Syndicated photos are photos that appear in "Shared with you" album; Photos 7+ only. """ - if self._db.photos_version < 8: + if self._db.photos_version < 7: return None try: @@ -1428,7 +1432,7 @@ class PhotoInfo: @cached_property def shared_moment(self) -> bool: - """Returns True if photo is part of a shared moment otherwise False""" + """Returns True if photo is part of a shared moment otherwise False (Photos 7+ only)""" return bool(self._info["moment_share"]) @property diff --git a/osxphotos/photosdb/__init__.py b/osxphotos/photosdb/__init__.py index ee6ca14c..83453e25 100644 --- a/osxphotos/photosdb/__init__.py +++ b/osxphotos/photosdb/__init__.py @@ -4,4 +4,4 @@ Processes a Photos.app library database to extract information about photos """ from .photosdb import PhotosDB -from .photosdb_utils import get_db_model_version, get_db_version, get_model_version +from .photosdb_utils import get_photos_version_from_model, get_db_version, get_model_version diff --git a/osxphotos/photosdb/_photosdb_process_syndicationinfo.py b/osxphotos/photosdb/_photosdb_process_syndicationinfo.py index 327ef0b2..b4c8a4c4 100644 --- a/osxphotos/photosdb/_photosdb_process_syndicationinfo.py +++ b/osxphotos/photosdb/_photosdb_process_syndicationinfo.py @@ -4,7 +4,7 @@ from __future__ import annotations from typing import TYPE_CHECKING -from .._constants import _DB_TABLE_NAMES +from .._constants import _DB_TABLE_NAMES, _PHOTOS_SYNDICATION_MODEL_VERSION from ..sqlite_utils import sqlite_open_ro if TYPE_CHECKING: @@ -16,15 +16,18 @@ def _process_syndicationinfo(self: PhotosDB): self._db_syndication_uuid = {} - if self.photos_version < 8: + if self.photos_version < 7: raise NotImplementedError( f"syndication info not implemented for this database version: {self.photos_version}" ) - else: - _process_syndicationinfo_8(self) + + if self._model_ver < _PHOTOS_SYNDICATION_MODEL_VERSION: + return + + _process_syndicationinfo_7(self) -def _process_syndicationinfo_8(photosdb: PhotosDB): +def _process_syndicationinfo_7(photosdb: PhotosDB): """Process Syndication info for Photos 8.0 and later Args: diff --git a/osxphotos/photosdb/photosdb.py b/osxphotos/photosdb/photosdb.py index 863d1d75..ee9e346d 100644 --- a/osxphotos/photosdb/photosdb.py +++ b/osxphotos/photosdb/photosdb.py @@ -62,7 +62,7 @@ from ..rich_utils import add_rich_markup_tag from ..sqlite_utils import sqlite_db_is_locked, sqlite_open_ro from ..unicode import normalize_unicode from ..utils import _check_file_exists, get_last_library_path, noop -from .photosdb_utils import get_db_model_version, get_db_version +from .photosdb_utils import get_photos_version_from_model, get_db_version, get_model_version if is_macos: import photoscript @@ -326,7 +326,7 @@ class PhotosDB: # photoanalysisd sometimes maintains this lock even after Photos is closed # In those cases, make a temp copy of the file for sqlite3 to read if sqlite_db_is_locked(self._dbfile): - verbose(f"Database locked, creating temporary copy.") + verbose("Database locked, creating temporary copy.") self._tmp_db = self._copy_db_file(self._dbfile) # _db_version is set from photos.db @@ -341,21 +341,23 @@ class PhotosDB: self._photos_ver = 4 else: self._photos_ver = 5 + self._model_ver = 0 # only set for Photos 5+ + # If Photos >= 5, actual data isn't in photos.db but in Photos.sqlite if int(self._db_version) > int(_PHOTOS_4_VERSION): dbpath = pathlib.Path(self._dbfile).parent dbfile = dbpath / "Photos.sqlite" if not _check_file_exists(dbfile): raise FileNotFoundError(f"dbfile {dbfile} does not exist", dbfile) - else: - self._dbfile_actual = self._tmp_db = dbfile - verbose(f"Processing database {self._filepath(self._dbfile_actual)}") - # if database is exclusively locked, make a copy of it and use the copy - if sqlite_db_is_locked(self._dbfile_actual): - verbose(f"Database locked, creating temporary copy.") - self._tmp_db = self._copy_db_file(self._dbfile_actual) + self._dbfile_actual = self._tmp_db = dbfile + verbose(f"Processing database {self._filepath(self._dbfile_actual)}") + # if database is exclusively locked, make a copy of it and use the copy + if sqlite_db_is_locked(self._dbfile_actual): + verbose("Database locked, creating temporary copy.") + 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) + self._photos_ver = get_photos_version_from_model(self._tmp_db) + self._model_ver = get_model_version(self._tmp_db) logger.debug( f"_dbfile = {self._dbfile}, _dbfile_actual = {self._dbfile_actual}" @@ -1235,7 +1237,7 @@ class PhotosDB: # photos 5+ only, for shared photos self._dbphotos[uuid]["cloudownerhashedpersonid"] = None - # photos 8+ only, shared moments + # photos 7+ only, shared moments self._dbphotos[uuid]["moment_share"] = None # compute signatures for finding possible duplicates @@ -2004,7 +2006,7 @@ class PhotosDB: # 41 ZGENERICASSET.ZADDEDDATE -- date item added to the library # 42 ZGENERICASSET.Z_PK -- primary key # 43 ZGENERICASSET.ZCLOUDOWNERHASHEDPERSONID -- used to look up owner name (for shared photos) - # 44 ZASSET.ZMOMENTSHARE -- FK for ZSHARE (shared moments, Photos 8+) + # 44 ZASSET.ZMOMENTSHARE -- FK for ZSHARE (shared moments, Photos 5+; in Photos 7+ these are in the scopes/momentshared folder) for row in c: uuid = row[0] @@ -2526,7 +2528,7 @@ class PhotosDB: verbose("Processing moments.") self._process_moments() - if self.photos_version >= 8: + if self.photos_version >= 7: verbose("Processing syndication info.") self._process_syndicationinfo() diff --git a/osxphotos/photosdb/photosdb_utils.py b/osxphotos/photosdb/photosdb_utils.py index 21bf87cc..9b731b08 100644 --- a/osxphotos/photosdb/photosdb_utils.py +++ b/osxphotos/photosdb/photosdb_utils.py @@ -24,7 +24,7 @@ from ..sqlite_utils import sqlite_open_ro __all__ = [ "get_db_version", "get_model_version", - "get_db_model_version", + "get_photos_version_from_model", "get_photos_library_version", ] @@ -94,7 +94,7 @@ def get_model_version(db_file: str) -> str: return plist["PLModelVersion"] -def get_db_model_version(db_file: str) -> int: +def get_photos_version_from_model(db_file: str) -> int: """Returns Photos version based on model version found in db_file Args: