Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature shared icloud library 860 #1149

Merged
merged 2 commits into from
Aug 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 71 additions & 67 deletions API_README.md

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions osxphotos/_constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@
# 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

# shared iCloud library versions; dev preview doesn't contain same columns as release version
_PHOTOS_SHARED_LIBRARY_VERSION = 16320 # 13.0

# some table names differ between Photos 5 and later versions
_DB_TABLE_NAMES = {
5: {
Expand Down
10 changes: 10 additions & 0 deletions osxphotos/cli/cli_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -621,6 +621,16 @@ def _add_options(wrapped):
is_flag=True,
help="Search for photos that are not part of a shared moment",
),
"--shared-library": click.Option(
["--shared-library"],
is_flag=True,
help="Search for photos that are part of a shared library",
),
"--not-shared-library": click.Option(
["--not-shared-library"],
is_flag=True,
help="Search for photos that are not part of a shared library",
),
"--regex": click.Option(
["--regex"],
metavar="REGEX TEMPLATE",
Expand Down
23 changes: 14 additions & 9 deletions osxphotos/cli/export.py
Original file line number Diff line number Diff line change
Expand Up @@ -977,6 +977,8 @@ def export(
not_saved_to_library,
shared_moment,
not_shared_moment,
shared_library,
not_shared_library,
selected=False, # Isn't provided on unsupported platforms
# debug, # debug, watch, breakpoint handled in cli/__init__.py
# watch,
Expand Down Expand Up @@ -1077,6 +1079,7 @@ def export(
exiftool_merge_persons = cfg.exiftool_merge_persons
exiftool_option = cfg.exiftool_option
exiftool_path = cfg.exiftool_path
export_aae = cfg.export_aae
export_as_hardlink = cfg.export_as_hardlink
export_by_date = cfg.export_by_date
exportdb = cfg.exportdb
Expand Down Expand Up @@ -1135,10 +1138,14 @@ def export(
not_panorama = cfg.not_panorama
not_portrait = cfg.not_portrait
not_reference = cfg.not_reference
not_saved_to_library = cfg.not_saved_to_library
not_screenshot = cfg.not_screenshot
not_selfie = cfg.not_selfie
not_shared = cfg.not_shared
not_shared_library = cfg.not_shared_library
not_shared_moment = cfg.not_shared_moment
not_slow_mo = cfg.not_slow_mo
not_syndicated = cfg.not_syndicated
not_time_lapse = cfg.not_time_lapse
only_movies = cfg.only_movies
only_new = cfg.only_new
Expand All @@ -1165,11 +1172,13 @@ def export(
replace_keywords = cfg.replace_keywords
report = cfg.report
retry = cfg.retry
saved_to_library = cfg.saved_to_library
screenshot = cfg.screenshot
selected = cfg.selected
selfie = cfg.selfie
shared = cfg.shared
export_aae = cfg.export_aae
shared_library = cfg.shared_library
shared_moment = cfg.shared_moment
sidecar = cfg.sidecar
sidecar_drop_ext = cfg.sidecar_drop_ext
sidecar_template = cfg.sidecar_template
Expand All @@ -1182,6 +1191,7 @@ def export(
skip_uuid_from_file = cfg.skip_uuid_from_file
slow_mo = cfg.slow_mo
strip = cfg.strip
syndicated = cfg.syndicated
theme = cfg.theme
time_lapse = cfg.time_lapse
timestamp = cfg.timestamp
Expand All @@ -1197,16 +1207,11 @@ def export(
uti = cfg.uti
uuid = cfg.uuid
uuid_from_file = cfg.uuid_from_file
# this is the one option that is named differently in the config file than the variable passed by --verbose (verbose_flag)
verbose_flag = cfg.verbose
verbose_flag = (
cfg.verbose
) # this is named differently in the config file than the variable passed by --verbose (verbose_flag)
xattr_template = cfg.xattr_template
year = cfg.year
syndicated = cfg.syndicated
not_syndicated = cfg.not_syndicated
saved_to_library = cfg.saved_to_library
not_saved_to_library = cfg.not_saved_to_library
shared_moment = cfg.shared_moment
not_shared_moment = cfg.not_shared_moment

# config file might have changed verbose
verbose = verbose_print(verbose=verbose_flag, timestamp=timestamp, theme=theme)
Expand Down
37 changes: 37 additions & 0 deletions osxphotos/photoinfo.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@
from .query_builder import get_query
from .scoreinfo import ScoreInfo
from .searchinfo import SearchInfo
from .shareinfo import ShareInfo, get_moment_share_info, get_share_info
from .shareparticipant import ShareParticipant, get_share_participants
from .uti import get_preferred_uti_extension, get_uti_for_extension
from .utils import _get_resource_loc, hexdigest, list_directory

Expand Down Expand Up @@ -1435,6 +1437,41 @@ def shared_moment(self) -> bool:
"""Returns True if photo is part of a shared moment otherwise False (Photos 7+ only)"""
return bool(self._info["moment_share"])

@cached_property
def shared_moment_info(self) -> ShareInfo | None:
"""Returns ShareInfo object with information about the shared moment the photo is part of (Photos 7+ only)"""
if self._db.photos_version < 7:
return None

try:
return get_moment_share_info(self._db, self.uuid)
except ValueError:
return None

@cached_property
def share_info(self) -> ShareInfo | None:
"""Returns ShareInfo object with information about the shared photo in a shared iCloud library (Photos 8+ only) (currently experimental)"""
if self._db.photos_version < 8:
return None

try:
return get_share_info(self._db, self.uuid)
except ValueError:
return None

@cached_property
def shared_library(self) -> bool:
"""Returns True if photo is in a shared iCloud library otherwise False (Photos 8+ only)"""
# TODO: this is just a guess right now as I don't currently use shared libraries
return bool(self._info["active_library_participation_state"])

@cached_property
def share_participants(self) -> list[ShareParticipant]:
"""Returns list of ShareParticpant objects with information on who the photo is shared with (Photos 8+ only)"""
if self._db.photos_version < 8:
return []
return get_share_participants(self._db, self.uuid)

@property
def labels(self):
"""returns list of labels applied to photo by Photos image categorization
Expand Down
62 changes: 62 additions & 0 deletions osxphotos/photosdb/_photosdb_process_shared_library.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
""" Methods for PhotosDB to process shared iCloud library data (#860)"""

from __future__ import annotations

import logging
from typing import TYPE_CHECKING

from .._constants import _DB_TABLE_NAMES, _PHOTOS_SHARED_LIBRARY_VERSION
from ..sqlite_utils import sqlite_open_ro

if TYPE_CHECKING:
from osxphotos.photosdb import PhotosDB

logger = logging.getLogger("osxphotos")


def _process_shared_library_info(self: PhotosDB):
"""Process syndication information"""

if self.photos_version < 7:
raise NotImplementedError(
f"syndication info not implemented for this database version: {self.photos_version}"
)

if self._model_ver < _PHOTOS_SHARED_LIBRARY_VERSION:
return

_process_shared_library_info_8(self)


def _process_shared_library_info_8(photosdb: PhotosDB):
"""Process shared iCloud library info for Photos 8.0 and later

Args:
photosdb: an OSXPhotosDB instance
"""

db = photosdb._tmp_db
zasset = _DB_TABLE_NAMES[photosdb._photos_ver]["ASSET"]

(conn, cursor) = sqlite_open_ro(db)

result = cursor.execute(
f"""
SELECT
{zasset}.ZUUID,
{zasset}.ZACTIVELIBRARYSCOPEPARTICIPATIONSTATE,
{zasset}.ZLIBRARYSCOPESHARESTATE,
{zasset}.ZLIBRARYSCOPE
FROM {zasset}
"""
)

for row in result:
uuid = row[0]
if uuid not in photosdb._dbphotos:
logger.debug(f"Skipping shared library info for missing uuid: {uuid}")
continue
info = photosdb._dbphotos[uuid]
info["active_library_participation_state"] = row[1]
info["library_scope_share_state"] = row[2]
info["library_scope"] = row[3]
29 changes: 27 additions & 2 deletions osxphotos/photosdb/photosdb.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,11 @@
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_photos_version_from_model, get_db_version, get_model_version
from .photosdb_utils import (
get_db_version,
get_model_version,
get_photos_version_from_model,
)

if is_macos:
import photoscript
Expand Down Expand Up @@ -91,6 +95,7 @@ class PhotosDB:
labels_normalized,
labels_normalized_as_dict,
)
from ._photosdb_process_shared_library import _process_shared_library_info
from ._photosdb_process_syndicationinfo import _process_syndicationinfo

def __init__(
Expand Down Expand Up @@ -286,7 +291,7 @@ def __init__(
# Dict to hold data on imports for Photos <= 4
self._db_import_group = {}

# Dict to hold syndication info for Photos >= 8
# Dict to hold syndication info for Photos >= 7
# key is UUID and value is dict of syndication info
self._db_syndication_uuid = {}

Expand Down Expand Up @@ -1552,6 +1557,12 @@ def _process_database4(self):
info["UTI_raw"] = None
info["raw_pair_info"] = None

# placeholders for shared library info on Photos 8+
for uuid in self._dbphotos:
self._dbphotos[uuid]["active_library_participation_state"] = None
self._dbphotos[uuid]["library_scope_share_state"] = None
self._dbphotos[uuid]["library_scope"] = None

# done with the database connection
conn.close()

Expand Down Expand Up @@ -2220,6 +2231,11 @@ def _process_database5(self):
info["UTI_edited_photo"] = None
info["UTI_edited_video"] = None

# placeholder for shared library info (Photos 8+)
info["active_library_participation_state"] = None
info["library_scope_share_state"] = None
info["library_scope"] = None

self._dbphotos[uuid] = info

# compute signatures for finding possible duplicates
Expand Down Expand Up @@ -2532,6 +2548,10 @@ def _process_database5(self):
verbose("Processing syndication info.")
self._process_syndicationinfo()

if self.photos_version >= 8:
verbose("Processing shared iCloud library info")
self._process_shared_library_info()

verbose("Done processing details from Photos library.")

def _process_moments(self):
Expand Down Expand Up @@ -3546,6 +3566,11 @@ def query(self, options: QueryOptions) -> List[PhotoInfo]:
elif options.not_shared_moment:
photos = [p for p in photos if not p.shared_moment]

if options.shared_library:
photos = [p for p in photos if p.shared_library]
elif options.not_shared_library:
photos = [p for p in photos if not p.shared_library]

if options.function:
for function in options.function:
photos = function[0](photos)
Expand Down
6 changes: 6 additions & 0 deletions osxphotos/queryoptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,8 @@ class QueryOptions:
not_saved_to_library: search for syndicated photos that have not been saved to the Photos library
shared_moment: search for photos that have been shared via a shared moment
not_shared_moment: search for photos that have not been shared via a shared moment
shared_library: search for photos that are part of a shared iCloud library
not_shared_library: search for photos that are not part of a shared iCloud library
"""

added_after: Optional[datetime.datetime] = None
Expand Down Expand Up @@ -204,6 +206,8 @@ class QueryOptions:
not_saved_to_library: Optional[bool] = None
shared_moment: Optional[bool] = None
not_shared_moment: Optional[bool] = None
shared_library: Optional[bool] = None
not_shared_library: Optional[bool] = None

def asdict(self):
return asdict(self)
Expand Down Expand Up @@ -276,7 +280,9 @@ def query_options_from_kwargs(**kwargs) -> QueryOptions:
("syndicated", "not_syndicated"),
("saved_to_library", "not_saved_to_library"),
("shared_moment", "not_shared_moment"),
("shared_library", "not_shared_library"),
]

# TODO: add option to validate requiring at least one query arg
for arg, not_arg in exclusive:
if kwargs.get(arg) and kwargs.get(not_arg):
Expand Down
Loading