parent
25d4015b65
commit
4efc0c9f56
@ -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`
|
||||||
|
|
||||||
|
|||||||
@ -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: {
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
@ -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()
|
||||||
|
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user