Added ImportInfo for Photos 5+
This commit is contained in:
36
README.md
36
README.md
@@ -15,6 +15,7 @@
|
||||
+ [PhotoInfo](#photoinfo)
|
||||
+ [ExifInfo](#exifinfo)
|
||||
+ [AlbumInfo](#albuminfo)
|
||||
+ [ImportInfo](#importinfo)
|
||||
+ [FolderInfo](#folderinfo)
|
||||
+ [PlaceInfo](#placeinfo)
|
||||
+ [ScoreInfo](#scoreinfo)
|
||||
@@ -767,6 +768,10 @@ Returns list of shared album names found in photos database (e.g. albums shared
|
||||
|
||||
**Note**: *Only valid for Photos 5 / MacOS 10.15*; on Photos <= 4, prints warning and returns empty list.
|
||||
|
||||
#### `import_info`
|
||||
|
||||
Returns a list of [ImportInfo](#importinfo) objects representing the import sessions for the database.
|
||||
|
||||
#### `folder_info`
|
||||
```python
|
||||
# assumes photosdb is a PhotosDB object (see above)
|
||||
@@ -1053,6 +1058,9 @@ Returns a list of albums the photo is contained in. See also [album_info](#album
|
||||
#### `album_info`
|
||||
Returns a list of [AlbumInfo](#AlbumInfo) objects representing the albums the photo is contained in. See also [albums](#albums).
|
||||
|
||||
#### `import_info`
|
||||
Returns an [ImportInfo](#importinfo) object representing the import session associated with the photo or `None` if there is no associated import session.
|
||||
|
||||
#### `persons`
|
||||
Returns a list of the names of the persons in the photo
|
||||
|
||||
@@ -1378,6 +1386,15 @@ Returns the title or name of the album.
|
||||
#### <a name="albumphotos">`photos`</a>
|
||||
Returns a list of [PhotoInfo](#PhotoInfo) objects representing each photo contained in the album sorted in the same order as in Photos. (e.g. if photos were manually sorted in the Photos albums, photos returned by `photos` will be in same order as they appear in the Photos album)
|
||||
|
||||
#### `creation_date`
|
||||
Returns the creation date as a timezone aware datetime.datetime object of the album.
|
||||
|
||||
#### `start_date`
|
||||
Returns the date of earliest photo in the album as a timezone aware datetime.datetime object.
|
||||
|
||||
#### `end_date`
|
||||
Returns the date of latest photo in the album as a timezone aware datetime.datetime object.
|
||||
|
||||
#### `folder_list`
|
||||
Returns a hierarchical list of [FolderInfo](#FolderInfo) objects representing the folders the album is contained in. For example, if album "AlbumInFolder" is in SubFolder2 of Folder1 as illustrated below, would return a list of `FolderInfo` objects representing ["Folder1", "SubFolder2"]
|
||||
|
||||
@@ -1403,6 +1420,25 @@ Photos Library
|
||||
#### `parent`
|
||||
Returns a [FolderInfo](#FolderInfo) object representing the albums parent folder or `None` if album is not a in a folder.
|
||||
|
||||
### ImportInfo
|
||||
PhotosDB.import_info returns a list of ImportInfo objects. Each ImportInfo object represents an import session in the library. PhotoInfo.import_info returns a single ImportInfo object representing the import session for the photo (or `None` if no associated import session).
|
||||
|
||||
**Note**: Photos 5+ only. Not implemented for Photos version <= 4.
|
||||
|
||||
#### `uuid`
|
||||
Returns the universally unique identifier (uuid) of the import session. This is how Photos keeps track of individual objects within the database.
|
||||
|
||||
#### <a name="albumphotos">`photos`</a>
|
||||
Returns a list of [PhotoInfo](#PhotoInfo) objects representing each photo contained in the album sorted in the same order as in Photos. (e.g. if photos were manually sorted in the Photos albums, photos returned by `photos` will be in same order as they appear in the Photos album)
|
||||
|
||||
#### `creation_date`
|
||||
Returns the creation date as a timezone aware datetime.datetime object of the import session.
|
||||
|
||||
#### `start_date`
|
||||
Returns the start date as a timezone aware datetime.datetime object for when the import session bega.
|
||||
|
||||
#### `end_date`
|
||||
Returns the end date as a timezone aware datetime.datetime object for when the import session completed.
|
||||
|
||||
### FolderInfo
|
||||
PhotosDB.folder_info returns a list of FolderInfo objects representing the top level folders in the library. Each FolderInfo object represents a single folder in the Photos library.
|
||||
|
||||
@@ -3,6 +3,11 @@ Constants used by osxphotos
|
||||
"""
|
||||
|
||||
import os.path
|
||||
from datetime import datetime
|
||||
|
||||
# Time delta: add this to Photos times to get unix time
|
||||
# Apple Epoch is Jan 1, 2001
|
||||
TIME_DELTA = (datetime(2001, 1, 1, 0, 0) - datetime(1970, 1, 1, 0, 0)).total_seconds()
|
||||
|
||||
# which Photos library database versions have been tested
|
||||
# Photos 2.0 (10.12.6) == 2622
|
||||
@@ -36,11 +41,15 @@ _DB_TABLE_NAMES = {
|
||||
"ASSET": "ZGENERICASSET",
|
||||
"KEYWORD_JOIN": "Z_1KEYWORDS.Z_37KEYWORDS",
|
||||
"ALBUM_JOIN": "Z_26ASSETS.Z_34ASSETS",
|
||||
"ALBUM_SORT_ORDER": "Z_26ASSETS.Z_FOK_34ASSETS",
|
||||
"IMPORT_FOK": "ZGENERICASSET.Z_FOK_IMPORTSESSION",
|
||||
},
|
||||
6: {
|
||||
"ASSET": "ZASSET",
|
||||
"KEYWORD_JOIN": "Z_1KEYWORDS.Z_36KEYWORDS",
|
||||
"ALBUM_JOIN": "Z_26ASSETS.Z_3ASSETS",
|
||||
"ALBUM_SORT_ORDER": "Z_26ASSETS.Z_FOK_3ASSETS",
|
||||
"IMPORT_FOK": "null",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -71,6 +80,7 @@ _PHOTOS_5_ALBUM_KIND = 2 # normal user album
|
||||
_PHOTOS_5_SHARED_ALBUM_KIND = 1505 # shared album
|
||||
_PHOTOS_5_FOLDER_KIND = 4000 # user folder
|
||||
_PHOTOS_5_ROOT_FOLDER_KIND = 3999 # root folder
|
||||
_PHOTOS_5_IMPORT_SESSION_ALBUM_KIND = 1506 # import session
|
||||
|
||||
_PHOTOS_4_ALBUM_KIND = 3 # RKAlbum.albumSubclass
|
||||
_PHOTOS_4_TOP_LEVEL_ALBUM = "TopLevelAlbums"
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
""" version info """
|
||||
|
||||
__version__ = "0.32.0"
|
||||
__version__ = "0.33.0"
|
||||
|
||||
@@ -10,7 +10,7 @@ Represents a single Folder in the Photos library and provides access to the fold
|
||||
PhotosDB.folders() returns a list of FolderInfo objects
|
||||
"""
|
||||
|
||||
import logging
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from ._constants import (
|
||||
_PHOTOS_4_ALBUM_KIND,
|
||||
@@ -18,11 +18,34 @@ from ._constants import (
|
||||
_PHOTOS_4_VERSION,
|
||||
_PHOTOS_5_ALBUM_KIND,
|
||||
_PHOTOS_5_FOLDER_KIND,
|
||||
TIME_DELTA,
|
||||
)
|
||||
from .datetime_utils import get_local_tz
|
||||
|
||||
|
||||
class AlbumInfo:
|
||||
def sort_list_by_keys(values, sort_keys):
|
||||
""" Sorts list values by a second list sort_keys
|
||||
e.g. given ["a","c","b"], [1, 3, 2], returns ["a", "b", "c"]
|
||||
|
||||
Args:
|
||||
values: a list of values to be sorted
|
||||
sort_keys: a list of keys to sort values by
|
||||
|
||||
Returns:
|
||||
list of values, sorted by sort_keys
|
||||
|
||||
Raises:
|
||||
ValueError: raised if len(values) != len(sort_keys)
|
||||
"""
|
||||
if len(values) != len(sort_keys):
|
||||
return ValueError("values and sort_keys must have same length")
|
||||
|
||||
return list(zip(*sorted(zip(sort_keys, values))))[1]
|
||||
|
||||
|
||||
class AlbumInfoBaseClass:
|
||||
"""
|
||||
Base class for AlbumInfo, ImportInfo
|
||||
Info about a specific Album, contains all the details about the album
|
||||
including folders, photos, etc.
|
||||
"""
|
||||
@@ -31,33 +54,107 @@ class AlbumInfo:
|
||||
self._uuid = uuid
|
||||
self._db = db
|
||||
self._title = self._db._dbalbum_details[uuid]["title"]
|
||||
|
||||
@property
|
||||
def title(self):
|
||||
""" return title / name of album """
|
||||
return self._title
|
||||
self._creation_date_timestamp = self._db._dbalbum_details[uuid]["creation_date"]
|
||||
self._start_date_timestamp = self._db._dbalbum_details[uuid]["start_date"]
|
||||
self._end_date_timestamp = self._db._dbalbum_details[uuid]["end_date"]
|
||||
self._local_tz = get_local_tz()
|
||||
|
||||
@property
|
||||
def uuid(self):
|
||||
""" return uuid of album """
|
||||
return self._uuid
|
||||
|
||||
@property
|
||||
def creation_date(self):
|
||||
""" return creation date of album """
|
||||
try:
|
||||
return self._creation_date
|
||||
except AttributeError:
|
||||
try:
|
||||
self._creation_date = (
|
||||
datetime.fromtimestamp(
|
||||
self._creation_date_timestamp + TIME_DELTA
|
||||
).astimezone(tz=self._local_tz)
|
||||
if self._creation_date_timestamp
|
||||
else datetime(1970, 1, 1, 0, 0, 0).astimezone(
|
||||
tz=timezone(timedelta(0))
|
||||
)
|
||||
)
|
||||
except ValueError:
|
||||
self._creation_date = datetime(1970, 1, 1, 0, 0, 0).astimezone(
|
||||
tz=timezone(timedelta(0))
|
||||
)
|
||||
return self._creation_date
|
||||
|
||||
@property
|
||||
def start_date(self):
|
||||
""" For Albums, return start date (earliest image) of album or None for albums with no images
|
||||
For Import Sessions, return start date of import session (when import began) """
|
||||
try:
|
||||
return self._start_date
|
||||
except AttributeError:
|
||||
try:
|
||||
self._start_date = (
|
||||
datetime.fromtimestamp(
|
||||
self._start_date_timestamp + TIME_DELTA
|
||||
).astimezone(tz=self._local_tz)
|
||||
if self._start_date_timestamp
|
||||
else None
|
||||
)
|
||||
except ValueError:
|
||||
self._start_date = None
|
||||
return self._start_date
|
||||
|
||||
@property
|
||||
def end_date(self):
|
||||
""" For Albums, return end date (most recent image) of album or None for albums with no images
|
||||
For Import Sessions, return end date of import sessions (when import was completed) """
|
||||
try:
|
||||
return self._end_date
|
||||
except AttributeError:
|
||||
try:
|
||||
self._end_date = (
|
||||
datetime.fromtimestamp(
|
||||
self._end_date_timestamp + TIME_DELTA
|
||||
).astimezone(tz=self._local_tz)
|
||||
if self._end_date_timestamp
|
||||
else None
|
||||
)
|
||||
except ValueError:
|
||||
self._end_date = None
|
||||
return self._end_date
|
||||
|
||||
@property
|
||||
def photos(self):
|
||||
""" return list of photos contained in album """
|
||||
return []
|
||||
|
||||
def __len__(self):
|
||||
""" return number of photos contained in album """
|
||||
return len(self.photos)
|
||||
|
||||
|
||||
class AlbumInfo(AlbumInfoBaseClass):
|
||||
"""
|
||||
Base class for AlbumInfo, ImportInfo
|
||||
Info about a specific Album, contains all the details about the album
|
||||
including folders, photos, etc.
|
||||
"""
|
||||
|
||||
@property
|
||||
def title(self):
|
||||
""" return title / name of album """
|
||||
return self._title
|
||||
|
||||
@property
|
||||
def photos(self):
|
||||
""" return list of photos contained in album sorted in same sort order as Photos """
|
||||
try:
|
||||
return self._photos
|
||||
except AttributeError:
|
||||
if self.uuid in self._db._dbalbums_album:
|
||||
uuid, sort_order = zip(*self._db._dbalbums_album[self.uuid])
|
||||
self._photos = self._db.photos(uuid=uuid)
|
||||
# PhotosDB.photos does not preserve order when passing in list of uuids
|
||||
# so need to build photo list one a time
|
||||
# sort uuids by sort order
|
||||
sorted_uuid = sorted(zip(sort_order, uuid))
|
||||
self._photos = [
|
||||
self._db.photos(uuid=[uuid])[0] for _, uuid in sorted_uuid
|
||||
]
|
||||
sorted_uuid = sort_list_by_keys(uuid, sort_order)
|
||||
self._photos = self._db.photos_by_uuid(sorted_uuid)
|
||||
else:
|
||||
self._photos = []
|
||||
return self._photos
|
||||
@@ -110,9 +207,24 @@ class AlbumInfo:
|
||||
)
|
||||
return self._parent
|
||||
|
||||
def __len__(self):
|
||||
""" return number of photos contained in album """
|
||||
return len(self.photos)
|
||||
|
||||
class ImportInfo(AlbumInfoBaseClass):
|
||||
@property
|
||||
def photos(self):
|
||||
""" return list of photos contained in import session """
|
||||
try:
|
||||
return self._photos
|
||||
except AttributeError:
|
||||
uuid_list, sort_order = zip(
|
||||
*[
|
||||
(uuid, self._db._dbphotos[uuid]["fok_import_session"])
|
||||
for uuid in self._db._dbphotos
|
||||
if self._db._dbphotos[uuid]["import_uuid"] == self.uuid
|
||||
]
|
||||
)
|
||||
sorted_uuid = sort_list_by_keys(uuid_list, sort_order)
|
||||
self._photos = self._db.photos_by_uuid(sorted_uuid)
|
||||
return self._photos
|
||||
|
||||
|
||||
class FolderInfo:
|
||||
|
||||
@@ -20,8 +20,7 @@ def datetime_remove_tz(dt):
|
||||
if type(dt) != datetime.datetime:
|
||||
raise TypeError(f"dt must be type datetime.datetime, not {type(dt)}")
|
||||
|
||||
dt_new = dt.replace(tzinfo=None)
|
||||
return dt_new
|
||||
return dt.replace(tzinfo=None)
|
||||
|
||||
|
||||
def datetime_has_tz(dt):
|
||||
@@ -32,9 +31,7 @@ def datetime_has_tz(dt):
|
||||
if type(dt) != datetime.datetime:
|
||||
raise TypeError(f"dt must be type datetime.datetime, not {type(dt)}")
|
||||
|
||||
if dt.tzinfo is not None and dt.tzinfo.utcoffset(dt) is not None:
|
||||
return True
|
||||
return False
|
||||
return dt.tzinfo is not None and dt.tzinfo.utcoffset(dt) is not None
|
||||
|
||||
|
||||
def datetime_naive_to_local(dt):
|
||||
@@ -53,5 +50,4 @@ def datetime_naive_to_local(dt):
|
||||
f"{dt} has tzinfo {dt.tzinfo} and offset {dt.tizinfo.utcoffset(dt)}"
|
||||
)
|
||||
|
||||
dt_local = dt.replace(tzinfo=get_local_tz())
|
||||
return dt_local
|
||||
return dt.replace(tzinfo=get_local_tz())
|
||||
|
||||
@@ -5,16 +5,12 @@ PhotosDB.photos() returns a list of PhotoInfo objects
|
||||
"""
|
||||
|
||||
import dataclasses
|
||||
import glob
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import os.path
|
||||
import pathlib
|
||||
import subprocess
|
||||
import sys
|
||||
from datetime import timedelta, timezone
|
||||
from pprint import pformat
|
||||
|
||||
import yaml
|
||||
|
||||
@@ -25,10 +21,11 @@ from .._constants import (
|
||||
_PHOTOS_4_ROOT_FOLDER,
|
||||
_PHOTOS_4_VERSION,
|
||||
_PHOTOS_5_ALBUM_KIND,
|
||||
_PHOTOS_5_IMPORT_SESSION_ALBUM_KIND,
|
||||
_PHOTOS_5_SHARED_ALBUM_KIND,
|
||||
_PHOTOS_5_SHARED_PHOTO_PATH,
|
||||
)
|
||||
from ..albuminfo import AlbumInfo
|
||||
from ..albuminfo import AlbumInfo, ImportInfo
|
||||
from ..personinfo import FaceInfo, PersonInfo
|
||||
from ..phototemplate import PhotoTemplate
|
||||
from ..placeinfo import PlaceInfo4, PlaceInfo5
|
||||
@@ -88,7 +85,7 @@ class PhotoInfo:
|
||||
def date(self):
|
||||
""" image creation date as timezone aware datetime object """
|
||||
return self._info["imageDate"]
|
||||
|
||||
|
||||
@property
|
||||
def date_modified(self):
|
||||
""" image modification date as timezone aware datetime object
|
||||
@@ -357,7 +354,7 @@ class PhotoInfo:
|
||||
except AttributeError:
|
||||
try:
|
||||
faces = self._db._db_faceinfo_uuid[self._uuid]
|
||||
self._faceinfo = [FaceInfo(db=self._db, pk=pk) for pk in faces]
|
||||
self._faceinfo = [FaceInfo(db=self._db, pk=pk) for pk in faces]
|
||||
except KeyError:
|
||||
# no faces
|
||||
self._faceinfo = []
|
||||
@@ -387,6 +384,19 @@ class PhotoInfo:
|
||||
]
|
||||
return self._album_info
|
||||
|
||||
@property
|
||||
def import_info(self):
|
||||
""" ImportInfo object representing import session for the photo or None if no import session """
|
||||
try:
|
||||
return self._import_info
|
||||
except AttributeError:
|
||||
self._import_info = (
|
||||
ImportInfo(db=self._db, uuid=self._info["import_uuid"])
|
||||
if self._info["import_uuid"] is not None
|
||||
else None
|
||||
)
|
||||
return self._import_info
|
||||
|
||||
@property
|
||||
def keywords(self):
|
||||
""" list of keywords for picture """
|
||||
@@ -745,7 +755,7 @@ class PhotoInfo:
|
||||
""" Return list of album UUIDs this photo is found in
|
||||
|
||||
Filters out albums in the trash and any special album types
|
||||
|
||||
|
||||
Returns: list of album UUIDs
|
||||
"""
|
||||
if self._db._db_version <= _PHOTOS_4_VERSION:
|
||||
|
||||
@@ -8,7 +8,6 @@ import os
|
||||
import os.path
|
||||
import pathlib
|
||||
import platform
|
||||
import sqlite3
|
||||
import sys
|
||||
import tempfile
|
||||
from datetime import datetime, timedelta, timezone
|
||||
@@ -26,15 +25,15 @@ from .._constants import (
|
||||
_PHOTOS_4_VERSION,
|
||||
_PHOTOS_5_ALBUM_KIND,
|
||||
_PHOTOS_5_FOLDER_KIND,
|
||||
_PHOTOS_5_IMPORT_SESSION_ALBUM_KIND,
|
||||
_PHOTOS_5_ROOT_FOLDER_KIND,
|
||||
_PHOTOS_5_SHARED_ALBUM_KIND,
|
||||
_PHOTOS_5_VERSION,
|
||||
_TESTED_DB_VERSIONS,
|
||||
_TESTED_OS_VERSIONS,
|
||||
_UNKNOWN_PERSON,
|
||||
TIME_DELTA,
|
||||
)
|
||||
from .._version import __version__
|
||||
from ..albuminfo import AlbumInfo, FolderInfo
|
||||
from ..albuminfo import AlbumInfo, FolderInfo, ImportInfo
|
||||
from ..datetime_utils import datetime_has_tz, datetime_naive_to_local
|
||||
from ..personinfo import PersonInfo
|
||||
from ..photoinfo import PhotoInfo
|
||||
@@ -46,7 +45,7 @@ from ..utils import (
|
||||
_open_sql_file,
|
||||
get_last_library_path,
|
||||
)
|
||||
from .photosdb_utils import get_db_version, get_db_model_version
|
||||
from .photosdb_utils import get_db_model_version, get_db_version
|
||||
|
||||
# TODO: Add test for imageTimeZoneOffsetSeconds = None
|
||||
# TODO: Add test for __str__
|
||||
@@ -485,6 +484,18 @@ class PhotosDB:
|
||||
self._albums_shared = self._get_albums(shared=True)
|
||||
return self._albums_shared
|
||||
|
||||
@property
|
||||
def import_info(self):
|
||||
""" return list of ImportInfo objects for each import session in the database """
|
||||
try:
|
||||
return self._import_info
|
||||
except AttributeError:
|
||||
self._import_info = [
|
||||
ImportInfo(db=self, uuid=album)
|
||||
for album in self._get_album_uuids(import_session=True)
|
||||
]
|
||||
return self._import_info
|
||||
|
||||
@property
|
||||
def db_version(self):
|
||||
""" return the database version as stored in LiGlobals table """
|
||||
@@ -514,6 +525,7 @@ class PhotosDB:
|
||||
""" If sqlite shared memory and write-ahead log files exist, those are copied too """
|
||||
# required because python's sqlite3 implementation can't read a locked file
|
||||
# _, suffix = os.path.splitext(fname)
|
||||
dest_name = dest_path = ""
|
||||
try:
|
||||
dest_name = pathlib.Path(fname).name
|
||||
dest_path = os.path.join(self._tempdir_name, dest_name)
|
||||
@@ -536,9 +548,6 @@ class PhotosDB:
|
||||
""" process the Photos database to extract info
|
||||
works on Photos version <= 4.0 """
|
||||
|
||||
# Epoch is Jan 1, 2001
|
||||
td = (datetime(2001, 1, 1, 0, 0) - datetime(1970, 1, 1, 0, 0)).total_seconds()
|
||||
|
||||
(conn, c) = _open_sql_file(self._tmp_db)
|
||||
|
||||
# get info to associate persons with photos
|
||||
@@ -685,7 +694,8 @@ class PhotosDB:
|
||||
isInTrash,
|
||||
folderUuid,
|
||||
albumType,
|
||||
albumSubclass
|
||||
albumSubclass,
|
||||
createDate
|
||||
FROM RKAlbum """
|
||||
)
|
||||
|
||||
@@ -698,6 +708,7 @@ class PhotosDB:
|
||||
# 5: folderUuid
|
||||
# 6: albumType
|
||||
# 7: albumSubclass -- if 3, normal user album
|
||||
# 8: createDate
|
||||
|
||||
for album in c:
|
||||
self._dbalbum_details[album[0]] = {
|
||||
@@ -715,6 +726,9 @@ class PhotosDB:
|
||||
"albumSubclass": album[7],
|
||||
# for compatability with Photos 5 where album kind is ZKIND
|
||||
"kind": album[7],
|
||||
"creation_date": album[8],
|
||||
"start_date": None, # Photos 5 only
|
||||
"end_date": None, # Photos 5 only
|
||||
}
|
||||
|
||||
# get details about folders
|
||||
@@ -920,7 +934,7 @@ class PhotosDB:
|
||||
# not accounted for
|
||||
try:
|
||||
self._dbphotos[uuid]["lastmodifieddate"] = datetime.fromtimestamp(
|
||||
row[4] + td
|
||||
row[4] + TIME_DELTA
|
||||
)
|
||||
except ValueError:
|
||||
self._dbphotos[uuid]["lastmodifieddate"] = None
|
||||
@@ -930,7 +944,7 @@ class PhotosDB:
|
||||
self._dbphotos[uuid]["imageTimeZoneOffsetSeconds"] = row[9]
|
||||
|
||||
try:
|
||||
imagedate = datetime.fromtimestamp(row[5] + td)
|
||||
imagedate = datetime.fromtimestamp(row[5] + TIME_DELTA)
|
||||
seconds = self._dbphotos[uuid]["imageTimeZoneOffsetSeconds"] or 0
|
||||
delta = timedelta(seconds=seconds)
|
||||
tz = timezone(delta)
|
||||
@@ -1066,6 +1080,11 @@ class PhotosDB:
|
||||
self._dbphotos[uuid]["original_orientation"] = row[38]
|
||||
self._dbphotos[uuid]["original_filesize"] = row[39]
|
||||
|
||||
# import session not yet handled for Photos 4
|
||||
self._dbphotos[uuid]["import_session"] = None
|
||||
self._dbphotos[uuid]["import_uuid"] = None
|
||||
self._dbphotos[uuid]["fok_import_session"] = None
|
||||
|
||||
# get additional details from RKMaster, needed for RAW processing
|
||||
c.execute(
|
||||
""" SELECT
|
||||
@@ -1419,16 +1438,16 @@ class PhotosDB:
|
||||
if _debug():
|
||||
logging.debug(f"_process_database5")
|
||||
|
||||
# Epoch is Jan 1, 2001
|
||||
td = (datetime(2001, 1, 1, 0, 0) - datetime(1970, 1, 1, 0, 0)).total_seconds()
|
||||
(conn, c) = _open_sql_file(self._tmp_db)
|
||||
|
||||
# some of the tables/columns have different names in different versions of Photos
|
||||
photos_ver = get_db_model_version(self._tmp_db)
|
||||
self._photos_ver = photos_ver
|
||||
asset_table = _DB_TABLE_NAMES[photos_ver]["ASSET"]
|
||||
keyword_join = _DB_TABLE_NAMES[photos_ver]["KEYWORD_JOIN"]
|
||||
album_join = _DB_TABLE_NAMES[photos_ver]["ALBUM_JOIN"]
|
||||
|
||||
(conn, c) = _open_sql_file(self._tmp_db)
|
||||
album_sort = _DB_TABLE_NAMES[photos_ver]["ALBUM_SORT_ORDER"]
|
||||
import_fok = _DB_TABLE_NAMES[photos_ver]["IMPORT_FOK"]
|
||||
|
||||
# Look for all combinations of persons and pictures
|
||||
if _debug():
|
||||
@@ -1539,7 +1558,7 @@ class PhotosDB:
|
||||
f""" SELECT
|
||||
ZGENERICALBUM.ZUUID,
|
||||
{asset_table}.ZUUID,
|
||||
{album_join}
|
||||
{album_sort}
|
||||
FROM {asset_table}
|
||||
JOIN Z_26ASSETS ON {album_join} = {asset_table}.Z_PK
|
||||
JOIN ZGENERICALBUM ON ZGENERICALBUM.Z_PK = Z_26ASSETS.Z_26ALBUMS
|
||||
@@ -1577,7 +1596,10 @@ class PhotosDB:
|
||||
"ZKIND, " # 6
|
||||
"ZPARENTFOLDER, " # 7
|
||||
"Z_PK, " # 8
|
||||
"ZTRASHEDSTATE " # 9
|
||||
"ZTRASHEDSTATE, " # 9
|
||||
"ZCREATIONDATE, " # 10
|
||||
"ZSTARTDATE, " # 11
|
||||
"ZENDDATE " # 12
|
||||
"FROM ZGENERICALBUM "
|
||||
)
|
||||
for album in c:
|
||||
@@ -1594,6 +1616,9 @@ class PhotosDB:
|
||||
"parentfolder": album[7],
|
||||
"pk": album[8],
|
||||
"intrash": False if album[9] == 0 else True,
|
||||
"creation_date": album[10],
|
||||
"start_date": album[11],
|
||||
"end_date": album[12],
|
||||
}
|
||||
|
||||
# add cross-reference by pk to uuid
|
||||
@@ -1771,7 +1796,7 @@ class PhotosDB:
|
||||
# I don't know what these mean but they will raise exception in datetime if
|
||||
# not accounted for
|
||||
try:
|
||||
info["lastmodifieddate"] = datetime.fromtimestamp(row[4] + td)
|
||||
info["lastmodifieddate"] = datetime.fromtimestamp(row[4] + TIME_DELTA)
|
||||
except ValueError:
|
||||
info["lastmodifieddate"] = None
|
||||
except TypeError:
|
||||
@@ -1780,7 +1805,7 @@ class PhotosDB:
|
||||
info["imageTimeZoneOffsetSeconds"] = row[6]
|
||||
|
||||
try:
|
||||
imagedate = datetime.fromtimestamp(row[5] + td)
|
||||
imagedate = datetime.fromtimestamp(row[5] + TIME_DELTA)
|
||||
seconds = info["imageTimeZoneOffsetSeconds"] or 0
|
||||
delta = timedelta(seconds=seconds)
|
||||
tz = timezone(delta)
|
||||
@@ -1925,6 +1950,12 @@ class PhotosDB:
|
||||
info["original_orientation"] = row[34]
|
||||
info["original_filesize"] = row[35]
|
||||
|
||||
# initialize import session info which will be filled in later
|
||||
# not every photo has an import session so initialize all records now
|
||||
info["import_session"] = None
|
||||
info["fok_import_session"] = None
|
||||
info["import_uuid"] = None
|
||||
|
||||
# associated RAW image info
|
||||
# will be filled in later
|
||||
info["has_raw"] = False
|
||||
@@ -1951,6 +1982,32 @@ class PhotosDB:
|
||||
# else:
|
||||
# info["burst"] = False
|
||||
|
||||
# get info on import sessions
|
||||
# 0 ZGENERICASSET.ZUUID
|
||||
# 1 ZGENERICASSET.ZIMPORTSESSION
|
||||
# 2 ZGENERICASSET.Z_FOK_IMPORTSESSION
|
||||
# 3 ZGENERICALBUM.ZUUID,
|
||||
c.execute(
|
||||
f"""SELECT
|
||||
{asset_table}.ZUUID,
|
||||
{asset_table}.ZIMPORTSESSION,
|
||||
{import_fok},
|
||||
ZGENERICALBUM.ZUUID
|
||||
FROM
|
||||
{asset_table}
|
||||
JOIN ZGENERICALBUM ON ZGENERICALBUM.Z_PK = {asset_table}.ZIMPORTSESSION
|
||||
"""
|
||||
)
|
||||
|
||||
for row in c:
|
||||
uuid = row[0]
|
||||
try:
|
||||
self._dbphotos[uuid]["import_session"] = row[1]
|
||||
self._dbphotos[uuid]["fok_import_session"] = row[2]
|
||||
self._dbphotos[uuid]["import_uuid"] = row[3]
|
||||
except KeyError:
|
||||
logging.debug(f"No info record for uuid {uuid} for import session")
|
||||
|
||||
# Get extended description
|
||||
c.execute(
|
||||
f"""SELECT {asset_table}.ZUUID,
|
||||
@@ -2362,16 +2419,26 @@ class PhotosDB:
|
||||
hierarchy = _recurse_folder_hierarchy(folders)
|
||||
return hierarchy
|
||||
|
||||
def _get_album_uuids(self, shared=False):
|
||||
def _get_album_uuids(self, shared=False, import_session=False):
|
||||
""" Return list of album UUIDs found in photos database
|
||||
|
||||
Filters out albums in the trash and any special album types
|
||||
|
||||
Args:
|
||||
shared: boolean; if True, returns shared albums, else normal albums
|
||||
import_session: boolean, if True, returns import session albums, else normal or shared albums
|
||||
Note: flags (shared, import_session) are mutually exclusive
|
||||
|
||||
Raises:
|
||||
ValueError: raised if mutually exclusive flags passed
|
||||
|
||||
Returns: list of album UUIDs
|
||||
"""
|
||||
if shared and import_session:
|
||||
raise ValueError(
|
||||
"flags are mutually exclusive: pass zero or one of shared, import_session"
|
||||
)
|
||||
|
||||
if self._db_version <= _PHOTOS_4_VERSION:
|
||||
version4 = True
|
||||
if shared:
|
||||
@@ -2379,11 +2446,21 @@ class PhotosDB:
|
||||
f"Shared albums not implemented for Photos library version {self._db_version}"
|
||||
)
|
||||
return [] # not implemented for _PHOTOS_4_VERSION
|
||||
elif import_session:
|
||||
logging.warning(
|
||||
f"Import sessions not implemented for Photos library version {self._db_version}"
|
||||
)
|
||||
return [] # not implemented for _PHOTOS_4_VERSION
|
||||
else:
|
||||
album_kind = _PHOTOS_4_ALBUM_KIND
|
||||
else:
|
||||
version4 = False
|
||||
album_kind = _PHOTOS_5_SHARED_ALBUM_KIND if shared else _PHOTOS_5_ALBUM_KIND
|
||||
if shared:
|
||||
album_kind = _PHOTOS_5_SHARED_ALBUM_KIND
|
||||
elif import_session:
|
||||
album_kind = _PHOTOS_5_IMPORT_SESSION_ALBUM_KIND
|
||||
else:
|
||||
album_kind = _PHOTOS_5_ALBUM_KIND
|
||||
|
||||
album_list = []
|
||||
# look through _dbalbum_details because _dbalbums_album won't have empty albums it
|
||||
|
||||
@@ -55,7 +55,10 @@ ALBUM_PHOTO_UUID_DICT = {
|
||||
],
|
||||
}
|
||||
|
||||
UUID_DICT = {"two_albums": "F12384F6-CD17-4151-ACBA-AE0E3688539E"}
|
||||
UUID_DICT = {
|
||||
"two_albums": "F12384F6-CD17-4151-ACBA-AE0E3688539E",
|
||||
"album_dates": "0C514A98-7B77-4E4F-801B-364B7B65EAFA",
|
||||
}
|
||||
|
||||
|
||||
def test_folders_1():
|
||||
@@ -228,6 +231,46 @@ def test_albums_photos():
|
||||
assert photo.uuid in ALBUM_PHOTO_UUID_DICT[album.title]
|
||||
|
||||
|
||||
def test_album_dates():
|
||||
""" Test album date methods """
|
||||
import datetime
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
|
||||
album = [a for a in photosdb.album_info if a.uuid == UUID_DICT["album_dates"]][0]
|
||||
assert album.creation_date == datetime.datetime(
|
||||
2019,
|
||||
7,
|
||||
27,
|
||||
6,
|
||||
19,
|
||||
13,
|
||||
706262,
|
||||
tzinfo=datetime.timezone(datetime.timedelta(days=-1, seconds=61200), "PDT"),
|
||||
)
|
||||
assert album.start_date == datetime.datetime(
|
||||
2018,
|
||||
9,
|
||||
28,
|
||||
12,
|
||||
35,
|
||||
49,
|
||||
63000,
|
||||
tzinfo=datetime.timezone(datetime.timedelta(days=-1, seconds=61200), "PDT"),
|
||||
)
|
||||
assert album.end_date == datetime.datetime(
|
||||
2018,
|
||||
9,
|
||||
28,
|
||||
13,
|
||||
9,
|
||||
33,
|
||||
22000,
|
||||
tzinfo=datetime.timezone(datetime.timedelta(days=-1, seconds=61200), "PDT"),
|
||||
)
|
||||
|
||||
|
||||
def test_photoinfo_albums():
|
||||
""" Test PhotoInfo.albums """
|
||||
import osxphotos
|
||||
|
||||
@@ -74,6 +74,7 @@ UUID_DICT = {
|
||||
"intrash": "71E3E212-00EB-430D-8A63-5E294B268554",
|
||||
"not_intrash": "DC99FBDD-7A52-4100-A5BB-344131646C30",
|
||||
"intrash_person_keywords": "6FD38366-3BF2-407D-81FE-7153EB6125B6",
|
||||
"import_session": "8846E3E6-8AC8-4857-8448-E3D025784410",
|
||||
}
|
||||
|
||||
UUID_PUMPKIN_FARM = [
|
||||
@@ -1069,3 +1070,55 @@ def test_date_modified_invalid():
|
||||
assert len(photos) == 1
|
||||
p = photos[0]
|
||||
assert p.date_modified is None
|
||||
|
||||
|
||||
def test_import_session_count():
|
||||
""" Test PhotosDB.import_session """
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
|
||||
import_sessions = photosdb.import_info
|
||||
assert len(import_sessions) == 10
|
||||
|
||||
|
||||
def test_import_session_photo():
|
||||
""" Test photo.import_session """
|
||||
import datetime
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
photo = photosdb.get_photo(UUID_DICT["import_session"])
|
||||
import_session = photo.import_info
|
||||
assert import_session.creation_date == datetime.datetime(
|
||||
2020,
|
||||
6,
|
||||
6,
|
||||
7,
|
||||
15,
|
||||
24,
|
||||
729811,
|
||||
tzinfo=datetime.timezone(datetime.timedelta(days=-1, seconds=61200), "PDT"),
|
||||
)
|
||||
assert import_session.start_date == datetime.datetime(
|
||||
2020,
|
||||
6,
|
||||
6,
|
||||
7,
|
||||
15,
|
||||
24,
|
||||
725564,
|
||||
tzinfo=datetime.timezone(datetime.timedelta(days=-1, seconds=61200), "PDT"),
|
||||
)
|
||||
assert import_session.end_date == datetime.datetime(
|
||||
2020,
|
||||
6,
|
||||
6,
|
||||
7,
|
||||
15,
|
||||
24,
|
||||
725564,
|
||||
tzinfo=datetime.timezone(datetime.timedelta(days=-1, seconds=61200), "PDT"),
|
||||
)
|
||||
assert len(import_session.photos) == 1
|
||||
|
||||
|
||||
Reference in New Issue
Block a user