Skip to content

Commit

Permalink
Fix for live photo path in referenced libraries, #1459
Browse files Browse the repository at this point in the history
  • Loading branch information
RhetTbull committed Mar 25, 2024
1 parent d64fe51 commit dae23f2
Show file tree
Hide file tree
Showing 11 changed files with 105 additions and 42 deletions.
1 change: 0 additions & 1 deletion osxphotos/albuminfo.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
AlbumSortOrder,
)
from .datetime_utils import get_local_tz
from .query_builder import get_query

__all__ = [
"sort_list_by_keys",
Expand Down
25 changes: 25 additions & 0 deletions osxphotos/bookmark.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"""Work with macOS CFURL Bookmarks"""

import os
import pathlib

from .platform import assert_macos, is_macos

if is_macos:
import mac_alias


def resolve_bookmark_path(bookmark_data: bytes) -> pathlib.Path | None:
"""Get the path from a CFURL file bookmark
This works without calling CFURLCreateByResolvingBookmarkData
which fails if the target file does not exist
"""
assert_macos()
try:
bookmark = mac_alias.Bookmark.from_bytes(bookmark_data)
except Exception as e:
raise ValueError(f"Invalid bookmark: {e}") from e
path_components = bookmark.get(mac_alias.kBookmarkPath, None)
if not path_components:
return None
return pathlib.Path(f"/{os.path.join(*path_components)}")
2 changes: 1 addition & 1 deletion osxphotos/cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ def __init__(self, db=None, json=False, debug=False, group=None):
@click.pass_context
def cli_main(ctx, profile, profile_sort, **kwargs):
"""OSXPhotos: the multi-tool for your Photos library.
To get help on a specific command, use "osxphotos COMMAND --help"
or "osxphotos help COMMAND"; for example, "osxphotos help export".
Expand Down
8 changes: 6 additions & 2 deletions osxphotos/cli/export.py
Original file line number Diff line number Diff line change
Expand Up @@ -1819,13 +1819,17 @@ def sigint_handler(signal, frame):
fileutil = FileUtilMacOS

if no_exportdb:
rich_click_echo("Using temporary export database, no state information will be saved.")
rich_click_echo(
"Using temporary export database, no state information will be saved."
)
else:
if export_db.was_created:
rich_click_echo(f"Created export database [filepath]{export_db_path}")
else:
exportdbversion = export_db.version
rich_click_echo(f"Using osxphotos export database: version [num]{exportdbversion}[/] located at [filepath]{export_db_path}")
rich_click_echo(
f"Using osxphotos export database: version [num]{exportdbversion}[/] located at [filepath]{export_db_path}"
)

upgraded = export_db.was_upgraded
if upgraded:
Expand Down
61 changes: 38 additions & 23 deletions osxphotos/photoinfo.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
""" PhotoInfo class: Represents a single photo in the Photos library and provides access to the photo's attributes
"""PhotoInfo class: Represents a single photo in the Photos library and provides access to the photo's attributes
PhotosDB.photos() returns a list of PhotoInfo objects
"""

Expand Down Expand Up @@ -53,6 +53,7 @@
)
from .adjustmentsinfo import AdjustmentsInfo
from .albuminfo import AlbumInfo, ImportInfo, ProjectInfo
from .bookmark import resolve_bookmark_path
from .commentinfo import CommentInfo, LikeInfo
from .exifinfo import ExifInfo
from .exiftool import ExifToolCaching, get_exiftool_path
Expand Down Expand Up @@ -969,36 +970,32 @@ def live_photo(self) -> bool:
"""Returns True if photo is a live photo, otherwise False"""
return self._info["live_photo"]

@property
@cached_property
def path_live_photo(self) -> str | None:
"""Returns path to the associated video file for a live photo
If photo is not a live photo, returns None
If photo is missing, returns None"""

photopath = None
if self._db._db_version <= _PHOTOS_4_VERSION:
return self._path_live_photo_4()
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 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
return self._path_live_syndicated()

filename = pathlib.Path(self.path)
photopath = filename.parent.joinpath(f"{filename.stem}_3.mov")
photopath = str(photopath)
if not os.path.isfile(photopath):
# In testing, I've seen occasional missing movie for live photo
# these appear to be valid -- e.g. video component not yet downloaded from iCloud
# TODO: should this be a warning or debug?
photopath = None
else:
photopath = None
elif not (self.live_photo and self.path and not self.ismissing):
# if photo is missing or original path missing, cannot determine path to live photo
return None

return photopath
if self.shared:
return self._path_live_photo_shared_5()
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
return self._path_live_syndicated()
if self.isreference:
return self._path_live_referenced()

filename = pathlib.Path(self.path)
photopath = filename.parent.joinpath(f"{filename.stem}_3.mov")
# live photo may be missing, in which case return None
return str(photopath) if photopath.is_file() else None

def _path_live_photo_shared_5(self) -> str | None:
"""Return path for live photo for shared photos"""
Expand Down Expand Up @@ -1076,6 +1073,24 @@ def _path_live_shared_moment(self) -> str | None:
)
return live_photo if os.path.isfile(live_photo) else None

def _path_live_referenced(self) -> str | None:
"""Return path for live video for a referenced photo"""
query = get_query(
"referenced_live_photo", self._db.photos_version, uuid=self.uuid
)
if result := self._db.execute(query).fetchone():
_, volume, path_relative, bookmark = result
path = pathlib.Path("/Volumes") / volume / path_relative
if path.exists():
return str(path)
# path didn't exist so try to resolve from the bookmark data
# this only works on macOS
if is_macos:
if path := resolve_bookmark_path(bookmark):
if path.exists():
return str(path)
return None

@cached_property
def path_derivatives(self) -> list[str]:
"""Return any derivative (preview) images associated with the photo as a list of paths, sorted by file size (largest first)"""
Expand Down
14 changes: 14 additions & 0 deletions osxphotos/queries/referenced_live_photo.sql.mako
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
-- Get the path data for a referenced Live Photo
SELECT
${asset_table}.ZUUID,
ZFILESYSTEMVOLUME.ZNAME,
ZFILESYSTEMBOOKMARK.ZPATHRELATIVETOVOLUME,
ZFILESYSTEMBOOKMARK.ZBOOKMARKDATA
FROM
${asset_table}
JOIN ZINTERNALRESOURCE ON ZINTERNALRESOURCE.ZASSET = ${asset_table}.Z_PK
JOIN ZFILESYSTEMBOOKMARK ON ZFILESYSTEMBOOKMARK.ZRESOURCE = ZINTERNALRESOURCE.Z_PK
JOIN ZFILESYSTEMVOLUME ON ZFILESYSTEMVOLUME.Z_PK = ZINTERNALRESOURCE.ZFILESYSTEMVOLUME
WHERE
ZINTERNALRESOURCE.ZDATASTORESUBTYPE = 18 -- Live video component
AND ${asset_table}.ZUUID = '${uuid}';
27 changes: 14 additions & 13 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# On macOS < Monterey (12.0, platform_release = 21.x), pyobjc 9.0 is the last version that works, #1324
bitmath>=1.3.3.1,<1.4.0.0
bpylist2>=4.1.1,<5.0.0
Click>=8.1.3,<9.0
mac-alias>=2.2.2,<3.0.0; sys_platform == 'darwin'
Mako>=1.2.2,<1.3.0
more-itertools>=8.8.0,<9.0.0
objexplore>=1.6.3,<2.0.0
Expand All @@ -10,27 +12,26 @@ pathvalidate>=2.4.1,<4.0.0
photoscript>=0.3.0,<0.4.0; sys_platform == 'darwin'
pip
ptpython>=3.0.20,<4.0.0
pyobjc-core>=9.0,<11.0; sys_platform == 'darwin' and platform_release >= '22.0'
pyobjc-framework-AppleScriptKit>=9.0,<11.0; sys_platform == 'darwin' and platform_release >= '22.0'
pyobjc-framework-AppleScriptObjC>=9.0,<11.0; sys_platform == 'darwin' and platform_release >= '22.0'
pyobjc-framework-AVFoundation>=9.0,<11.0; sys_platform == 'darwin' and platform_release >= '22.0'
pyobjc-framework-Cocoa>=9.0,<11.0; sys_platform == 'darwin' and platform_release >= '22.0'
pyobjc-framework-CoreServices>=9.0,<11.0; sys_platform == 'darwin' and platform_release >= '22.0'
pyobjc-framework-Metal>=9.0,<11.0; sys_platform == 'darwin' and platform_release >= '22.0'
pyobjc-framework-Photos>=9.0,<11.0; sys_platform == 'darwin' and platform_release >= '22.0'
pyobjc-framework-Quartz>=9.0,<11.0; sys_platform == 'darwin' and platform_release >= '22.0'
pyobjc-framework-Vision>=9.0,<11.0; sys_platform == 'darwin' and platform_release >= '22.0'
# On macOS < Monterey (12.0, platform_release = 21.x), pyobjc 9.0 is the last version that works, #1324
pyobjc-core>=9.0,<10.0; sys_platform == 'darwin' and platform_release < '22.0'
pyobjc-core>=9.0,<11.0; sys_platform == 'darwin' and platform_release >= '22.0'
pyobjc-framework-AppleScriptKit>=9.0,<10.0; sys_platform == 'darwin' and platform_release < '22.0'
pyobjc-framework-AppleScriptKit>=9.0,<11.0; sys_platform == 'darwin' and platform_release >= '22.0'
pyobjc-framework-AppleScriptObjC>=9.0,<10.0; sys_platform == 'darwin' and platform_release < '22.0'
pyobjc-framework-AppleScriptObjC>=9.0,<11.0; sys_platform == 'darwin' and platform_release >= '22.0'
pyobjc-framework-AVFoundation>=9.0,<10.0; sys_platform == 'darwin' and platform_release < '22.0'
pyobjc-framework-AVFoundation>=9.0,<11.0; sys_platform == 'darwin' and platform_release >= '22.0'
pyobjc-framework-Cocoa>=9.0,<10.0; sys_platform == 'darwin' and platform_release < '22.0'
pyobjc-framework-Cocoa>=9.0,<11.0; sys_platform == 'darwin' and platform_release >= '22.0'
pyobjc-framework-CoreServices>=9.0,<10.0; sys_platform == 'darwin' and platform_release < '22.0'
pyobjc-framework-CoreServices>=9.0,<11.0; sys_platform == 'darwin' and platform_release >= '22.0'
pyobjc-framework-Metal>=9.0,<10.0; sys_platform == 'darwin' and platform_release < '22.0'
pyobjc-framework-Metal>=9.0,<11.0; sys_platform == 'darwin' and platform_release >= '22.0'
pyobjc-framework-Photos>=9.0,<10.0; sys_platform == 'darwin' and platform_release < '22.0'
pyobjc-framework-Photos>=9.0,<11.0; sys_platform == 'darwin' and platform_release >= '22.0'
pyobjc-framework-Quartz>=9.0,<10.0; sys_platform == 'darwin' and platform_release < '22.0'
pyobjc-framework-Quartz>=9.0,<11.0; sys_platform == 'darwin' and platform_release >= '22.0'
pyobjc-framework-Vision>=9.0,<10.0; sys_platform == 'darwin' and platform_release < '22.0'
pyobjc-framework-Vision>=9.0,<11.0; sys_platform == 'darwin' and platform_release >= '22.0'
pytimeparse2>=1.4.0,<2.0.0
PyYAML>=6.0.0,<7.0.0
requests>=2.27.1,<3.0.0
Expand All @@ -43,5 +44,5 @@ textx>=4.0.1,<5.0.0
toml>=0.10.2,<0.11.0
wrapt>=1.14.1,<2.0.0
wurlitzer>=3.0.2,<4.0.0
xdg==5.1.1; python_version <= '3.9'
xdg-base-dirs>=6.0.0; python_version >= '3.10'
xdg-base-dirs>=6.0.0; python_version >= '3.10'
xdg==5.1.1; python_version <= '3.9'
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
"bitmath>=1.3.3.1,<1.4.0.0",
"bpylist2>=4.1.1,<5.0.0",
"Click>=8.1.3,<9.0",
"mac-alias>=2.2.2,<3.0.0; sys_platform == 'darwin'",
"Mako>=1.2.2,<1.3.0",
"more-itertools>=8.8.0,<9.0.0",
"objexplore>=1.6.3,<2.0.0",
Expand Down
Binary file added tests/test-images/IMG_6161.HEIC
Binary file not shown.
Binary file added tests/test-images/IMG_6161.mov
Binary file not shown.
8 changes: 6 additions & 2 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -7933,7 +7933,9 @@ def test_export_exportdb():
],
)
assert result.exit_code == 0
assert re.search(r"Using osxphotos export database: version.*export\.db", result.output)
assert re.search(
r"Using osxphotos export database: version.*export\.db", result.output
)

# export again w/o --exportdb
result = runner.invoke(
Expand Down Expand Up @@ -8004,7 +8006,9 @@ def test_export_exportdb_ramdb():
],
)
assert result.exit_code == 0
assert re.search(r"Using osxphotos export database: version.*export\.db", result.output)
assert re.search(
r"Using osxphotos export database: version.*export\.db", result.output
)
assert "exported: 0" in result.output


Expand Down

0 comments on commit dae23f2

Please sign in to comment.