Added support for movies for Photos 5; fixed bugs in ismissing and path
34
README.md
@ -25,7 +25,7 @@
|
||||
- [`library_path`](#library_path)
|
||||
- [`db_path`](#db_path)
|
||||
- [`db_version`](#db_version)
|
||||
- [`photos(keywords=[], uuid=[], persons=[], albums=[])`](#photoskeywords-uuid-persons-albums)
|
||||
- [` photos(keywords=None, uuid=None, persons=None, albums=None, images=True, movies=False)`](#-photoskeywordsnone-uuidnone-personsnone-albumsnone-imagestrue-moviesfalse)
|
||||
+ [PhotoInfo](#photoinfo)
|
||||
- [`uuid`](#uuid)
|
||||
- [`filename`](#filename)
|
||||
@ -45,6 +45,9 @@
|
||||
- [`hidden`](#hidden)
|
||||
- [`location`](#location)
|
||||
- [`shared`](#shared)
|
||||
- [`isphoto`](#isphoto)
|
||||
- [`ismovie`](#ismovie)
|
||||
- [`uti`](#uti)
|
||||
- [`json()`](#json)
|
||||
- [`export(dest, *filename, edited=False, overwrite=False, increment=True, sidecar=False)`](#exportdest-filename-editedfalse-overwritefalse-incrementtrue-sidecarfalse)
|
||||
+ [Utility Functions](#utility-functions)
|
||||
@ -380,7 +383,7 @@ photosdb.db_version
|
||||
Returns the version number for Photos library database. You likely won't need this but it's provided in case needed for debugging. PhotosDB will print a warning to `sys.stderr` if you open a database version that has not been tested.
|
||||
|
||||
|
||||
#### `photos(keywords=[], uuid=[], persons=[], albums=[])`
|
||||
#### ` photos(keywords=None, uuid=None, persons=None, albums=None, images=True, movies=False)`
|
||||
|
||||
```python
|
||||
# assumes photosdb is a PhotosDB object (see above)
|
||||
@ -397,7 +400,9 @@ photos = photosdb.photos(
|
||||
keywords = [],
|
||||
uuid = [],
|
||||
persons = [],
|
||||
albums = []
|
||||
albums = [],
|
||||
images = bool,
|
||||
movies = bool,
|
||||
)
|
||||
```
|
||||
|
||||
@ -405,8 +410,10 @@ photos = photosdb.photos(
|
||||
- ```uuid```: list of one or more uuids. Returns only photos whos UUID matches. **Note**: The UUID is the universally unique identifier that the Photos database uses to identify each photo. You shouldn't normally need to use this but it is a way to access a specific photo if you know the UUID. If more than more uuid is provided, returns photos that match any of the uuids (e.g. treated as "or")
|
||||
- ```persons```: list of one or more persons. Returns only photos containing the person(s). If more than one person provided, returns photos that match any of the persons (e.g. treated as "or")
|
||||
- ```albums```: list of one or more album names. Returns only photos contained in the album(s). If more than one album name is provided, returns photos contained in any of the albums (.e.g. treated as "or")
|
||||
- ```images```: bool; if True, returns photos/images; default is True
|
||||
- ```movies```: bool; if True, returns movies/videos; default is False
|
||||
|
||||
If more than one of these parameters is provided, they are treated as "and" criteria. E.g.
|
||||
If more than one of (keywords, uuid, persons, albums) is provided, they are treated as "and" criteria. E.g.
|
||||
|
||||
Finds all photos with (keyword = "wedding" or "birthday") and (persons = "Juan Rodriguez")
|
||||
|
||||
@ -447,6 +454,16 @@ photos2 = photosdb.photos(keywords=["Kids"])
|
||||
photos3 = [p for p in photos2 if p not in photos1]
|
||||
```
|
||||
|
||||
By default, photos() only returns images, not movies. To also get movies, pass movies=True:
|
||||
```python
|
||||
photos_and_movies = photosdb.photos(movies=True)
|
||||
```
|
||||
|
||||
To get only movies:
|
||||
```python
|
||||
movies = photosdb.photos(images=False, movies=True)
|
||||
```
|
||||
|
||||
### PhotoInfo
|
||||
PhotosDB.photos() returns a list of PhotoInfo objects. Each PhotoInfo object represents a single photo in the Photos library.
|
||||
|
||||
@ -506,6 +523,15 @@ Returns True if photo is in a shared album, otherwise False.
|
||||
|
||||
**Note**: *Only valid on Photos 5 / MacOS 10.15*; on Photos <= 4, returns None instead of True/False.
|
||||
|
||||
#### `isphoto`
|
||||
Returns True if type is photo/still image, otherwise False
|
||||
|
||||
#### `ismovie`
|
||||
Returns True if type is movie/video, otherwise False
|
||||
|
||||
#### `uti`
|
||||
Returns Uniform Type Identifier (UTI) for the image, for example: 'public.jpeg' or 'com.apple.quicktime-movie'
|
||||
|
||||
#### `json()`
|
||||
Returns a JSON representation of all photo info
|
||||
|
||||
|
||||
@ -3,6 +3,7 @@ import logging
|
||||
from ._version import __version__
|
||||
from .photoinfo import PhotoInfo
|
||||
from .photosdb import PhotosDB
|
||||
from .utils import _set_debug, _debug, _get_logger
|
||||
|
||||
# TODO: find edited photos: see https://github.com/orangeturtle739/photos-export/blob/master/extract_photos.py
|
||||
# TODO: Add test for imageTimeZoneOffsetSeconds = None
|
||||
@ -13,31 +14,3 @@ from .photosdb import PhotosDB
|
||||
# TODO: Add special albums and magic albums
|
||||
# TODO: cleanup os.path and pathlib code (import pathlib and also from pathlib import Path)
|
||||
|
||||
|
||||
# set _DEBUG = True to enable debug output
|
||||
_DEBUG = False
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.DEBUG,
|
||||
format="%(asctime)s - %(levelname)s - %(filename)s - %(lineno)d - %(message)s",
|
||||
)
|
||||
|
||||
if not _DEBUG:
|
||||
logging.disable(logging.DEBUG)
|
||||
|
||||
|
||||
def _get_logger():
|
||||
"""Used only for testing
|
||||
|
||||
Returns:
|
||||
logging.Logger object -- logging.Logger object for osxphotos
|
||||
"""
|
||||
return logging.Logger(__name__)
|
||||
|
||||
|
||||
def _debug(debug):
|
||||
""" Enable or disable debug logging """
|
||||
if debug:
|
||||
logging.disable(logging.NOTSET)
|
||||
else:
|
||||
logging.disable(logging.DEBUG)
|
||||
|
||||
@ -16,12 +16,12 @@ from ._version import __version__
|
||||
from .utils import create_path_by_date
|
||||
|
||||
# TODO: add "--any" to search any field (e.g. keyword, description, title contains "wedding") (add case insensitive option)
|
||||
|
||||
# TODO: add search for filename
|
||||
|
||||
class CLI_Obj:
|
||||
def __init__(self, db=None, json=False, debug=False):
|
||||
if debug:
|
||||
osxphotos._debug(True)
|
||||
osxphotos._set_debug(True)
|
||||
self.db = db
|
||||
self.json = json
|
||||
|
||||
@ -106,6 +106,14 @@ def info(cli_obj):
|
||||
movies = pdb.photos(images=False, movies=True)
|
||||
info["movie_count"] = len(movies)
|
||||
|
||||
if pdb.db_version >= _PHOTOS_5_VERSION:
|
||||
shared_photos = [p for p in photos if p.shared]
|
||||
info["shared_photo_count"] = len(shared_photos)
|
||||
|
||||
shared_movies = [p for p in movies if p.shared]
|
||||
info["shared_movie_count"] = len(shared_movies)
|
||||
|
||||
|
||||
keywords = pdb.keywords_as_dict
|
||||
info["keywords_count"] = len(keywords)
|
||||
info["keywords"] = keywords
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
Constants used by osxphotos
|
||||
"""
|
||||
|
||||
|
||||
# which Photos library database versions have been tested
|
||||
# Photos 2.0 (10.12.6) == 2622
|
||||
# Photos 3.0 (10.13.6) == 3301
|
||||
@ -28,3 +29,4 @@ _PHOTOS_5_SHARED_PHOTO_PATH = "resources/cloudsharing/data"
|
||||
# What type of file? Based on ZGENERICASSET.ZKIND in Photos 5 database
|
||||
_PHOTO_TYPE = 0
|
||||
_MOVIE_TYPE = 1
|
||||
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
""" version info """
|
||||
|
||||
__version__ = "0.18.03"
|
||||
__version__ = "0.19.00"
|
||||
|
||||
@ -83,20 +83,6 @@ class PhotoInfo:
|
||||
return photopath
|
||||
# TODO: Is there a way to use applescript or PhotoKit to force the download in this
|
||||
|
||||
if self._info["masterFingerprint"]:
|
||||
# if masterFingerprint is not null, path appears to be valid
|
||||
if self._info["directory"].startswith("/"):
|
||||
photopath = os.path.join(
|
||||
self._info["directory"], self._info["filename"]
|
||||
)
|
||||
else:
|
||||
photopath = os.path.join(
|
||||
self._db._masters_path,
|
||||
self._info["directory"],
|
||||
self._info["filename"],
|
||||
)
|
||||
return photopath
|
||||
|
||||
if self._info["shared"]:
|
||||
# shared photo
|
||||
photopath = os.path.join(
|
||||
@ -107,13 +93,23 @@ class PhotoInfo:
|
||||
)
|
||||
return photopath
|
||||
|
||||
# if all else fails, photopath = None
|
||||
photopath = None
|
||||
logging.debug(
|
||||
f"WARNING: photopath None, masterFingerprint null, not shared {pformat(self._info)}"
|
||||
# if self._info["masterFingerprint"]:
|
||||
# if masterFingerprint is not null, path appears to be valid
|
||||
if self._info["directory"].startswith("/"):
|
||||
photopath = os.path.join(self._info["directory"], self._info["filename"])
|
||||
else:
|
||||
photopath = os.path.join(
|
||||
self._db._masters_path, self._info["directory"], self._info["filename"]
|
||||
)
|
||||
return photopath
|
||||
|
||||
# if all else fails, photopath = None
|
||||
# photopath = None
|
||||
# logging.debug(
|
||||
# f"WARNING: photopath None, masterFingerprint null, not shared {pformat(self._info)}"
|
||||
# )
|
||||
# return photopath
|
||||
|
||||
@property
|
||||
def path_edited(self):
|
||||
""" absolute path on disk of the edited picture """
|
||||
@ -176,12 +172,20 @@ class PhotoInfo:
|
||||
if self._info["hasAdjustments"]:
|
||||
library = self._db._library_path
|
||||
directory = self._uuid[0] # first char of uuid
|
||||
filename = None
|
||||
if self._info["type"] == _PHOTO_TYPE:
|
||||
# it's a photo
|
||||
filename = f"{self._uuid}_1_201_a.jpeg"
|
||||
elif self._info["type"] == _MOVIE_TYPE:
|
||||
# it's a movie
|
||||
filename = f"{self._uuid}_2_0_a.mov"
|
||||
else:
|
||||
# don't know what it is!
|
||||
logging.debug(f"WARNING: unknown type {self._info['type']}")
|
||||
return None
|
||||
|
||||
photopath = os.path.join(
|
||||
library,
|
||||
"resources",
|
||||
"renders",
|
||||
directory,
|
||||
f"{self._uuid}_1_201_a.jpeg",
|
||||
library, "resources", "renders", directory, filename
|
||||
)
|
||||
|
||||
if not os.path.isfile(photopath):
|
||||
|
||||
@ -16,16 +16,16 @@ from pprint import pformat
|
||||
from shutil import copyfile
|
||||
|
||||
from ._constants import (
|
||||
_MOVIE_TYPE,
|
||||
_PHOTO_TYPE,
|
||||
_PHOTOS_5_VERSION,
|
||||
_TESTED_DB_VERSIONS,
|
||||
_TESTED_OS_VERSIONS,
|
||||
_UNKNOWN_PERSON,
|
||||
_PHOTO_TYPE,
|
||||
_MOVIE_TYPE,
|
||||
)
|
||||
from ._version import __version__
|
||||
from .photoinfo import PhotoInfo
|
||||
from .utils import _check_file_exists, _get_os_version, get_last_library_path
|
||||
from .utils import _check_file_exists, _get_os_version, get_last_library_path, _debug
|
||||
|
||||
# TODO: Add test for imageTimeZoneOffsetSeconds = None
|
||||
# TODO: Fix command line so multiple --keyword, etc. are AND (instead of OR as they are in .photos())
|
||||
@ -83,6 +83,7 @@ class PhotosDB:
|
||||
# list of temporary files created so we can clean them up later
|
||||
self._tmp_files = []
|
||||
|
||||
if _debug():
|
||||
logging.debug(f"dbfile = {dbfile}")
|
||||
|
||||
# get the path to photos library database
|
||||
@ -117,6 +118,7 @@ class PhotosDB:
|
||||
if not _check_file_exists(dbfile):
|
||||
raise FileNotFoundError(f"dbfile {dbfile} does not exist", dbfile)
|
||||
|
||||
if _debug():
|
||||
logging.debug(f"dbfile = {dbfile}")
|
||||
|
||||
self._dbfile = self._dbfile_actual = os.path.abspath(dbfile)
|
||||
@ -126,6 +128,7 @@ class PhotosDB:
|
||||
|
||||
# If Photos >= 5, actual data isn't in photos.db but in Photos.sqlite
|
||||
if int(self._db_version) >= int(_PHOTOS_5_VERSION):
|
||||
if _debug():
|
||||
logging.debug(f"version is {self._db_version}")
|
||||
dbpath = pathlib.Path(self._dbfile).parent
|
||||
dbfile = dbpath / "Photos.sqlite"
|
||||
@ -134,6 +137,7 @@ class PhotosDB:
|
||||
else:
|
||||
self._tmp_db = self._copy_db_file(dbfile)
|
||||
self._dbfile_actual = dbfile
|
||||
if _debug():
|
||||
logging.debug(
|
||||
f"_dbfile = {self._dbfile}, _dbfile_actual = {self._dbfile_actual}"
|
||||
)
|
||||
@ -148,6 +152,7 @@ class PhotosDB:
|
||||
masters_path = os.path.join(library_path, "originals")
|
||||
self._masters_path = masters_path
|
||||
|
||||
if _debug():
|
||||
logging.debug(f"library = {library_path}, masters = {masters_path}")
|
||||
|
||||
if int(self._db_version) < int(_PHOTOS_5_VERSION):
|
||||
@ -162,11 +167,13 @@ class PhotosDB:
|
||||
# logging.debug(f"tmp files = {self._tmp_files}")
|
||||
for f in self._tmp_files:
|
||||
if os.path.exists(f):
|
||||
if _debug():
|
||||
logging.debug(f"cleaning up {f}")
|
||||
try:
|
||||
os.remove(f)
|
||||
self._tmp_files.remove(f)
|
||||
except Exception as e:
|
||||
if _debug():
|
||||
logging.debug("exception %e removing %s" % (e, f))
|
||||
else:
|
||||
self._tmp_files.remove(f)
|
||||
@ -334,6 +341,7 @@ class PhotosDB:
|
||||
raise Exception
|
||||
|
||||
self._tmp_files.extend(tmp_files)
|
||||
if _debug():
|
||||
logging.debug(self._tmp_files)
|
||||
|
||||
return tmp
|
||||
@ -437,6 +445,7 @@ class PhotosDB:
|
||||
"cloudownerhashedpersonid": None, # Photos 5
|
||||
}
|
||||
|
||||
if _debug():
|
||||
logging.debug(f"Finished walking through albums")
|
||||
logging.debug(pformat(self._dbalbums_album))
|
||||
logging.debug(pformat(self._dbalbums_uuid))
|
||||
@ -504,6 +513,7 @@ class PhotosDB:
|
||||
|
||||
for row in c:
|
||||
uuid = row[0]
|
||||
if _debug():
|
||||
logging.debug(f"uuid = '{uuid}, master = '{row[2]}")
|
||||
self._dbphotos[uuid] = {}
|
||||
self._dbphotos[uuid]["_uuid"] = uuid # stored here for easier debugging
|
||||
@ -549,6 +559,7 @@ class PhotosDB:
|
||||
self._dbphotos[uuid]["type"] = _MOVIE_TYPE
|
||||
else:
|
||||
# unknown
|
||||
if _debug():
|
||||
logging.debug(f"WARNING: {uuid} found unknown type {row[21]}")
|
||||
self._dbphotos[uuid]["type"] = None
|
||||
|
||||
@ -591,6 +602,7 @@ class PhotosDB:
|
||||
and row[6] == 2
|
||||
):
|
||||
if "edit_resource_id" in self._dbphotos[uuid]:
|
||||
if _debug():
|
||||
logging.debug(
|
||||
f"WARNING: found more than one edit_resource_id for "
|
||||
f"UUID {row[0]},adjustmentUUID {row[1]}, modelID {row[2]}"
|
||||
@ -656,6 +668,7 @@ class PhotosDB:
|
||||
# remove temporary files
|
||||
self._cleanup_tmp_files()
|
||||
|
||||
if _debug():
|
||||
logging.debug("Faces:")
|
||||
logging.debug(pformat(self._dbfaces_uuid))
|
||||
|
||||
@ -681,6 +694,7 @@ class PhotosDB:
|
||||
""" process the Photos database to extract info """
|
||||
""" works on Photos version >= 5.0 """
|
||||
|
||||
if _debug():
|
||||
logging.debug(f"_process_database5")
|
||||
|
||||
# Epoch is Jan 1, 2001
|
||||
@ -689,6 +703,7 @@ class PhotosDB:
|
||||
(conn, c) = self._open_sql_file(self._tmp_db)
|
||||
|
||||
# Look for all combinations of persons and pictures
|
||||
if _debug():
|
||||
logging.debug(f"Getting information about persons")
|
||||
|
||||
c.execute(
|
||||
@ -707,6 +722,8 @@ class PhotosDB:
|
||||
self._dbfaces_person[person_name] = []
|
||||
self._dbfaces_uuid[person[1]].append(person_name)
|
||||
self._dbfaces_person[person_name].append(person[1])
|
||||
|
||||
if _debug():
|
||||
logging.debug(f"Finished walking through persons")
|
||||
logging.debug(pformat(self._dbfaces_person))
|
||||
logging.debug(self._dbfaces_uuid)
|
||||
@ -749,6 +766,7 @@ class PhotosDB:
|
||||
"cloudidentifier": None, # Photos4
|
||||
}
|
||||
|
||||
if _debug():
|
||||
logging.debug(f"Finished walking through albums")
|
||||
logging.debug(pformat(self._dbalbums_album))
|
||||
logging.debug(pformat(self._dbalbums_uuid))
|
||||
@ -770,6 +788,8 @@ class PhotosDB:
|
||||
self._dbkeywords_keyword[keyword[0]] = []
|
||||
self._dbkeywords_uuid[keyword[1]].append(keyword[0])
|
||||
self._dbkeywords_keyword[keyword[0]].append(keyword[1])
|
||||
|
||||
if _debug():
|
||||
logging.debug(f"Finished walking through keywords")
|
||||
logging.debug(pformat(self._dbkeywords_keyword))
|
||||
logging.debug(pformat(self._dbkeywords_uuid))
|
||||
@ -778,6 +798,8 @@ class PhotosDB:
|
||||
c.execute("SELECT ZUUID, ZNAME from ZFILESYSTEMVOLUME")
|
||||
for vol in c:
|
||||
self._dbvolumes[vol[0]] = vol[1]
|
||||
|
||||
if _debug():
|
||||
logging.debug(f"Finished walking through volumes")
|
||||
logging.debug(self._dbvolumes)
|
||||
|
||||
@ -800,7 +822,7 @@ class PhotosDB:
|
||||
"ZGENERICASSET.ZLATITUDE, "
|
||||
"ZGENERICASSET.ZLONGITUDE, "
|
||||
"ZGENERICASSET.ZHASADJUSTMENTS, "
|
||||
"ZGENERICASSET.ZCLOUDOWNERHASHEDPERSONID, "
|
||||
"ZGENERICASSET.ZCLOUDBATCHPUBLISHDATE, "
|
||||
"ZGENERICASSET.ZKIND, "
|
||||
"ZGENERICASSET.ZUNIFORMTYPEIDENTIFIER "
|
||||
"FROM ZGENERICASSET "
|
||||
@ -825,7 +847,7 @@ class PhotosDB:
|
||||
# 13 "ZGENERICASSET.ZLATITUDE, "
|
||||
# 14 "ZGENERICASSET.ZLONGITUDE, "
|
||||
# 15 "ZGENERICASSET.ZHASADJUSTMENTS "
|
||||
# 16 "ZCLOUDOWNERHASHEDPERSONID " -- If not null, indicates a shared photo
|
||||
# 16 "ZCLOUDBATCHPUBLISHDATE " -- If not null, indicates a shared photo
|
||||
# 17 "ZKIND," -- 0 = photo, 1 = movie
|
||||
# 18 " ZUNIFORMTYPEIDENTIFIER " -- UTI
|
||||
|
||||
@ -865,7 +887,7 @@ class PhotosDB:
|
||||
|
||||
self._dbphotos[uuid]["hasAdjustments"] = row[15]
|
||||
|
||||
self._dbphotos[uuid]["cloudOwnerHashedPersonID"] = row[16]
|
||||
self._dbphotos[uuid]["cloudbatchpublishdate"] = row[16]
|
||||
self._dbphotos[uuid]["shared"] = True if row[16] is not None else False
|
||||
|
||||
# these will get filled in later
|
||||
@ -883,6 +905,7 @@ class PhotosDB:
|
||||
elif row[17] == 1:
|
||||
self._dbphotos[uuid]["type"] = _MOVIE_TYPE
|
||||
else:
|
||||
if _debug():
|
||||
logging.debug(f"WARNING: {uuid} found unknown type {row[17]}")
|
||||
self._dbphotos[uuid]["type"] = None
|
||||
|
||||
@ -902,6 +925,7 @@ class PhotosDB:
|
||||
if uuid in self._dbphotos:
|
||||
self._dbphotos[uuid]["extendedDescription"] = row[1]
|
||||
else:
|
||||
if _debug():
|
||||
logging.debug(
|
||||
f"WARNING: found description {row[1]} but no photo for {uuid}"
|
||||
)
|
||||
@ -921,32 +945,18 @@ class PhotosDB:
|
||||
if uuid in self._dbphotos:
|
||||
self._dbphotos[uuid]["adjustmentFormatID"] = row[2]
|
||||
else:
|
||||
if _debug():
|
||||
logging.debug(
|
||||
f"WARNING: found adjustmentformatidentifier {row[2]} but no photo for uuid {row[0]}"
|
||||
)
|
||||
|
||||
# get information on local/remote availability
|
||||
c.execute(
|
||||
"SELECT ZGENERICASSET.ZUUID, "
|
||||
"ZINTERNALRESOURCE.ZLOCALAVAILABILITY, "
|
||||
"ZINTERNALRESOURCE.ZREMOTEAVAILABILITY "
|
||||
"FROM ZGENERICASSET "
|
||||
"JOIN ZADDITIONALASSETATTRIBUTES ON ZADDITIONALASSETATTRIBUTES.ZASSET = ZGENERICASSET.Z_PK "
|
||||
"JOIN ZINTERNALRESOURCE ON ZINTERNALRESOURCE.ZFINGERPRINT = ZADDITIONALASSETATTRIBUTES.ZMASTERFINGERPRINT "
|
||||
)
|
||||
|
||||
for row in c:
|
||||
uuid = row[0]
|
||||
if uuid in self._dbphotos:
|
||||
self._dbphotos[uuid]["localAvailability"] = row[1]
|
||||
self._dbphotos[uuid]["remoteAvailability"] = row[2]
|
||||
if row[1] != 1:
|
||||
self._dbphotos[uuid]["isMissing"] = 1
|
||||
else:
|
||||
self._dbphotos[uuid]["isMissing"] = 0
|
||||
# Find missing photos
|
||||
# TODO: this code is very kludgy and I had to make lots of assumptions
|
||||
# it's probably wrong and needs to be re-worked once I figure out how to reliably
|
||||
# determine if a photo is missing in Photos 5
|
||||
|
||||
# Get info on remote/local availability for photos in shared albums
|
||||
# Shared photos have a null fingerprint
|
||||
# Shared photos have a null fingerprint (and some other photos do too)
|
||||
c.execute(
|
||||
""" SELECT
|
||||
ZGENERICASSET.ZUUID,
|
||||
@ -955,7 +965,37 @@ class PhotosDB:
|
||||
FROM ZGENERICASSET
|
||||
JOIN ZADDITIONALASSETATTRIBUTES ON ZADDITIONALASSETATTRIBUTES.ZASSET = ZGENERICASSET.Z_PK
|
||||
JOIN ZINTERNALRESOURCE ON ZINTERNALRESOURCE.ZASSET = ZADDITIONALASSETATTRIBUTES.ZASSET
|
||||
WHERE ZINTERNALRESOURCE.ZFINGERPRINT IS NULL AND ZINTERNALRESOURCE.ZDATASTORESUBTYPE = 3 """
|
||||
WHERE ZDATASTORESUBTYPE = 0 OR ZDATASTORESUBTYPE = 3 """
|
||||
# WHERE ZINTERNALRESOURCE.ZFINGERPRINT IS NULL AND ZINTERNALRESOURCE.ZDATASTORESUBTYPE = 3 """
|
||||
)
|
||||
|
||||
for row in c:
|
||||
uuid = row[0]
|
||||
if uuid in self._dbphotos:
|
||||
# and self._dbphotos[uuid]["isMissing"] is None:
|
||||
self._dbphotos[uuid]["localAvailability"] = row[1]
|
||||
self._dbphotos[uuid]["remoteAvailability"] = row[2]
|
||||
|
||||
# old = self._dbphotos[uuid]["isMissing"]
|
||||
|
||||
if row[1] != 1:
|
||||
self._dbphotos[uuid]["isMissing"] = 1
|
||||
else:
|
||||
self._dbphotos[uuid]["isMissing"] = 0
|
||||
|
||||
# if old is not None and old != self._dbphotos[uuid]["isMissing"]:
|
||||
# logging.warning(
|
||||
# f"{uuid} isMissing changed: {old} {self._dbphotos[uuid]['isMissing']}"
|
||||
# )
|
||||
|
||||
# get information on local/remote availability
|
||||
c.execute(
|
||||
""" SELECT ZGENERICASSET.ZUUID,
|
||||
ZINTERNALRESOURCE.ZLOCALAVAILABILITY,
|
||||
ZINTERNALRESOURCE.ZREMOTEAVAILABILITY
|
||||
FROM ZGENERICASSET
|
||||
JOIN ZADDITIONALASSETATTRIBUTES ON ZADDITIONALASSETATTRIBUTES.ZASSET = ZGENERICASSET.Z_PK
|
||||
JOIN ZINTERNALRESOURCE ON ZINTERNALRESOURCE.ZFINGERPRINT = ZADDITIONALASSETATTRIBUTES.ZMASTERFINGERPRINT """
|
||||
)
|
||||
|
||||
for row in c:
|
||||
@ -963,11 +1003,20 @@ class PhotosDB:
|
||||
if uuid in self._dbphotos:
|
||||
self._dbphotos[uuid]["localAvailability"] = row[1]
|
||||
self._dbphotos[uuid]["remoteAvailability"] = row[2]
|
||||
|
||||
# old = self._dbphotos[uuid]["isMissing"]
|
||||
|
||||
if row[1] != 1:
|
||||
self._dbphotos[uuid]["isMissing"] = 1
|
||||
else:
|
||||
self._dbphotos[uuid]["isMissing"] = 0
|
||||
|
||||
# if old is not None and old != self._dbphotos[uuid]["isMissing"]:
|
||||
# logging.warning(
|
||||
# f"{uuid} isMissing changed: {old} {self._dbphotos[uuid]['isMissing']}"
|
||||
# )
|
||||
|
||||
if _debug():
|
||||
logging.debug(pformat(self._dbphotos))
|
||||
|
||||
# add faces and keywords to photo data
|
||||
@ -998,6 +1047,7 @@ class PhotosDB:
|
||||
conn.close()
|
||||
self._cleanup_tmp_files()
|
||||
|
||||
if _debug():
|
||||
logging.debug("Faces:")
|
||||
logging.debug(pformat(self._dbfaces_uuid))
|
||||
|
||||
@ -1013,6 +1063,9 @@ class PhotosDB:
|
||||
logging.debug("Albums by album:")
|
||||
logging.debug(pformat(self._dbalbums_album))
|
||||
|
||||
logging.debug("Album details:")
|
||||
logging.debug(pformat(self._dbalbum_details))
|
||||
|
||||
logging.debug("Volumes:")
|
||||
logging.debug(pformat(self._dbvolumes))
|
||||
|
||||
|
||||
@ -11,6 +11,41 @@ import CoreFoundation
|
||||
import objc
|
||||
from Foundation import *
|
||||
|
||||
_DEBUG = False
|
||||
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.DEBUG,
|
||||
format="%(asctime)s - %(levelname)s - %(filename)s - %(lineno)d - %(message)s",
|
||||
)
|
||||
|
||||
if not _DEBUG:
|
||||
logging.disable(logging.DEBUG)
|
||||
|
||||
|
||||
def _get_logger():
|
||||
"""Used only for testing
|
||||
|
||||
Returns:
|
||||
logging.Logger object -- logging.Logger object for osxphotos
|
||||
"""
|
||||
return logging.Logger(__name__)
|
||||
|
||||
|
||||
def _set_debug(debug):
|
||||
""" Enable or disable debug logging """
|
||||
global _DEBUG
|
||||
_DEBUG = debug
|
||||
if debug:
|
||||
logging.disable(logging.NOTSET)
|
||||
else:
|
||||
logging.disable(logging.DEBUG)
|
||||
|
||||
|
||||
def _debug():
|
||||
""" returns True if debugging turned on (via _set_debug), otherwise, false """
|
||||
return _DEBUG
|
||||
|
||||
|
||||
def _get_os_version():
|
||||
# returns tuple containing OS version
|
||||
|
||||
@ -3,8 +3,8 @@
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>PhotoAnalysisGraphLastBackgroundGraphRebuildJobDate</key>
|
||||
<date>2019-12-26T14:55:03Z</date>
|
||||
<date>2019-12-28T22:35:14Z</date>
|
||||
<key>PhotoAnalysisGraphLastBackgroundMemoryGenerationJobDate</key>
|
||||
<date>2019-12-26T14:55:03Z</date>
|
||||
<date>2019-12-29T08:28:13Z</date>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@ -11,6 +11,6 @@
|
||||
<key>PLLastRevGeoForcedProviderOutOfDateCheckVersionKey</key>
|
||||
<integer>1</integer>
|
||||
<key>PLLastRevGeoVerFileFetchDateKey</key>
|
||||
<date>2019-12-20T15:56:12Z</date>
|
||||
<date>2019-12-28T22:33:47Z</date>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
<key>hostuuid</key>
|
||||
<string>9575E48B-8D5F-5654-ABAC-4431B1167324</string>
|
||||
<key>pid</key>
|
||||
<integer>1986</integer>
|
||||
<integer>16385</integer>
|
||||
<key>processname</key>
|
||||
<string>photolibraryd</string>
|
||||
<key>uid</key>
|
||||
|
||||
@ -3,24 +3,24 @@
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>BackgroundHighlightCollection</key>
|
||||
<date>2019-12-27T04:06:37Z</date>
|
||||
<date>2019-12-29T06:18:40Z</date>
|
||||
<key>BackgroundHighlightEnrichment</key>
|
||||
<date>2019-12-27T04:06:36Z</date>
|
||||
<date>2019-12-29T06:18:40Z</date>
|
||||
<key>BackgroundJobAssetRevGeocode</key>
|
||||
<date>2019-12-27T04:06:37Z</date>
|
||||
<date>2019-12-29T06:18:40Z</date>
|
||||
<key>BackgroundJobSearch</key>
|
||||
<date>2019-12-27T04:06:37Z</date>
|
||||
<date>2019-12-29T06:18:40Z</date>
|
||||
<key>BackgroundPeopleSuggestion</key>
|
||||
<date>2019-12-27T04:06:36Z</date>
|
||||
<date>2019-12-29T06:18:40Z</date>
|
||||
<key>BackgroundUserBehaviorProcessor</key>
|
||||
<date>2019-12-27T04:06:37Z</date>
|
||||
<date>2019-12-28T23:29:58Z</date>
|
||||
<key>PhotoAnalysisGraphLastBackgroundGraphConsistencyUpdateJobDateKey</key>
|
||||
<date>2019-12-27T04:06:44Z</date>
|
||||
<date>2019-12-29T06:18:45Z</date>
|
||||
<key>PhotoAnalysisGraphLastBackgroundGraphRebuildJobDate</key>
|
||||
<date>2019-12-27T04:06:36Z</date>
|
||||
<date>2019-12-28T23:29:57Z</date>
|
||||
<key>PhotoAnalysisGraphLastBackgroundMemoryGenerationJobDate</key>
|
||||
<date>2019-12-27T04:06:37Z</date>
|
||||
<date>2019-12-29T06:18:40Z</date>
|
||||
<key>SiriPortraitDonation</key>
|
||||
<date>2019-12-27T04:06:37Z</date>
|
||||
<date>2019-12-28T23:29:58Z</date>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@ -3,8 +3,8 @@
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>FaceIDModelLastGenerationKey</key>
|
||||
<date>2019-12-27T04:06:38Z</date>
|
||||
<date>2019-12-28T23:29:59Z</date>
|
||||
<key>LastContactClassificationKey</key>
|
||||
<date>2019-12-27T04:06:40Z</date>
|
||||
<date>2019-12-28T23:30:01Z</date>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>LibrarySchemaVersion</key>
|
||||
<integer>5001</integer>
|
||||
<key>MetaSchemaVersion</key>
|
||||
<integer>3</integer>
|
||||
</dict>
|
||||
</plist>
|
||||
BIN
tests/Test-Movie-5_0.photoslibrary/database/Photos.sqlite
Normal file
BIN
tests/Test-Movie-5_0.photoslibrary/database/Photos.sqlite-shm
Normal file
BIN
tests/Test-Movie-5_0.photoslibrary/database/Photos.sqlite-wal
Normal file
@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>hostname</key>
|
||||
<string>Rhets-MacBook-Pro.local</string>
|
||||
<key>hostuuid</key>
|
||||
<string>9575E48B-8D5F-5654-ABAC-4431B1167324</string>
|
||||
<key>pid</key>
|
||||
<integer>433</integer>
|
||||
<key>processname</key>
|
||||
<string>photolibraryd</string>
|
||||
<key>uid</key>
|
||||
<integer>501</integer>
|
||||
</dict>
|
||||
</plist>
|
||||
BIN
tests/Test-Movie-5_0.photoslibrary/database/metaSchema.db
Normal file
BIN
tests/Test-Movie-5_0.photoslibrary/database/photos.db
Normal file
BIN
tests/Test-Movie-5_0.photoslibrary/database/search/psi.sqlite
Normal file
@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>insertAlbum</key>
|
||||
<array/>
|
||||
<key>insertAsset</key>
|
||||
<array/>
|
||||
<key>insertHighlight</key>
|
||||
<array/>
|
||||
<key>insertMemory</key>
|
||||
<array/>
|
||||
<key>insertMoment</key>
|
||||
<array/>
|
||||
<key>removeAlbum</key>
|
||||
<array/>
|
||||
<key>removeAsset</key>
|
||||
<array/>
|
||||
<key>removeHighlight</key>
|
||||
<array/>
|
||||
<key>removeMemory</key>
|
||||
<array/>
|
||||
<key>removeMoment</key>
|
||||
<array/>
|
||||
</dict>
|
||||
</plist>
|
||||
@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>embeddingVersion</key>
|
||||
<string>1</string>
|
||||
<key>localeIdentifier</key>
|
||||
<string>en_US</string>
|
||||
<key>sceneTaxonomySHA</key>
|
||||
<string>87914a047c69fbe8013fad2c70fa70c6c03b08b56190fe4054c880e6b9f57cc3</string>
|
||||
<key>searchIndexVersion</key>
|
||||
<string>10</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 545 KiB |
|
After Width: | Height: | Size: 578 KiB |
|
After Width: | Height: | Size: 453 KiB |
|
After Width: | Height: | Size: 532 KiB |
|
After Width: | Height: | Size: 504 KiB |
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CollapsedSidebarSectionIdentifiers</key>
|
||||
<array/>
|
||||
</dict>
|
||||
</plist>
|
||||
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>PLLibraryServicesManager.LocaleIdentifier</key>
|
||||
<string>en_US</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 198 KiB |
|
After Width: | Height: | Size: 331 KiB |
|
After Width: | Height: | Size: 132 KiB |
|
After Width: | Height: | Size: 490 KiB |
|
After Width: | Height: | Size: 290 KiB |
|
After Width: | Height: | Size: 292 KiB |
|
After Width: | Height: | Size: 189 KiB |
|
After Width: | Height: | Size: 276 KiB |
|
After Width: | Height: | Size: 230 KiB |
|
After Width: | Height: | Size: 61 KiB |
|
After Width: | Height: | Size: 103 KiB |
|
After Width: | Height: | Size: 110 KiB |
|
After Width: | Height: | Size: 109 KiB |