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`
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`

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_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: {

View File

@ -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

View File

@ -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

View File

@ -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:

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 ..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.")
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()

View File

@ -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: