Added ImportInfo for Photos 5+
This commit is contained in:
36
README.md
36
README.md
@@ -15,6 +15,7 @@
|
|||||||
+ [PhotoInfo](#photoinfo)
|
+ [PhotoInfo](#photoinfo)
|
||||||
+ [ExifInfo](#exifinfo)
|
+ [ExifInfo](#exifinfo)
|
||||||
+ [AlbumInfo](#albuminfo)
|
+ [AlbumInfo](#albuminfo)
|
||||||
|
+ [ImportInfo](#importinfo)
|
||||||
+ [FolderInfo](#folderinfo)
|
+ [FolderInfo](#folderinfo)
|
||||||
+ [PlaceInfo](#placeinfo)
|
+ [PlaceInfo](#placeinfo)
|
||||||
+ [ScoreInfo](#scoreinfo)
|
+ [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.
|
**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`
|
#### `folder_info`
|
||||||
```python
|
```python
|
||||||
# assumes photosdb is a PhotosDB object (see above)
|
# 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`
|
#### `album_info`
|
||||||
Returns a list of [AlbumInfo](#AlbumInfo) objects representing the albums the photo is contained in. See also [albums](#albums).
|
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`
|
#### `persons`
|
||||||
Returns a list of the names of the persons in the photo
|
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>
|
#### <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)
|
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`
|
#### `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"]
|
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`
|
#### `parent`
|
||||||
Returns a [FolderInfo](#FolderInfo) object representing the albums parent folder or `None` if album is not a in a folder.
|
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
|
### 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.
|
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
|
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
|
# which Photos library database versions have been tested
|
||||||
# Photos 2.0 (10.12.6) == 2622
|
# Photos 2.0 (10.12.6) == 2622
|
||||||
@@ -36,11 +41,15 @@ _DB_TABLE_NAMES = {
|
|||||||
"ASSET": "ZGENERICASSET",
|
"ASSET": "ZGENERICASSET",
|
||||||
"KEYWORD_JOIN": "Z_1KEYWORDS.Z_37KEYWORDS",
|
"KEYWORD_JOIN": "Z_1KEYWORDS.Z_37KEYWORDS",
|
||||||
"ALBUM_JOIN": "Z_26ASSETS.Z_34ASSETS",
|
"ALBUM_JOIN": "Z_26ASSETS.Z_34ASSETS",
|
||||||
|
"ALBUM_SORT_ORDER": "Z_26ASSETS.Z_FOK_34ASSETS",
|
||||||
|
"IMPORT_FOK": "ZGENERICASSET.Z_FOK_IMPORTSESSION",
|
||||||
},
|
},
|
||||||
6: {
|
6: {
|
||||||
"ASSET": "ZASSET",
|
"ASSET": "ZASSET",
|
||||||
"KEYWORD_JOIN": "Z_1KEYWORDS.Z_36KEYWORDS",
|
"KEYWORD_JOIN": "Z_1KEYWORDS.Z_36KEYWORDS",
|
||||||
"ALBUM_JOIN": "Z_26ASSETS.Z_3ASSETS",
|
"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_SHARED_ALBUM_KIND = 1505 # shared album
|
||||||
_PHOTOS_5_FOLDER_KIND = 4000 # user folder
|
_PHOTOS_5_FOLDER_KIND = 4000 # user folder
|
||||||
_PHOTOS_5_ROOT_FOLDER_KIND = 3999 # root 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_ALBUM_KIND = 3 # RKAlbum.albumSubclass
|
||||||
_PHOTOS_4_TOP_LEVEL_ALBUM = "TopLevelAlbums"
|
_PHOTOS_4_TOP_LEVEL_ALBUM = "TopLevelAlbums"
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
""" version info """
|
""" 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
|
PhotosDB.folders() returns a list of FolderInfo objects
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
from datetime import datetime, timedelta, timezone
|
||||||
|
|
||||||
from ._constants import (
|
from ._constants import (
|
||||||
_PHOTOS_4_ALBUM_KIND,
|
_PHOTOS_4_ALBUM_KIND,
|
||||||
@@ -18,11 +18,34 @@ from ._constants import (
|
|||||||
_PHOTOS_4_VERSION,
|
_PHOTOS_4_VERSION,
|
||||||
_PHOTOS_5_ALBUM_KIND,
|
_PHOTOS_5_ALBUM_KIND,
|
||||||
_PHOTOS_5_FOLDER_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
|
Info about a specific Album, contains all the details about the album
|
||||||
including folders, photos, etc.
|
including folders, photos, etc.
|
||||||
"""
|
"""
|
||||||
@@ -31,33 +54,107 @@ class AlbumInfo:
|
|||||||
self._uuid = uuid
|
self._uuid = uuid
|
||||||
self._db = db
|
self._db = db
|
||||||
self._title = self._db._dbalbum_details[uuid]["title"]
|
self._title = self._db._dbalbum_details[uuid]["title"]
|
||||||
|
self._creation_date_timestamp = self._db._dbalbum_details[uuid]["creation_date"]
|
||||||
@property
|
self._start_date_timestamp = self._db._dbalbum_details[uuid]["start_date"]
|
||||||
def title(self):
|
self._end_date_timestamp = self._db._dbalbum_details[uuid]["end_date"]
|
||||||
""" return title / name of album """
|
self._local_tz = get_local_tz()
|
||||||
return self._title
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def uuid(self):
|
def uuid(self):
|
||||||
""" return uuid of album """
|
""" return uuid of album """
|
||||||
return self._uuid
|
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
|
@property
|
||||||
def photos(self):
|
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:
|
try:
|
||||||
return self._photos
|
return self._photos
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
if self.uuid in self._db._dbalbums_album:
|
if self.uuid in self._db._dbalbums_album:
|
||||||
uuid, sort_order = zip(*self._db._dbalbums_album[self.uuid])
|
uuid, sort_order = zip(*self._db._dbalbums_album[self.uuid])
|
||||||
self._photos = self._db.photos(uuid=uuid)
|
sorted_uuid = sort_list_by_keys(uuid, sort_order)
|
||||||
# PhotosDB.photos does not preserve order when passing in list of uuids
|
self._photos = self._db.photos_by_uuid(sorted_uuid)
|
||||||
# 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
|
|
||||||
]
|
|
||||||
else:
|
else:
|
||||||
self._photos = []
|
self._photos = []
|
||||||
return self._photos
|
return self._photos
|
||||||
@@ -110,9 +207,24 @@ class AlbumInfo:
|
|||||||
)
|
)
|
||||||
return self._parent
|
return self._parent
|
||||||
|
|
||||||
def __len__(self):
|
|
||||||
""" return number of photos contained in album """
|
class ImportInfo(AlbumInfoBaseClass):
|
||||||
return len(self.photos)
|
@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:
|
class FolderInfo:
|
||||||
|
|||||||
@@ -20,8 +20,7 @@ def datetime_remove_tz(dt):
|
|||||||
if type(dt) != datetime.datetime:
|
if type(dt) != datetime.datetime:
|
||||||
raise TypeError(f"dt must be type datetime.datetime, not {type(dt)}")
|
raise TypeError(f"dt must be type datetime.datetime, not {type(dt)}")
|
||||||
|
|
||||||
dt_new = dt.replace(tzinfo=None)
|
return dt.replace(tzinfo=None)
|
||||||
return dt_new
|
|
||||||
|
|
||||||
|
|
||||||
def datetime_has_tz(dt):
|
def datetime_has_tz(dt):
|
||||||
@@ -32,9 +31,7 @@ def datetime_has_tz(dt):
|
|||||||
if type(dt) != datetime.datetime:
|
if type(dt) != datetime.datetime:
|
||||||
raise TypeError(f"dt must be type datetime.datetime, not {type(dt)}")
|
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 dt.tzinfo is not None and dt.tzinfo.utcoffset(dt) is not None
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def datetime_naive_to_local(dt):
|
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)}"
|
f"{dt} has tzinfo {dt.tzinfo} and offset {dt.tizinfo.utcoffset(dt)}"
|
||||||
)
|
)
|
||||||
|
|
||||||
dt_local = dt.replace(tzinfo=get_local_tz())
|
return dt.replace(tzinfo=get_local_tz())
|
||||||
return dt_local
|
|
||||||
|
|||||||
@@ -5,16 +5,12 @@ PhotosDB.photos() returns a list of PhotoInfo objects
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import dataclasses
|
import dataclasses
|
||||||
import glob
|
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import os.path
|
import os.path
|
||||||
import pathlib
|
import pathlib
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
from datetime import timedelta, timezone
|
from datetime import timedelta, timezone
|
||||||
from pprint import pformat
|
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
@@ -25,10 +21,11 @@ from .._constants import (
|
|||||||
_PHOTOS_4_ROOT_FOLDER,
|
_PHOTOS_4_ROOT_FOLDER,
|
||||||
_PHOTOS_4_VERSION,
|
_PHOTOS_4_VERSION,
|
||||||
_PHOTOS_5_ALBUM_KIND,
|
_PHOTOS_5_ALBUM_KIND,
|
||||||
|
_PHOTOS_5_IMPORT_SESSION_ALBUM_KIND,
|
||||||
_PHOTOS_5_SHARED_ALBUM_KIND,
|
_PHOTOS_5_SHARED_ALBUM_KIND,
|
||||||
_PHOTOS_5_SHARED_PHOTO_PATH,
|
_PHOTOS_5_SHARED_PHOTO_PATH,
|
||||||
)
|
)
|
||||||
from ..albuminfo import AlbumInfo
|
from ..albuminfo import AlbumInfo, ImportInfo
|
||||||
from ..personinfo import FaceInfo, PersonInfo
|
from ..personinfo import FaceInfo, PersonInfo
|
||||||
from ..phototemplate import PhotoTemplate
|
from ..phototemplate import PhotoTemplate
|
||||||
from ..placeinfo import PlaceInfo4, PlaceInfo5
|
from ..placeinfo import PlaceInfo4, PlaceInfo5
|
||||||
@@ -88,7 +85,7 @@ class PhotoInfo:
|
|||||||
def date(self):
|
def date(self):
|
||||||
""" image creation date as timezone aware datetime object """
|
""" image creation date as timezone aware datetime object """
|
||||||
return self._info["imageDate"]
|
return self._info["imageDate"]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def date_modified(self):
|
def date_modified(self):
|
||||||
""" image modification date as timezone aware datetime object
|
""" image modification date as timezone aware datetime object
|
||||||
@@ -357,7 +354,7 @@ class PhotoInfo:
|
|||||||
except AttributeError:
|
except AttributeError:
|
||||||
try:
|
try:
|
||||||
faces = self._db._db_faceinfo_uuid[self._uuid]
|
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:
|
except KeyError:
|
||||||
# no faces
|
# no faces
|
||||||
self._faceinfo = []
|
self._faceinfo = []
|
||||||
@@ -387,6 +384,19 @@ class PhotoInfo:
|
|||||||
]
|
]
|
||||||
return self._album_info
|
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
|
@property
|
||||||
def keywords(self):
|
def keywords(self):
|
||||||
""" list of keywords for picture """
|
""" list of keywords for picture """
|
||||||
@@ -745,7 +755,7 @@ class PhotoInfo:
|
|||||||
""" Return list of album UUIDs this photo is found in
|
""" Return list of album UUIDs this photo is found in
|
||||||
|
|
||||||
Filters out albums in the trash and any special album types
|
Filters out albums in the trash and any special album types
|
||||||
|
|
||||||
Returns: list of album UUIDs
|
Returns: list of album UUIDs
|
||||||
"""
|
"""
|
||||||
if self._db._db_version <= _PHOTOS_4_VERSION:
|
if self._db._db_version <= _PHOTOS_4_VERSION:
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import os
|
|||||||
import os.path
|
import os.path
|
||||||
import pathlib
|
import pathlib
|
||||||
import platform
|
import platform
|
||||||
import sqlite3
|
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
@@ -26,15 +25,15 @@ from .._constants import (
|
|||||||
_PHOTOS_4_VERSION,
|
_PHOTOS_4_VERSION,
|
||||||
_PHOTOS_5_ALBUM_KIND,
|
_PHOTOS_5_ALBUM_KIND,
|
||||||
_PHOTOS_5_FOLDER_KIND,
|
_PHOTOS_5_FOLDER_KIND,
|
||||||
|
_PHOTOS_5_IMPORT_SESSION_ALBUM_KIND,
|
||||||
_PHOTOS_5_ROOT_FOLDER_KIND,
|
_PHOTOS_5_ROOT_FOLDER_KIND,
|
||||||
_PHOTOS_5_SHARED_ALBUM_KIND,
|
_PHOTOS_5_SHARED_ALBUM_KIND,
|
||||||
_PHOTOS_5_VERSION,
|
|
||||||
_TESTED_DB_VERSIONS,
|
|
||||||
_TESTED_OS_VERSIONS,
|
_TESTED_OS_VERSIONS,
|
||||||
_UNKNOWN_PERSON,
|
_UNKNOWN_PERSON,
|
||||||
|
TIME_DELTA,
|
||||||
)
|
)
|
||||||
from .._version import __version__
|
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 ..datetime_utils import datetime_has_tz, datetime_naive_to_local
|
||||||
from ..personinfo import PersonInfo
|
from ..personinfo import PersonInfo
|
||||||
from ..photoinfo import PhotoInfo
|
from ..photoinfo import PhotoInfo
|
||||||
@@ -46,7 +45,7 @@ from ..utils import (
|
|||||||
_open_sql_file,
|
_open_sql_file,
|
||||||
get_last_library_path,
|
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 imageTimeZoneOffsetSeconds = None
|
||||||
# TODO: Add test for __str__
|
# TODO: Add test for __str__
|
||||||
@@ -485,6 +484,18 @@ class PhotosDB:
|
|||||||
self._albums_shared = self._get_albums(shared=True)
|
self._albums_shared = self._get_albums(shared=True)
|
||||||
return self._albums_shared
|
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
|
@property
|
||||||
def db_version(self):
|
def db_version(self):
|
||||||
""" return the database version as stored in LiGlobals table """
|
""" 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 """
|
""" 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
|
# required because python's sqlite3 implementation can't read a locked file
|
||||||
# _, suffix = os.path.splitext(fname)
|
# _, suffix = os.path.splitext(fname)
|
||||||
|
dest_name = dest_path = ""
|
||||||
try:
|
try:
|
||||||
dest_name = pathlib.Path(fname).name
|
dest_name = pathlib.Path(fname).name
|
||||||
dest_path = os.path.join(self._tempdir_name, dest_name)
|
dest_path = os.path.join(self._tempdir_name, dest_name)
|
||||||
@@ -536,9 +548,6 @@ class PhotosDB:
|
|||||||
""" process the Photos database to extract info
|
""" process the Photos database to extract info
|
||||||
works on Photos version <= 4.0 """
|
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)
|
(conn, c) = _open_sql_file(self._tmp_db)
|
||||||
|
|
||||||
# get info to associate persons with photos
|
# get info to associate persons with photos
|
||||||
@@ -685,7 +694,8 @@ class PhotosDB:
|
|||||||
isInTrash,
|
isInTrash,
|
||||||
folderUuid,
|
folderUuid,
|
||||||
albumType,
|
albumType,
|
||||||
albumSubclass
|
albumSubclass,
|
||||||
|
createDate
|
||||||
FROM RKAlbum """
|
FROM RKAlbum """
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -698,6 +708,7 @@ class PhotosDB:
|
|||||||
# 5: folderUuid
|
# 5: folderUuid
|
||||||
# 6: albumType
|
# 6: albumType
|
||||||
# 7: albumSubclass -- if 3, normal user album
|
# 7: albumSubclass -- if 3, normal user album
|
||||||
|
# 8: createDate
|
||||||
|
|
||||||
for album in c:
|
for album in c:
|
||||||
self._dbalbum_details[album[0]] = {
|
self._dbalbum_details[album[0]] = {
|
||||||
@@ -715,6 +726,9 @@ class PhotosDB:
|
|||||||
"albumSubclass": album[7],
|
"albumSubclass": album[7],
|
||||||
# for compatability with Photos 5 where album kind is ZKIND
|
# for compatability with Photos 5 where album kind is ZKIND
|
||||||
"kind": album[7],
|
"kind": album[7],
|
||||||
|
"creation_date": album[8],
|
||||||
|
"start_date": None, # Photos 5 only
|
||||||
|
"end_date": None, # Photos 5 only
|
||||||
}
|
}
|
||||||
|
|
||||||
# get details about folders
|
# get details about folders
|
||||||
@@ -920,7 +934,7 @@ class PhotosDB:
|
|||||||
# not accounted for
|
# not accounted for
|
||||||
try:
|
try:
|
||||||
self._dbphotos[uuid]["lastmodifieddate"] = datetime.fromtimestamp(
|
self._dbphotos[uuid]["lastmodifieddate"] = datetime.fromtimestamp(
|
||||||
row[4] + td
|
row[4] + TIME_DELTA
|
||||||
)
|
)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
self._dbphotos[uuid]["lastmodifieddate"] = None
|
self._dbphotos[uuid]["lastmodifieddate"] = None
|
||||||
@@ -930,7 +944,7 @@ class PhotosDB:
|
|||||||
self._dbphotos[uuid]["imageTimeZoneOffsetSeconds"] = row[9]
|
self._dbphotos[uuid]["imageTimeZoneOffsetSeconds"] = row[9]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
imagedate = datetime.fromtimestamp(row[5] + td)
|
imagedate = datetime.fromtimestamp(row[5] + TIME_DELTA)
|
||||||
seconds = self._dbphotos[uuid]["imageTimeZoneOffsetSeconds"] or 0
|
seconds = self._dbphotos[uuid]["imageTimeZoneOffsetSeconds"] or 0
|
||||||
delta = timedelta(seconds=seconds)
|
delta = timedelta(seconds=seconds)
|
||||||
tz = timezone(delta)
|
tz = timezone(delta)
|
||||||
@@ -1066,6 +1080,11 @@ class PhotosDB:
|
|||||||
self._dbphotos[uuid]["original_orientation"] = row[38]
|
self._dbphotos[uuid]["original_orientation"] = row[38]
|
||||||
self._dbphotos[uuid]["original_filesize"] = row[39]
|
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
|
# get additional details from RKMaster, needed for RAW processing
|
||||||
c.execute(
|
c.execute(
|
||||||
""" SELECT
|
""" SELECT
|
||||||
@@ -1419,16 +1438,16 @@ class PhotosDB:
|
|||||||
if _debug():
|
if _debug():
|
||||||
logging.debug(f"_process_database5")
|
logging.debug(f"_process_database5")
|
||||||
|
|
||||||
# Epoch is Jan 1, 2001
|
(conn, c) = _open_sql_file(self._tmp_db)
|
||||||
td = (datetime(2001, 1, 1, 0, 0) - datetime(1970, 1, 1, 0, 0)).total_seconds()
|
|
||||||
|
|
||||||
|
# some of the tables/columns have different names in different versions of Photos
|
||||||
photos_ver = get_db_model_version(self._tmp_db)
|
photos_ver = get_db_model_version(self._tmp_db)
|
||||||
self._photos_ver = photos_ver
|
self._photos_ver = photos_ver
|
||||||
asset_table = _DB_TABLE_NAMES[photos_ver]["ASSET"]
|
asset_table = _DB_TABLE_NAMES[photos_ver]["ASSET"]
|
||||||
keyword_join = _DB_TABLE_NAMES[photos_ver]["KEYWORD_JOIN"]
|
keyword_join = _DB_TABLE_NAMES[photos_ver]["KEYWORD_JOIN"]
|
||||||
album_join = _DB_TABLE_NAMES[photos_ver]["ALBUM_JOIN"]
|
album_join = _DB_TABLE_NAMES[photos_ver]["ALBUM_JOIN"]
|
||||||
|
album_sort = _DB_TABLE_NAMES[photos_ver]["ALBUM_SORT_ORDER"]
|
||||||
(conn, c) = _open_sql_file(self._tmp_db)
|
import_fok = _DB_TABLE_NAMES[photos_ver]["IMPORT_FOK"]
|
||||||
|
|
||||||
# Look for all combinations of persons and pictures
|
# Look for all combinations of persons and pictures
|
||||||
if _debug():
|
if _debug():
|
||||||
@@ -1539,7 +1558,7 @@ class PhotosDB:
|
|||||||
f""" SELECT
|
f""" SELECT
|
||||||
ZGENERICALBUM.ZUUID,
|
ZGENERICALBUM.ZUUID,
|
||||||
{asset_table}.ZUUID,
|
{asset_table}.ZUUID,
|
||||||
{album_join}
|
{album_sort}
|
||||||
FROM {asset_table}
|
FROM {asset_table}
|
||||||
JOIN Z_26ASSETS ON {album_join} = {asset_table}.Z_PK
|
JOIN Z_26ASSETS ON {album_join} = {asset_table}.Z_PK
|
||||||
JOIN ZGENERICALBUM ON ZGENERICALBUM.Z_PK = Z_26ASSETS.Z_26ALBUMS
|
JOIN ZGENERICALBUM ON ZGENERICALBUM.Z_PK = Z_26ASSETS.Z_26ALBUMS
|
||||||
@@ -1577,7 +1596,10 @@ class PhotosDB:
|
|||||||
"ZKIND, " # 6
|
"ZKIND, " # 6
|
||||||
"ZPARENTFOLDER, " # 7
|
"ZPARENTFOLDER, " # 7
|
||||||
"Z_PK, " # 8
|
"Z_PK, " # 8
|
||||||
"ZTRASHEDSTATE " # 9
|
"ZTRASHEDSTATE, " # 9
|
||||||
|
"ZCREATIONDATE, " # 10
|
||||||
|
"ZSTARTDATE, " # 11
|
||||||
|
"ZENDDATE " # 12
|
||||||
"FROM ZGENERICALBUM "
|
"FROM ZGENERICALBUM "
|
||||||
)
|
)
|
||||||
for album in c:
|
for album in c:
|
||||||
@@ -1594,6 +1616,9 @@ 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],
|
||||||
|
"start_date": album[11],
|
||||||
|
"end_date": album[12],
|
||||||
}
|
}
|
||||||
|
|
||||||
# add cross-reference by pk to uuid
|
# 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
|
# I don't know what these mean but they will raise exception in datetime if
|
||||||
# not accounted for
|
# not accounted for
|
||||||
try:
|
try:
|
||||||
info["lastmodifieddate"] = datetime.fromtimestamp(row[4] + td)
|
info["lastmodifieddate"] = datetime.fromtimestamp(row[4] + TIME_DELTA)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
info["lastmodifieddate"] = None
|
info["lastmodifieddate"] = None
|
||||||
except TypeError:
|
except TypeError:
|
||||||
@@ -1780,7 +1805,7 @@ class PhotosDB:
|
|||||||
info["imageTimeZoneOffsetSeconds"] = row[6]
|
info["imageTimeZoneOffsetSeconds"] = row[6]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
imagedate = datetime.fromtimestamp(row[5] + td)
|
imagedate = datetime.fromtimestamp(row[5] + TIME_DELTA)
|
||||||
seconds = info["imageTimeZoneOffsetSeconds"] or 0
|
seconds = info["imageTimeZoneOffsetSeconds"] or 0
|
||||||
delta = timedelta(seconds=seconds)
|
delta = timedelta(seconds=seconds)
|
||||||
tz = timezone(delta)
|
tz = timezone(delta)
|
||||||
@@ -1925,6 +1950,12 @@ class PhotosDB:
|
|||||||
info["original_orientation"] = row[34]
|
info["original_orientation"] = row[34]
|
||||||
info["original_filesize"] = row[35]
|
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
|
# associated RAW image info
|
||||||
# will be filled in later
|
# will be filled in later
|
||||||
info["has_raw"] = False
|
info["has_raw"] = False
|
||||||
@@ -1951,6 +1982,32 @@ class PhotosDB:
|
|||||||
# else:
|
# else:
|
||||||
# info["burst"] = False
|
# 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
|
# Get extended description
|
||||||
c.execute(
|
c.execute(
|
||||||
f"""SELECT {asset_table}.ZUUID,
|
f"""SELECT {asset_table}.ZUUID,
|
||||||
@@ -2362,16 +2419,26 @@ class PhotosDB:
|
|||||||
hierarchy = _recurse_folder_hierarchy(folders)
|
hierarchy = _recurse_folder_hierarchy(folders)
|
||||||
return hierarchy
|
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
|
""" Return list of album UUIDs found in photos database
|
||||||
|
|
||||||
Filters out albums in the trash and any special album types
|
Filters out albums in the trash and any special album types
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
shared: boolean; if True, returns shared albums, else normal albums
|
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
|
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:
|
if self._db_version <= _PHOTOS_4_VERSION:
|
||||||
version4 = True
|
version4 = True
|
||||||
if shared:
|
if shared:
|
||||||
@@ -2379,11 +2446,21 @@ class PhotosDB:
|
|||||||
f"Shared albums not implemented for Photos library version {self._db_version}"
|
f"Shared albums not implemented for Photos library version {self._db_version}"
|
||||||
)
|
)
|
||||||
return [] # not implemented for _PHOTOS_4_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:
|
else:
|
||||||
album_kind = _PHOTOS_4_ALBUM_KIND
|
album_kind = _PHOTOS_4_ALBUM_KIND
|
||||||
else:
|
else:
|
||||||
version4 = False
|
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 = []
|
album_list = []
|
||||||
# look through _dbalbum_details because _dbalbums_album won't have empty albums it
|
# 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():
|
def test_folders_1():
|
||||||
@@ -228,6 +231,46 @@ def test_albums_photos():
|
|||||||
assert photo.uuid in ALBUM_PHOTO_UUID_DICT[album.title]
|
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():
|
def test_photoinfo_albums():
|
||||||
""" Test PhotoInfo.albums """
|
""" Test PhotoInfo.albums """
|
||||||
import osxphotos
|
import osxphotos
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ UUID_DICT = {
|
|||||||
"intrash": "71E3E212-00EB-430D-8A63-5E294B268554",
|
"intrash": "71E3E212-00EB-430D-8A63-5E294B268554",
|
||||||
"not_intrash": "DC99FBDD-7A52-4100-A5BB-344131646C30",
|
"not_intrash": "DC99FBDD-7A52-4100-A5BB-344131646C30",
|
||||||
"intrash_person_keywords": "6FD38366-3BF2-407D-81FE-7153EB6125B6",
|
"intrash_person_keywords": "6FD38366-3BF2-407D-81FE-7153EB6125B6",
|
||||||
|
"import_session": "8846E3E6-8AC8-4857-8448-E3D025784410",
|
||||||
}
|
}
|
||||||
|
|
||||||
UUID_PUMPKIN_FARM = [
|
UUID_PUMPKIN_FARM = [
|
||||||
@@ -1069,3 +1070,55 @@ def test_date_modified_invalid():
|
|||||||
assert len(photos) == 1
|
assert len(photos) == 1
|
||||||
p = photos[0]
|
p = photos[0]
|
||||||
assert p.date_modified is None
|
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