Fixed syndicated photos to work on Photos 7, #1116 (#1127)

This commit is contained in:
Rhet Turnbull 2023-07-20 06:20:07 -07:00 committed by GitHub
parent 25d4015b65
commit 4efc0c9f56
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 54 additions and 42 deletions

View File

@ -1181,12 +1181,12 @@ Returns True if photo is a [cloud asset](#iscloudasset) and is synched to iCloud
#### `syndicated` #### `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` #### `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. 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` ### `shared_moment`

View File

@ -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_8_MODEL_VERSION = [16000, 16999] # Ventura dev preview: 16119
_PHOTOS_9_MODEL_VERSION = [17000, 17999] # Sonoma dev preview: 17120 _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 # some table names differ between Photos 5 and later versions
_DB_TABLE_NAMES = { _DB_TABLE_NAMES = {
5: { 5: {

View File

@ -173,7 +173,11 @@ class PhotoInfo:
"""Returns candidate path for original photo on Photos >= version 5""" """Returns candidate path for original photo on Photos >= version 5"""
if self._info["shared"]: if self._info["shared"]:
return self._path_5_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 # 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 # 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 # 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): def _path_syndication(self):
"""Return path for syndicated photo on Photos >= version 8""" """Return path for syndicated photo on Photos >= version 7"""
# Photos 8+ stores syndicated photos in a separate directory # Photos 7+ stores syndicated photos in a separate directory
# in ~/Photos Library.photoslibrary/scopes/syndication/originals/X/UUID.ext # in ~/Photos Library.photoslibrary/scopes/syndication/originals/X/UUID.ext
# where X is first digit of UUID # where X is first digit of UUID
syndication_path = "scopes/syndication/originals" syndication_path = "scopes/syndication/originals"
@ -237,8 +241,8 @@ class PhotoInfo:
return path if os.path.isfile(path) else None return path if os.path.isfile(path) else None
def _path_shared_moment(self): def _path_shared_moment(self):
"""Return path for shared moment photo on Photos >= version 8""" """Return path for shared moment photo on Photos >= version 7"""
# Photos 8+ stores shared moment photos in a separate directory # Photos 7+ stores shared moment photos in a separate directory
# in ~/Photos Library.photoslibrary/scopes/momentshared/originals/X/UUID.ext # in ~/Photos Library.photoslibrary/scopes/momentshared/originals/X/UUID.ext
# where X is first digit of UUID # where X is first digit of UUID
momentshared_path = "scopes/momentshared/originals" momentshared_path = "scopes/momentshared/originals"
@ -371,7 +375,7 @@ class PhotoInfo:
) )
def _path_edited_4(self) -> str | None: 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"]: if not self._info["hasAdjustments"]:
return None return None
@ -476,7 +480,7 @@ class PhotoInfo:
# In Photos 5, raw is in same folder as original but with _4.ext # In Photos 5, raw is in same folder as original but with _4.ext
# Unless "Copy Items to the Photos Library" is not checked # 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 # 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 # 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 # 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: elif self.live_photo and self.path and not self.ismissing:
if self.shared: if self.shared:
return self._path_live_photo_shared_5() 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() return self._path_live_shared_moment()
if self.syndicated and not self.saved_to_library: if self.syndicated and not self.saved_to_library:
# syndicated ("Shared with you") photos not yet saved to library # syndicated ("Shared with you") photos not yet saved to library
@ -995,8 +999,8 @@ class PhotoInfo:
return photopath return photopath
def _path_live_syndicated(self): def _path_live_syndicated(self):
"""Return path for live syndicated photo on Photos >= version 8""" """Return path for live syndicated photo on Photos >= version 7"""
# Photos 8+ stores live syndicated photos in a separate directory # Photos 7+ stores live syndicated photos in a separate directory
# in ~/Photos Library.photoslibrary/scopes/syndication/originals/X/UUID_3.mov # in ~/Photos Library.photoslibrary/scopes/syndication/originals/X/UUID_3.mov
# where X is first digit of UUID # where X is first digit of UUID
syndication_path = "scopes/syndication/originals" syndication_path = "scopes/syndication/originals"
@ -1011,8 +1015,8 @@ class PhotoInfo:
return live_photo if os.path.isfile(live_photo) else None return live_photo if os.path.isfile(live_photo) else None
def _path_live_shared_moment(self): def _path_live_shared_moment(self):
"""Return path for live shared moment photo on Photos >= version 8""" """Return path for live shared moment photo on Photos >= version 7"""
# Photos 8+ stores live shared moment photos in a separate directory # Photos 7+ stores live shared moment photos in a separate directory
# in ~/Photos Library.photoslibrary/scopes/momentshared/originals/X/UUID_3.mov # in ~/Photos Library.photoslibrary/scopes/momentshared/originals/X/UUID_3.mov
# where X is first digit of UUID # where X is first digit of UUID
shared_moment_path = "scopes/momentshared/originals" shared_moment_path = "scopes/momentshared/originals"
@ -1036,7 +1040,7 @@ class PhotoInfo:
return self._path_derivatives_5_shared() return self._path_derivatives_5_shared()
directory = self._uuid[0] # first char of uuid directory = self._uuid[0] # first char of uuid
if self.shared_moment: if self.shared_moment and self._db.photos_version >= 7:
# shared moments # shared moments
derivative_path = "scopes/momentshared/resources/derivatives" derivative_path = "scopes/momentshared/resources/derivatives"
thumb_path = ( thumb_path = (
@ -1398,9 +1402,9 @@ class PhotoInfo:
def syndicated(self) -> bool | None: def syndicated(self) -> bool | None:
"""Return true if photo was shared via syndication (e.g. via Messages, etc.); """Return true if photo was shared via syndication (e.g. via Messages, etc.);
these are photos that appear in "Shared with you" album. 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 return None
try: try:
@ -1415,10 +1419,10 @@ class PhotoInfo:
def saved_to_library(self) -> bool | None: def saved_to_library(self) -> bool | None:
"""Return True if syndicated photo has been 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. returns False if photo is not syndicated or has not been saved to the library.
Returns None if not Photos 8+. Returns None if not Photos 7+.
Syndicated photos are photos that appear in "Shared with you" album; Photos 8+ only. 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 return None
try: try:
@ -1428,7 +1432,7 @@ class PhotoInfo:
@cached_property @cached_property
def shared_moment(self) -> bool: 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"]) return bool(self._info["moment_share"])
@property @property

View File

@ -4,4 +4,4 @@ Processes a Photos.app library database to extract information about photos
""" """
from .photosdb import PhotosDB 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

View File

@ -4,7 +4,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING 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 from ..sqlite_utils import sqlite_open_ro
if TYPE_CHECKING: if TYPE_CHECKING:
@ -16,15 +16,18 @@ def _process_syndicationinfo(self: PhotosDB):
self._db_syndication_uuid = {} self._db_syndication_uuid = {}
if self.photos_version < 8: if self.photos_version < 7:
raise NotImplementedError( raise NotImplementedError(
f"syndication info not implemented for this database version: {self.photos_version}" 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 """Process Syndication info for Photos 8.0 and later
Args: Args:

View File

@ -62,7 +62,7 @@ from ..rich_utils import add_rich_markup_tag
from ..sqlite_utils import sqlite_db_is_locked, sqlite_open_ro from ..sqlite_utils import sqlite_db_is_locked, sqlite_open_ro
from ..unicode import normalize_unicode from ..unicode import normalize_unicode
from ..utils import _check_file_exists, get_last_library_path, noop 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: if is_macos:
import photoscript import photoscript
@ -326,7 +326,7 @@ class PhotosDB:
# photoanalysisd sometimes maintains this lock even after Photos is closed # photoanalysisd sometimes maintains this lock even after Photos is closed
# In those cases, make a temp copy of the file for sqlite3 to read # In those cases, make a temp copy of the file for sqlite3 to read
if sqlite_db_is_locked(self._dbfile): 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) self._tmp_db = self._copy_db_file(self._dbfile)
# _db_version is set from photos.db # _db_version is set from photos.db
@ -341,21 +341,23 @@ class PhotosDB:
self._photos_ver = 4 self._photos_ver = 4
else: else:
self._photos_ver = 5 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 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):
dbpath = pathlib.Path(self._dbfile).parent dbpath = pathlib.Path(self._dbfile).parent
dbfile = dbpath / "Photos.sqlite" dbfile = dbpath / "Photos.sqlite"
if not _check_file_exists(dbfile): if not _check_file_exists(dbfile):
raise FileNotFoundError(f"dbfile {dbfile} does not exist", dbfile) raise FileNotFoundError(f"dbfile {dbfile} does not exist", dbfile)
else: self._dbfile_actual = self._tmp_db = dbfile
self._dbfile_actual = self._tmp_db = dbfile verbose(f"Processing database {self._filepath(self._dbfile_actual)}")
verbose(f"Processing database {self._filepath(self._dbfile_actual)}") # if database is exclusively locked, make a copy of it and use the copy
# if database is exclusively locked, make a copy of it and use the copy if sqlite_db_is_locked(self._dbfile_actual):
if sqlite_db_is_locked(self._dbfile_actual): verbose("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 # 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( logger.debug(
f"_dbfile = {self._dbfile}, _dbfile_actual = {self._dbfile_actual}" f"_dbfile = {self._dbfile}, _dbfile_actual = {self._dbfile_actual}"
@ -1235,7 +1237,7 @@ class PhotosDB:
# photos 5+ only, for shared photos # photos 5+ only, for shared photos
self._dbphotos[uuid]["cloudownerhashedpersonid"] = None self._dbphotos[uuid]["cloudownerhashedpersonid"] = None
# photos 8+ only, shared moments # photos 7+ only, shared moments
self._dbphotos[uuid]["moment_share"] = None self._dbphotos[uuid]["moment_share"] = None
# compute signatures for finding possible duplicates # compute signatures for finding possible duplicates
@ -2004,7 +2006,7 @@ class PhotosDB:
# 41 ZGENERICASSET.ZADDEDDATE -- date item added to the library # 41 ZGENERICASSET.ZADDEDDATE -- date item added to the library
# 42 ZGENERICASSET.Z_PK -- primary key # 42 ZGENERICASSET.Z_PK -- primary key
# 43 ZGENERICASSET.ZCLOUDOWNERHASHEDPERSONID -- used to look up owner name (for shared photos) # 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: for row in c:
uuid = row[0] uuid = row[0]
@ -2526,7 +2528,7 @@ class PhotosDB:
verbose("Processing moments.") verbose("Processing moments.")
self._process_moments() self._process_moments()
if self.photos_version >= 8: if self.photos_version >= 7:
verbose("Processing syndication info.") verbose("Processing syndication info.")
self._process_syndicationinfo() self._process_syndicationinfo()

View File

@ -24,7 +24,7 @@ from ..sqlite_utils import sqlite_open_ro
__all__ = [ __all__ = [
"get_db_version", "get_db_version",
"get_model_version", "get_model_version",
"get_db_model_version", "get_photos_version_from_model",
"get_photos_library_version", "get_photos_library_version",
] ]
@ -94,7 +94,7 @@ def get_model_version(db_file: str) -> str:
return plist["PLModelVersion"] 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 """Returns Photos version based on model version found in db_file
Args: Args: