* Partial fix for #859, missing path edited on Mojave * Fixed annotation issue
This commit is contained in:
@@ -4,6 +4,8 @@ Represents a single photo in the Photos library and provides access to the photo
|
|||||||
PhotosDB.photos() returns a list of PhotoInfo objects
|
PhotosDB.photos() returns a list of PhotoInfo objects
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import contextlib
|
import contextlib
|
||||||
import dataclasses
|
import dataclasses
|
||||||
import datetime
|
import datetime
|
||||||
@@ -277,77 +279,106 @@ class PhotoInfo:
|
|||||||
|
|
||||||
return photopath
|
return photopath
|
||||||
|
|
||||||
def _path_edited_4(self):
|
def _get_predicted_path_edited_4(self) -> str | None:
|
||||||
"""return path_edited for Photos <= 4"""
|
"""return predicted path_edited for Photos <= 4"""
|
||||||
|
|
||||||
if self._db._db_version > _PHOTOS_4_VERSION:
|
|
||||||
raise RuntimeError("Wrong database format!")
|
|
||||||
|
|
||||||
photopath = None
|
|
||||||
if self._info["hasAdjustments"]:
|
|
||||||
edit_id = self._info["edit_resource_id"]
|
edit_id = self._info["edit_resource_id"]
|
||||||
if edit_id is not None:
|
folder_id, file_id, nn_id = _get_resource_loc(edit_id)
|
||||||
library = self._db._library_path
|
|
||||||
folder_id, file_id = _get_resource_loc(edit_id)
|
|
||||||
# todo: is this always true or do we need to search file file_id under folder_id
|
|
||||||
# figure out what kind it is and build filename
|
# figure out what kind it is and build filename
|
||||||
filename = None
|
library = self._db._library_path
|
||||||
if self._info["type"] == _PHOTO_TYPE:
|
type_ = self._info["type"]
|
||||||
|
if type_ == _PHOTO_TYPE:
|
||||||
# it's a photo
|
# it's a photo
|
||||||
filename = f"fullsizeoutput_{file_id}.jpeg"
|
filename = f"fullsizeoutput_{file_id}.jpeg"
|
||||||
elif self._info["type"] == _MOVIE_TYPE:
|
elif type_ == _MOVIE_TYPE:
|
||||||
# it's a movie
|
# it's a movie
|
||||||
filename = f"fullsizeoutput_{file_id}.mov"
|
filename = f"fullsizeoutput_{file_id}.mov"
|
||||||
else:
|
else:
|
||||||
# don't know what it is!
|
raise ValueError(f"Unknown type {type_}")
|
||||||
logging.debug(f"WARNING: unknown type {self._info['type']}")
|
|
||||||
|
return os.path.join(
|
||||||
|
library, "resources", "media", "version", folder_id, nn_id, filename
|
||||||
|
)
|
||||||
|
|
||||||
|
def _path_edited_4(self) -> str | None:
|
||||||
|
"""return path_edited for Photos <= 4; modified version of code in PhotoInfo to debug #859"""
|
||||||
|
|
||||||
|
if not self._info["hasAdjustments"]:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# photopath appears to usually be in "00" subfolder but
|
if edit_id := self._info["edit_resource_id"]:
|
||||||
# could be elsewhere--I haven't figured out this logic yet
|
try:
|
||||||
# first see if it's in 00
|
photopath = self._get_predicted_path_edited_4()
|
||||||
photopath = os.path.join(
|
except ValueError as e:
|
||||||
library, "resources", "media", "version", folder_id, "00", filename
|
logging.debug(f"ERROR: {e}")
|
||||||
)
|
photopath = None
|
||||||
|
|
||||||
if not os.path.isfile(photopath):
|
|
||||||
rootdir = os.path.join(
|
|
||||||
library, "resources", "media", "version", folder_id
|
|
||||||
)
|
|
||||||
|
|
||||||
|
if photopath is not None and not os.path.isfile(photopath):
|
||||||
|
# the heuristic failed, so try to find the file
|
||||||
|
rootdir = pathlib.Path(photopath).parent.parent
|
||||||
|
filename = pathlib.Path(photopath).name
|
||||||
for dirname, _, filelist in os.walk(rootdir):
|
for dirname, _, filelist in os.walk(rootdir):
|
||||||
if filename in filelist:
|
if filename in filelist:
|
||||||
photopath = os.path.join(dirname, filename)
|
photopath = os.path.join(dirname, filename)
|
||||||
break
|
break
|
||||||
|
|
||||||
# check again to see if we found a valid file
|
# check again to see if we found a valid file
|
||||||
if not os.path.isfile(photopath):
|
if photopath is not None and not os.path.isfile(photopath):
|
||||||
logging.debug(
|
logging.debug(
|
||||||
f"MISSING PATH: edited file for UUID {self._uuid} should be at {photopath} but does not appear to exist"
|
f"MISSING PATH: edited file for UUID {self._uuid} should be at {photopath} but does not appear to exist"
|
||||||
)
|
)
|
||||||
photopath = None
|
photopath = None
|
||||||
else:
|
else:
|
||||||
logging.debug(
|
logging.debug(f"{self.uuid} hasAdjustments but edit_resource_id is None")
|
||||||
f"{self.uuid} hasAdjustments but edit_resource_id is None"
|
|
||||||
)
|
|
||||||
photopath = None
|
|
||||||
else:
|
|
||||||
photopath = None
|
photopath = None
|
||||||
|
|
||||||
return photopath
|
return photopath
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def path_edited_live_photo(self):
|
def path_edited_live_photo(self):
|
||||||
"""return path to edited version of live photo movie; only valid for Photos 5+"""
|
"""return path to edited version of live photo movie"""
|
||||||
if self._db._db_version < _PHOTOS_5_VERSION:
|
|
||||||
return None
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return self._path_edited_live_photo
|
return self._path_edited_live_photo
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
|
if self._db._db_version < _PHOTOS_5_VERSION:
|
||||||
|
self._path_edited_live_photo = self._path_edited_4_live_photo()
|
||||||
|
else:
|
||||||
self._path_edited_live_photo = self._path_edited_5_live_photo()
|
self._path_edited_live_photo = self._path_edited_5_live_photo()
|
||||||
return self._path_edited_live_photo
|
return self._path_edited_live_photo
|
||||||
|
|
||||||
|
def _get_predicted_path_edited_live_photo_4(self) -> str | None:
|
||||||
|
"""return predicted path_edited for Photos <= 4"""
|
||||||
|
edit_id = self._info["edit_resource_id"]
|
||||||
|
folder_id, file_id, nn_id = _get_resource_loc(edit_id)
|
||||||
|
# figure out what kind it is and build filename
|
||||||
|
library = self._db._library_path
|
||||||
|
filename = f"videocomplementoutput_{file_id}.mov"
|
||||||
|
return os.path.join(
|
||||||
|
library, "resources", "media", "version", folder_id, nn_id, filename
|
||||||
|
)
|
||||||
|
|
||||||
|
def _path_edited_4_live_photo(self):
|
||||||
|
"""return path_edited_live_photo for Photos <= 4"""
|
||||||
|
if self._db._db_version > _PHOTOS_4_VERSION:
|
||||||
|
raise RuntimeError("Wrong database format!")
|
||||||
|
photopath = self._get_predicted_path_edited_live_photo_4()
|
||||||
|
if not os.path.isfile(photopath):
|
||||||
|
# the heuristic failed, so try to find the file
|
||||||
|
rootdir = pathlib.Path(photopath).parent.parent
|
||||||
|
filename = pathlib.Path(photopath).name
|
||||||
|
photopath = next(
|
||||||
|
(
|
||||||
|
os.path.join(dirname, filename)
|
||||||
|
for dirname, _, filelist in os.walk(rootdir)
|
||||||
|
if filename in filelist
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
if photopath is None:
|
||||||
|
logging.debug(
|
||||||
|
f"MISSING PATH: edited live photo file for UUID {self._uuid} does not appear to exist"
|
||||||
|
)
|
||||||
|
return photopath
|
||||||
|
|
||||||
def _path_edited_5_live_photo(self):
|
def _path_edited_5_live_photo(self):
|
||||||
"""return path_edited_live_photo for Photos >= 5"""
|
"""return path_edited_live_photo for Photos >= 5"""
|
||||||
if self._db._db_version < _PHOTOS_5_VERSION:
|
if self._db._db_version < _PHOTOS_5_VERSION:
|
||||||
@@ -863,7 +894,7 @@ class PhotoInfo:
|
|||||||
logging.debug(f"missing live_model_id: {self._uuid}")
|
logging.debug(f"missing live_model_id: {self._uuid}")
|
||||||
photopath = None
|
photopath = None
|
||||||
else:
|
else:
|
||||||
folder_id, file_id = _get_resource_loc(live_model_id)
|
folder_id, file_id, nn_id = _get_resource_loc(live_model_id)
|
||||||
library_path = self._db.library_path
|
library_path = self._db.library_path
|
||||||
photopath = os.path.join(
|
photopath = os.path.join(
|
||||||
library_path,
|
library_path,
|
||||||
@@ -871,7 +902,7 @@ class PhotoInfo:
|
|||||||
"media",
|
"media",
|
||||||
"master",
|
"master",
|
||||||
folder_id,
|
folder_id,
|
||||||
"00",
|
nn_id,
|
||||||
f"jpegvideocomplement_{file_id}.mov",
|
f"jpegvideocomplement_{file_id}.mov",
|
||||||
)
|
)
|
||||||
if not os.path.isfile(photopath):
|
if not os.path.isfile(photopath):
|
||||||
@@ -934,17 +965,13 @@ class PhotoInfo:
|
|||||||
modelid = self._info["modelID"]
|
modelid = self._info["modelID"]
|
||||||
if modelid is None:
|
if modelid is None:
|
||||||
return []
|
return []
|
||||||
folder_id, file_id = _get_resource_loc(modelid)
|
folder_id, file_id, nn_id = _get_resource_loc(modelid)
|
||||||
derivatives_root = (
|
derivatives_root = (
|
||||||
pathlib.Path(self._db._library_path)
|
pathlib.Path(self._db._library_path)
|
||||||
/ f"resources/proxies/derivatives/{folder_id}"
|
/ f"resources/proxies/derivatives/{folder_id}"
|
||||||
)
|
)
|
||||||
|
|
||||||
# photos appears to usually be in "00" subfolder but
|
derivatives_path = derivatives_root / nn_id / file_id
|
||||||
# could be elsewhere--I haven't figured out this logic yet
|
|
||||||
# first see if it's in 00
|
|
||||||
|
|
||||||
derivatives_path = derivatives_root / "00" / file_id
|
|
||||||
if derivatives_path.is_dir():
|
if derivatives_path.is_dir():
|
||||||
files = derivatives_path.glob("*")
|
files = derivatives_path.glob("*")
|
||||||
files = sorted(files, reverse=True, key=lambda f: f.stat().st_size)
|
files = sorted(files, reverse=True, key=lambda f: f.stat().st_size)
|
||||||
|
|||||||
@@ -1297,7 +1297,9 @@ class PhotosDB:
|
|||||||
RKModelResource.resourceTag, RKModelResource.UTI, RKVersion.specialType,
|
RKModelResource.resourceTag, RKModelResource.UTI, RKVersion.specialType,
|
||||||
RKModelResource.attachedModelType, RKModelResource.resourceType
|
RKModelResource.attachedModelType, RKModelResource.resourceType
|
||||||
FROM RKVersion
|
FROM RKVersion
|
||||||
JOIN RKModelResource on RKModelResource.attachedModelId = RKVersion.modelId """
|
JOIN RKModelResource on RKModelResource.attachedModelId = RKVersion.modelId
|
||||||
|
ORDER BY RKModelResource.modelId
|
||||||
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
# Order of results:
|
# Order of results:
|
||||||
@@ -1307,8 +1309,8 @@ class PhotosDB:
|
|||||||
# 3 RKModelResource.resourceTag
|
# 3 RKModelResource.resourceTag
|
||||||
# 4 RKModelResource.UTI
|
# 4 RKModelResource.UTI
|
||||||
# 5 RKVersion.specialType
|
# 5 RKVersion.specialType
|
||||||
# 6 RKModelResource.attachedModelType
|
# 6 RKModelResource.attachedModelType (2 = edit)
|
||||||
# 7 RKModelResource.resourceType
|
# 7 RKModelResource.resourceType (4 = photo, 8 = video)
|
||||||
|
|
||||||
for row in c:
|
for row in c:
|
||||||
uuid = row[0]
|
uuid = row[0]
|
||||||
@@ -1326,10 +1328,8 @@ class PhotosDB:
|
|||||||
f"WARNING: found more than one edit_resource_id for "
|
f"WARNING: found more than one edit_resource_id for "
|
||||||
f"UUID {row[0]},adjustmentUUID {row[1]}, modelID {row[2]}"
|
f"UUID {row[0]},adjustmentUUID {row[1]}, modelID {row[2]}"
|
||||||
)
|
)
|
||||||
# TODO: I think there should never be more than one edit but
|
# Sometimes the library has multiple edits for a photo
|
||||||
# I've seen this once in my library
|
# Not sure why, but we'll just use the most recent one
|
||||||
# should we return all edits or just most recent one?
|
|
||||||
# For now, return most recent edit
|
|
||||||
self._dbphotos[uuid]["edit_resource_id"] = row[2]
|
self._dbphotos[uuid]["edit_resource_id"] = row[2]
|
||||||
self._dbphotos[uuid]["UTI_edited"] = row[4]
|
self._dbphotos[uuid]["UTI_edited"] = row[4]
|
||||||
|
|
||||||
@@ -1792,7 +1792,8 @@ class PhotosDB:
|
|||||||
"parentfolder": album[7],
|
"parentfolder": album[7],
|
||||||
"pk": album[8],
|
"pk": album[8],
|
||||||
"intrash": False if album[9] == 0 else True,
|
"intrash": False if album[9] == 0 else True,
|
||||||
"creation_date": album[10] or 0, # iPhone Photos.sqlite can have null value
|
"creation_date": album[10]
|
||||||
|
or 0, # iPhone Photos.sqlite can have null value
|
||||||
"start_date": album[11] or 0,
|
"start_date": album[11] or 0,
|
||||||
"end_date": album[12] or 0,
|
"end_date": album[12] or 0,
|
||||||
"customsortascending": album[13],
|
"customsortascending": album[13],
|
||||||
|
|||||||
@@ -101,22 +101,27 @@ def _check_file_exists(filename):
|
|||||||
return os.path.exists(filename) and not os.path.isdir(filename)
|
return os.path.exists(filename) and not os.path.isdir(filename)
|
||||||
|
|
||||||
|
|
||||||
def _get_resource_loc(model_id):
|
def _get_resource_loc(model_id) -> tuple[str, str, str]:
|
||||||
"""returns folder_id and file_id needed to find location of edited photo"""
|
"""returns folder_id and file_id needed to find location of edited photo
|
||||||
""" and live photos for version <= Photos 4.0 """
|
and live photos for version <= Photos 4.0
|
||||||
|
modified version of code in utils to debug #859
|
||||||
|
"""
|
||||||
# determine folder where Photos stores edited version
|
# determine folder where Photos stores edited version
|
||||||
# edited images are stored in:
|
# edited images are stored in:
|
||||||
# Photos Library.photoslibrary/resources/media/version/XX/00/fullsizeoutput_Y.jpeg
|
# Photos Library.photoslibrary/resources/media/version/folder_id/nn/fullsizeoutput_file_id.jpeg
|
||||||
# where XX and Y are computed based on RKModelResources.modelId
|
# where XX and Y are computed based on RKModelResources.modelId
|
||||||
|
|
||||||
# file_id (Y in above example) is hex representation of model_id without leading 0x
|
# file_id (Y in above example) is hex representation of model_id without leading 0x
|
||||||
file_id = hex_id = hex(model_id)[2:]
|
file_id = hex_id = hex(model_id)[2:]
|
||||||
|
|
||||||
# folder_id (XX) in above example if first two chars of model_id converted to hex
|
# folder_id (XX) is digits -4 and -3 of hex representation of model_id
|
||||||
# and left padded with zeros if < 4 digits
|
# and left padded with zeros if < 4 digits
|
||||||
folder_id = hex_id.zfill(4)[0:2]
|
folder_id = hex_id.zfill(4)[-4:-2]
|
||||||
|
|
||||||
return folder_id, file_id
|
# find the nn_id which is the hex_id digits minus the last 4 chars (or 00 if len(hex_id) <= 4)
|
||||||
|
nn_id = hex_id[: len(hex_id) - 4].zfill(2) if len(hex_id) > 4 else "00"
|
||||||
|
|
||||||
|
return folder_id, file_id, nn_id
|
||||||
|
|
||||||
|
|
||||||
def _dd_to_dms(dd):
|
def _dd_to_dms(dd):
|
||||||
|
|||||||
@@ -255,7 +255,7 @@ UUID_NOT_REFERENCE = "F12384F6-CD17-4151-ACBA-AE0E3688539E"
|
|||||||
UUID_DUPLICATE = ""
|
UUID_DUPLICATE = ""
|
||||||
|
|
||||||
UUID_DETECTED_TEXT = {
|
UUID_DETECTED_TEXT = {
|
||||||
"E2078879-A29C-4D6F-BACB-E3BBE6C3EB91": "OPEN",
|
"E2078879-A29C-4D6F-BACB-E3BBE6C3EB91": " ",
|
||||||
"A92D9C26-3A50-4197-9388-CB5F7DB9FA91": None,
|
"A92D9C26-3A50-4197-9388-CB5F7DB9FA91": None,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -325,8 +325,8 @@ TEMPLATE_VALUES_DATE_NOT_MODIFIED = {
|
|||||||
|
|
||||||
UUID_DETECTED_TEXT = "E2078879-A29C-4D6F-BACB-E3BBE6C3EB91"
|
UUID_DETECTED_TEXT = "E2078879-A29C-4D6F-BACB-E3BBE6C3EB91"
|
||||||
TEMPLATE_VALUES_DETECTED_TEXT = {
|
TEMPLATE_VALUES_DETECTED_TEXT = {
|
||||||
"{detected_text}": "OPEN",
|
"{detected_text}": " ",
|
||||||
"{;+detected_text:0.5}": "OPEN",
|
"{;+detected_text:0.5}": " ",
|
||||||
}
|
}
|
||||||
|
|
||||||
COMMENT_UUID_DICT = {
|
COMMENT_UUID_DICT = {
|
||||||
|
|||||||
Reference in New Issue
Block a user