Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b7c7b9f066 | ||
|
|
21e7020fec | ||
|
|
952741d488 | ||
|
|
9fef12ed37 | ||
|
|
97362fc0f1 | ||
|
|
e09f0b40f1 | ||
|
|
b749681c6d | ||
|
|
8544667c72 | ||
|
|
d6a22b765a | ||
|
|
96365728c2 | ||
|
|
c01f713f00 | ||
|
|
1aa3838c38 | ||
|
|
cde56e9d13 | ||
|
|
1c9da5ed6f | ||
|
|
d74f7f499b | ||
|
|
c85bb02304 |
40
CHANGELOG.md
40
CHANGELOG.md
@@ -4,6 +4,46 @@ All notable changes to this project will be documented in this file. Dates are d
|
||||
|
||||
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
||||
|
||||
#### [v0.27.3](https://github.com/RhetTbull/osxphotos/compare/v0.27.1...v0.27.3)
|
||||
|
||||
> 12 April 2020
|
||||
|
||||
- Added additional tests for album_info [`97362fc`](https://github.com/RhetTbull/osxphotos/commit/97362fc0f13b2867abc013f4ba97ae60b0700894)
|
||||
- Fixed bug with handling of deleted albums [`9fef12e`](https://github.com/RhetTbull/osxphotos/commit/9fef12ed37634a7bdb11232976b4b2ddccd1a7cb)
|
||||
|
||||
#### [v0.27.1](https://github.com/RhetTbull/osxphotos/compare/v0.27.0...v0.27.1)
|
||||
|
||||
> 12 April 2020
|
||||
|
||||
- Changed AlbumInfo and FolderInfo interface to maintain backwards compatibility with PhotosDB.albums [`e09f0b4`](https://github.com/RhetTbull/osxphotos/commit/e09f0b40f1671d70ee399cdc519492b04fac8adc)
|
||||
- Updated CHANGELOG.md [`b749681`](https://github.com/RhetTbull/osxphotos/commit/b749681c6d2545eacf653ab1b2a5d1384e3123eb)
|
||||
|
||||
#### [v0.27.0](https://github.com/RhetTbull/osxphotos/compare/v0.26.1...v0.27.0)
|
||||
|
||||
> 11 April 2020
|
||||
|
||||
- Update README.md [`#95`](https://github.com/RhetTbull/osxphotos/pull/95)
|
||||
- Added tests and README for AlbumInfo and FolderInfo [`d6a22b7`](https://github.com/RhetTbull/osxphotos/commit/d6a22b765ab17f6ef1ba8c50b77946f090979968)
|
||||
- Added albuminfo.py for AlbumInfo and FolderInfo classes [`9636572`](https://github.com/RhetTbull/osxphotos/commit/96365728c2ff42abfb6828872ffac53b4c3c8024)
|
||||
- Updated CHANGELOG.md [`cde56e9`](https://github.com/RhetTbull/osxphotos/commit/cde56e9d13baf3098ec85839cf1aaa33b4915ac9)
|
||||
- Update README.md TOC [`8544667`](https://github.com/RhetTbull/osxphotos/commit/8544667c729ea0d7fe39671d909e09cda519e250)
|
||||
|
||||
#### [v0.26.1](https://github.com/RhetTbull/osxphotos/compare/v0.26.0...v0.26.1)
|
||||
|
||||
> 11 April 2020
|
||||
|
||||
- Bug fix for PhotosDB.photos() query [`1c9da5e`](https://github.com/RhetTbull/osxphotos/commit/1c9da5ed6ffa21f0577906b65b7da08951725d1f)
|
||||
- Updated test library [`d74f7f4`](https://github.com/RhetTbull/osxphotos/commit/d74f7f499bf59f37ec81cfa9d49cbbf3aafb5961)
|
||||
- Updated CHANGELOG.md [`c85bb02`](https://github.com/RhetTbull/osxphotos/commit/c85bb023042e072d6688060eb259156c2fa579b9)
|
||||
|
||||
#### [v0.26.0](https://github.com/RhetTbull/osxphotos/compare/v0.25.1...v0.26.0)
|
||||
|
||||
> 11 April 2020
|
||||
|
||||
- Added test for 10.15.4 [`1820715`](https://github.com/RhetTbull/osxphotos/commit/182071584904d001a9b199eef5febfb79e00696e)
|
||||
- Changed PhotosDB albums interface as prep for adding folders [`3e50626`](https://github.com/RhetTbull/osxphotos/commit/3e5062684ab6d706d91d4abeb4e3b0ca47867b70)
|
||||
- Updated CHANGELOG.md [`a6ca3f4`](https://github.com/RhetTbull/osxphotos/commit/a6ca3f453ce0fae4e8d13c7c256ed69a16d2e3f2)
|
||||
|
||||
#### [v0.25.1](https://github.com/RhetTbull/osxphotos/compare/v0.25.0...v0.25.1)
|
||||
|
||||
> 5 April 2020
|
||||
|
||||
168
README.md
168
README.md
@@ -13,6 +13,8 @@
|
||||
* [Package Interface](#package-interface)
|
||||
+ [PhotosDB](#photosdb)
|
||||
+ [PhotoInfo](#photoinfo)
|
||||
+ [AlbumInfo](#albuminfo)
|
||||
+ [FolderInfo](#folderinfo)
|
||||
+ [PlaceInfo](#placeinfo)
|
||||
+ [Template Functions](#template-functions)
|
||||
+ [Utility Functions](#utility-functions)
|
||||
@@ -105,16 +107,21 @@ Options:
|
||||
order: 1. last opened library, 2. system
|
||||
library, 3. ~/Pictures/Photos
|
||||
Library.photoslibrary
|
||||
--keyword KEYWORD Search for keyword KEYWORD. If more than one
|
||||
keyword, treated as "OR", e.g. find photos
|
||||
match any keyword
|
||||
--person PERSON Search for person PERSON. If more than one
|
||||
person, treated as "OR", e.g. find photos
|
||||
match any person
|
||||
--album ALBUM Search for album ALBUM. If more than one
|
||||
album, treated as "OR", e.g. find photos
|
||||
match any album
|
||||
--uuid UUID Search for UUID(s).
|
||||
--keyword KEYWORD Search for photos with keyword KEYWORD. If
|
||||
more than one keyword, treated as "OR", e.g.
|
||||
find photos match any keyword
|
||||
--person PERSON Search for photos with person PERSON. If
|
||||
more than one person, treated as "OR", e.g.
|
||||
find photos match any person
|
||||
--album ALBUM Search for photos in album ALBUM. If more
|
||||
than one album, treated as "OR", e.g. find
|
||||
photos match any album
|
||||
--folder FOLDER Search for photos in an album in folder
|
||||
FOLDER. If more than one folder, treated as
|
||||
"OR", e.g. find photos in any FOLDER. Only
|
||||
searches top level folders (e.g. does not
|
||||
look at subfolders)
|
||||
--uuid UUID Search for photos with UUID(s).
|
||||
--title TITLE Search for TITLE in title of photo.
|
||||
--no-title Search for photos with no title.
|
||||
--description DESC Search for DESC in description of photo.
|
||||
@@ -230,6 +237,11 @@ Options:
|
||||
output directory in the form
|
||||
'{name,DEFAULT}'. See below for additional
|
||||
details on templating system.
|
||||
--no-extended-attributes Don't copy extended attributes when
|
||||
exporting. You only need this if exporting
|
||||
to a filesystem that doesn't support Mac OS
|
||||
extended attributes. Only use this if you
|
||||
get an error while exporting.
|
||||
-h, --help Show this message and exit.
|
||||
|
||||
**Templating System**
|
||||
@@ -332,10 +344,13 @@ exported, one to each directory. For example: --directory
|
||||
of the following directories if the photos were created in 2019 and were in
|
||||
albums 'Vacation' and 'Family': 2019/Vacation, 2019/Family
|
||||
|
||||
Substitution Description
|
||||
{album} Album(s) photo is contained in
|
||||
{keyword} Keyword(s) assigned to photo
|
||||
{person} Person(s) / face(s) in a photo
|
||||
Substitution Description
|
||||
{album} Album(s) photo is contained in
|
||||
{folder_album} Folder path + album photo is contained in. e.g.
|
||||
'Folder/Subfolder/Album' or just 'Album' if no enclosing
|
||||
folder
|
||||
{keyword} Keyword(s) assigned to photo
|
||||
{person} Person(s) / face(s) in a photo
|
||||
```
|
||||
|
||||
Example: export all photos to ~/Desktop/export, including edited versions and live photo movies, group in folders by date created
|
||||
@@ -554,22 +569,52 @@ keywords = photosdb.keywords
|
||||
|
||||
Returns a list of the keywords found in the Photos library
|
||||
|
||||
#### `album_names`
|
||||
#### `album_info`
|
||||
```python
|
||||
# assumes photosdb is a PhotosDB object (see above)
|
||||
albums = photosdb.album_names
|
||||
albums = photosdb.album_info
|
||||
```
|
||||
|
||||
Returns a list of the albums found in the Photos library.
|
||||
Returns a list of [AlbumInfo](#AlbumInfo) objects representing albums in the database or empty list if there are no albums. See also [albums](#albums).
|
||||
|
||||
#### `albums`
|
||||
```python
|
||||
# assumes photosdb is a PhotosDB object (see above)
|
||||
album_names = photosdb.albums
|
||||
```
|
||||
|
||||
Returns a list of the album names found in the Photos library.
|
||||
|
||||
**Note**: In Photos 5.0 (MacOS 10.15/Catalina), It is possible to have more than one album with the same name in Photos. Albums with duplicate names are treated as a single album and the photos in each are combined. For example, if you have two albums named "Wedding" and each has 2 photos, osxphotos will treat this as a single album named "Wedding" with 4 photos in it.
|
||||
|
||||
#### `album_names_shared`
|
||||
See also [album_info](#album_info.)
|
||||
|
||||
Returns list of shared albums found in photos database (e.g. albums shared via iCloud photo sharing)
|
||||
#### `albums_shared`
|
||||
|
||||
Returns list of shared album names found in photos database (e.g. albums shared via iCloud photo sharing)
|
||||
|
||||
**Note**: *Only valid for Photos 5 / MacOS 10.15*; on Photos <= 4, prints warning and returns empty list.
|
||||
|
||||
#### `folder_info`
|
||||
```python
|
||||
# assumes photosdb is a PhotosDB object (see above)
|
||||
folders = photosdb.folder_info
|
||||
```
|
||||
|
||||
Returns a list of [FolderInfo](#FolderInfo) objects representing top level folders in the database or empty list if there are no folders. See also [folders](#folders).
|
||||
|
||||
**Note**: Currently folder_info is only implemented for Photos 5 (Catalina); will return empty list and output warning if called on earlier database versions.
|
||||
|
||||
#### `folders`
|
||||
```python
|
||||
# assumes photosdb is a PhotosDB object (see above)
|
||||
folders = photosdb.folders
|
||||
```
|
||||
|
||||
Returns a list names of top level folder names in the database.
|
||||
|
||||
**Note**: Currently folders is only implemented for Photos 5 (Catalina); will return empty list and output warning if called on earlier database versions.
|
||||
|
||||
#### `persons`
|
||||
```python
|
||||
# assumes photosdb is a PhotosDB object (see above)
|
||||
@@ -779,7 +824,10 @@ Returns the title of the photo
|
||||
Returns a list of keywords (e.g. tags) applied to the photo
|
||||
|
||||
#### `albums`
|
||||
Returns a list of albums the photo is contained in
|
||||
Returns a list of albums the photo is contained in. See also [album_info](#album_info).
|
||||
|
||||
#### `album_info`
|
||||
Returns a list of [AlbumInfo](#AlbumInfo) objects representing the albums the photo is contained in. See also [albums](#albums).
|
||||
|
||||
#### `persons`
|
||||
Returns a list of the names of the persons in the photo
|
||||
@@ -926,7 +974,83 @@ Then
|
||||
|
||||
If overwrite=False and increment=False, export will fail if destination file already exists
|
||||
|
||||
**Implementation Note**: Because the usual python file copy methods don't preserve all the metadata available on MacOS, export uses /usr/bin/ditto to do the copy for export. ditto preserves most metadata such as extended attributes, permissions, ACLs, etc.
|
||||
**Implementation Note**: Because the usual python file copy methods don't preserve all the metadata available on MacOS, export uses `/usr/bin/ditto` to do the copy for export. ditto preserves most metadata such as extended attributes, permissions, ACLs, etc.
|
||||
|
||||
### AlbumInfo
|
||||
PhotosDB.album_info and PhotoInfo.album_info return a list of AlbumInfo objects. Each AlbumInfo object represents a single album in the Photos library.
|
||||
|
||||
#### `uuid`
|
||||
Returns the universally unique identifier (uuid) of the album. This is how Photos keeps track of individual objects within the database.
|
||||
|
||||
#### `title`
|
||||
Returns the title or name of the album.
|
||||
|
||||
#### `photos`
|
||||
Returns a list of [PhotoInfo](#PhotoInfo) objects representing each photo contained in the album.
|
||||
|
||||
#### `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"]
|
||||
|
||||
```txt
|
||||
Photos Library
|
||||
├── Folder1
|
||||
├── SubFolder1
|
||||
├── SubFolder2
|
||||
└── AlbumInFolder
|
||||
```
|
||||
|
||||
#### `folder_names`
|
||||
Returns a hierarchical list of names of the folders the album is contained in. For example, if album is in SubFolder2 of Folder1 as illustrated below, would return ["Folder1", "SubFolder2"].
|
||||
|
||||
```txt
|
||||
Photos Library
|
||||
├── Folder1
|
||||
├── SubFolder1
|
||||
├── SubFolder2
|
||||
└── AlbumInFolder
|
||||
```
|
||||
|
||||
#### `parent`
|
||||
Returns a [FolderInfo](#FolderInfo) object representing the albums parent folder or `None` if album is not a in a folder.
|
||||
|
||||
|
||||
### FolderInfo
|
||||
PhotosDB.folders 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.
|
||||
|
||||
#### `uuid`
|
||||
Returns the universally unique identifier (uuid) of the folder. This is how Photos keeps track of individual objects within the database.
|
||||
|
||||
#### `title`
|
||||
Returns the title or name of the folder.
|
||||
|
||||
#### `album_info`
|
||||
Returns a list of [AlbumInfo](#AlbumInfo) objects representing each album contained in the folder.
|
||||
|
||||
#### `subfolders`
|
||||
Returns a list of [FolderInfo](#FolderInfo) objects representing the sub-folders of the folder.
|
||||
|
||||
#### `parent`
|
||||
Returns a [FolderInfo](#FolderInfo) object representing the folder's parent folder or `None` if album is not a in a folder.
|
||||
|
||||
**Note**: FolderInfo and AlbumInfo objects effectively work as a linked list. The children of a folder are contained in `subfolders` and `album_info` and the parent object of both `AlbumInfo` and `FolderInfo` is represented by `parent`. For example:
|
||||
|
||||
```python
|
||||
>>> import osxphotos
|
||||
>>> photosdb = osxphotos.PhotosDB()
|
||||
>>> photosdb.subfolders
|
||||
[<osxphotos.albuminfo.FolderInfo object at 0x10fcc0160>]
|
||||
>>> photosdb.folder_info[0].title
|
||||
'Folder1'
|
||||
>>> photosdb.folder_info[0].subfolders[1].title
|
||||
'SubFolder2'
|
||||
>>> photosdb.folder_info[0].subfolders[1].album_info[0].title
|
||||
'AlbumInFolder'
|
||||
>>> photosdb.folder_info[0].subfolders[1].album_info[0].parent.title
|
||||
'SubFolder2'
|
||||
>>> photosdb.folder_info[0].subfolders[1].album_info[0].parent.album_info[0].title
|
||||
'AlbumInFolder'
|
||||
```
|
||||
|
||||
|
||||
### PlaceInfo
|
||||
[PhotoInfo.place](#place) returns a PlaceInfo object if the photo contains valid reverse geolocation information. PlaceInfo has the following properties.
|
||||
@@ -1174,7 +1298,7 @@ Testing against "real world" Photos libraries would be especially helpful. If y
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
This packge works by creating a copy of the sqlite3 database that photos uses to store data about the photos library. the class photosdb then queries this database to extract information about the photos such as persons (faces identified in the photos), albums, keywords, etc. If your library is large, the database can be hundreds of MB in size and the copy then read can take many 10s of seconds to complete. Once copied, the entire database is processed and an in-memory data structure is created meaning all subsequent accesses of the PhotosDB object occur much more quickly.
|
||||
This package works by creating a copy of the sqlite3 database that photos uses to store data about the photos library. the class photosdb then queries this database to extract information about the photos such as persons (faces identified in the photos), albums, keywords, etc. If your library is large, the database can be hundreds of MB in size and the copy read then can take many 10s of seconds to complete. Once copied, the entire database is processed and an in-memory data structure is created meaning all subsequent accesses of the PhotosDB object occur much more quickly.
|
||||
|
||||
If apple changes the database format this will likely break.
|
||||
|
||||
|
||||
@@ -187,7 +187,7 @@ def query_options(f):
|
||||
metavar="KEYWORD",
|
||||
default=None,
|
||||
multiple=True,
|
||||
help="Search for keyword KEYWORD. "
|
||||
help="Search for photos with keyword KEYWORD. "
|
||||
'If more than one keyword, treated as "OR", e.g. find photos match any keyword',
|
||||
),
|
||||
o(
|
||||
@@ -195,7 +195,7 @@ def query_options(f):
|
||||
metavar="PERSON",
|
||||
default=None,
|
||||
multiple=True,
|
||||
help="Search for person PERSON. "
|
||||
help="Search for photos with person PERSON. "
|
||||
'If more than one person, treated as "OR", e.g. find photos match any person',
|
||||
),
|
||||
o(
|
||||
@@ -203,15 +203,24 @@ def query_options(f):
|
||||
metavar="ALBUM",
|
||||
default=None,
|
||||
multiple=True,
|
||||
help="Search for album ALBUM. "
|
||||
help="Search for photos in album ALBUM. "
|
||||
'If more than one album, treated as "OR", e.g. find photos match any album',
|
||||
),
|
||||
o(
|
||||
"--folder",
|
||||
metavar="FOLDER",
|
||||
default=None,
|
||||
multiple=True,
|
||||
help="Search for photos in an album in folder FOLDER. "
|
||||
'If more than one folder, treated as "OR", e.g. find photos in any FOLDER. '
|
||||
"Only searches top level folders (e.g. does not look at subfolders)",
|
||||
),
|
||||
o(
|
||||
"--uuid",
|
||||
metavar="UUID",
|
||||
default=None,
|
||||
multiple=True,
|
||||
help="Search for UUID(s).",
|
||||
help="Search for photos with UUID(s).",
|
||||
),
|
||||
o(
|
||||
"--title",
|
||||
@@ -670,6 +679,7 @@ def query(
|
||||
keyword,
|
||||
person,
|
||||
album,
|
||||
folder,
|
||||
uuid,
|
||||
title,
|
||||
no_title,
|
||||
@@ -728,6 +738,7 @@ def query(
|
||||
keyword,
|
||||
person,
|
||||
album,
|
||||
folder,
|
||||
uuid,
|
||||
edited,
|
||||
external_edit,
|
||||
@@ -781,6 +792,7 @@ def query(
|
||||
keyword=keyword,
|
||||
person=person,
|
||||
album=album,
|
||||
folder=folder,
|
||||
uuid=uuid,
|
||||
title=title,
|
||||
no_title=no_title,
|
||||
@@ -932,6 +944,7 @@ def export(
|
||||
keyword,
|
||||
person,
|
||||
album,
|
||||
folder,
|
||||
uuid,
|
||||
title,
|
||||
no_title,
|
||||
@@ -1051,6 +1064,7 @@ def export(
|
||||
keyword=keyword,
|
||||
person=person,
|
||||
album=album,
|
||||
folder=folder,
|
||||
uuid=uuid,
|
||||
title=title,
|
||||
no_title=no_title,
|
||||
@@ -1270,6 +1284,7 @@ def _query(
|
||||
keyword=None,
|
||||
person=None,
|
||||
album=None,
|
||||
folder=None,
|
||||
uuid=None,
|
||||
title=None,
|
||||
no_title=None,
|
||||
@@ -1316,10 +1331,10 @@ def _query(
|
||||
place=None,
|
||||
no_place=None,
|
||||
):
|
||||
""" run a query against PhotosDB to extract the photos based on user supply criteria """
|
||||
""" used by query and export commands """
|
||||
""" arguments must be passed in same order as query and export """
|
||||
""" if either is modified, need to ensure all three functions are updated """
|
||||
""" run a query against PhotosDB to extract the photos based on user supply criteria
|
||||
used by query and export commands
|
||||
arguments must be passed in same order as query and export
|
||||
if either is modified, need to ensure all three functions are updated """
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=db)
|
||||
photos = photosdb.photos(
|
||||
@@ -1333,6 +1348,21 @@ def _query(
|
||||
to_date=to_date,
|
||||
)
|
||||
|
||||
if folder:
|
||||
# search for photos in an album in folder
|
||||
# finds photos that have albums whose top level folder matches folder
|
||||
photo_list = []
|
||||
for f in folder:
|
||||
photo_list.extend(
|
||||
[
|
||||
p
|
||||
for p in photos
|
||||
if p.album_info
|
||||
and f in [a.folder_names[0] for a in p.album_info if a.folder_names]
|
||||
]
|
||||
)
|
||||
photos = photo_list
|
||||
|
||||
if title:
|
||||
# search title field for text
|
||||
# if more than one, find photos with all title values in title
|
||||
|
||||
@@ -40,3 +40,9 @@ _MOVIE_TYPE = 1
|
||||
# Name of XMP template file
|
||||
_TEMPLATE_DIR = os.path.join(os.path.dirname(__file__), "templates")
|
||||
_XMP_TEMPLATE_NAME = "xmp_sidecar.mako"
|
||||
|
||||
# Constants used for processing folders and albums
|
||||
_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
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
""" version info """
|
||||
|
||||
__version__ = "0.26.0"
|
||||
__version__ = "0.27.4"
|
||||
|
||||
176
osxphotos/albuminfo.py
Normal file
176
osxphotos/albuminfo.py
Normal file
@@ -0,0 +1,176 @@
|
||||
"""
|
||||
AlbumInfo and FolderInfo classes for dealing with albums and folders
|
||||
|
||||
AlbumInfo class
|
||||
Represents a single Album in the Photos library and provides access to the album's attributes
|
||||
PhotosDB.albums() returns a list of AlbumInfo objects
|
||||
|
||||
FolderInfo class
|
||||
Represents a single Folder in the Photos library and provides access to the folders attributes
|
||||
PhotosDB.folders() returns a list of FolderInfo objects
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from ._constants import _PHOTOS_5_ALBUM_KIND, _PHOTOS_5_FOLDER_KIND, _PHOTOS_5_VERSION
|
||||
|
||||
|
||||
class AlbumInfo:
|
||||
"""
|
||||
Info about a specific Album, contains all the details about the album
|
||||
including folders, photos, etc.
|
||||
"""
|
||||
|
||||
def __init__(self, db=None, uuid=None):
|
||||
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
|
||||
|
||||
@property
|
||||
def uuid(self):
|
||||
""" return uuid of album """
|
||||
return self._uuid
|
||||
|
||||
@property
|
||||
def photos(self):
|
||||
""" return list of photos contained in album """
|
||||
try:
|
||||
return self._photos
|
||||
except AttributeError:
|
||||
uuid = self._db._dbalbums_album[self._uuid]
|
||||
self._photos = self._db.photos(uuid=uuid)
|
||||
return self._photos
|
||||
|
||||
@property
|
||||
def folder_names(self):
|
||||
""" return hierarchical list of folders the album is contained in
|
||||
the folder list is in form:
|
||||
["Top level folder", "sub folder 1", "sub folder 2", ...]
|
||||
returns empty list if album is not in any folders """
|
||||
|
||||
if self._db._db_version < _PHOTOS_5_VERSION:
|
||||
logging.warning("Folders not yet implemented for this DB version")
|
||||
return []
|
||||
|
||||
try:
|
||||
return self._folder_names
|
||||
except AttributeError:
|
||||
self._folder_names = self._db._album_folder_hierarchy_list(self._uuid)
|
||||
return self._folder_names
|
||||
|
||||
@property
|
||||
def folder_list(self):
|
||||
""" return hierarchical list of folders the album is contained in
|
||||
as list of FolderInfo objects in form
|
||||
["Top level folder", "sub folder 1", "sub folder 2", ...]
|
||||
returns empty list if album is not in any folders """
|
||||
|
||||
if self._db._db_version < _PHOTOS_5_VERSION:
|
||||
logging.warning("Folders not yet implemented for this DB version")
|
||||
return []
|
||||
|
||||
try:
|
||||
return self._folders
|
||||
except AttributeError:
|
||||
self._folders = self._db._album_folder_hierarchy_folderinfo(self._uuid)
|
||||
return self._folders
|
||||
|
||||
@property
|
||||
def parent(self):
|
||||
""" returns FolderInfo object for parent folder or None if no parent (e.g. top-level album) """
|
||||
if self._db._db_version < _PHOTOS_5_VERSION:
|
||||
logging.warning("Folders not yet implemented for this DB version")
|
||||
return None
|
||||
|
||||
try:
|
||||
return self._parent
|
||||
except AttributeError:
|
||||
parent_pk = self._db._dbalbum_details[self._uuid]["parentfolder"]
|
||||
self._parent = (
|
||||
FolderInfo(db=self._db, uuid=self._db._dbalbums_pk[parent_pk])
|
||||
if parent_pk != self._db._folder_root_pk
|
||||
else None
|
||||
)
|
||||
return self._parent
|
||||
|
||||
def __len__(self):
|
||||
""" return number of photos contained in album """
|
||||
return len(self.photos)
|
||||
|
||||
|
||||
class FolderInfo:
|
||||
"""
|
||||
Info about a specific folder, contains all the details about the folder
|
||||
including folders, albums, etc
|
||||
"""
|
||||
|
||||
def __init__(self, db=None, uuid=None):
|
||||
self._uuid = uuid
|
||||
self._db = db
|
||||
self._pk = self._db._dbalbum_details[uuid]["pk"]
|
||||
self._title = self._db._dbalbum_details[uuid]["title"]
|
||||
|
||||
@property
|
||||
def title(self):
|
||||
""" return title / name of folder"""
|
||||
return self._title
|
||||
|
||||
@property
|
||||
def uuid(self):
|
||||
""" return uuid of folder """
|
||||
return self._uuid
|
||||
|
||||
@property
|
||||
def album_info(self):
|
||||
""" return list of albums (as AlbumInfo objects) contained in the folder """
|
||||
try:
|
||||
return self._albums
|
||||
except AttributeError:
|
||||
albums = [
|
||||
AlbumInfo(db=self._db, uuid=album)
|
||||
for album, detail in self._db._dbalbum_details.items()
|
||||
if not detail["intrash"]
|
||||
and detail["kind"] == _PHOTOS_5_ALBUM_KIND
|
||||
and detail["parentfolder"] == self._pk
|
||||
]
|
||||
self._albums = albums
|
||||
return self._albums
|
||||
|
||||
@property
|
||||
def parent(self):
|
||||
""" returns FolderInfo object for parent or None if no parent (e.g. top-level folder) """
|
||||
try:
|
||||
return self._parent
|
||||
except AttributeError:
|
||||
parent_pk = self._db._dbalbum_details[self._uuid]["parentfolder"]
|
||||
self._parent = (
|
||||
FolderInfo(db=self._db, uuid=self._db._dbalbums_pk[parent_pk])
|
||||
if parent_pk != self._db._folder_root_pk
|
||||
else None
|
||||
)
|
||||
return self._parent
|
||||
|
||||
@property
|
||||
def subfolders(self):
|
||||
""" return list of folders (as FolderInfo objects) contained in the folder """
|
||||
try:
|
||||
return self._folders
|
||||
except AttributeError:
|
||||
folders = [
|
||||
FolderInfo(db=self._db, uuid=album)
|
||||
for album, detail in self._db._dbalbum_details.items()
|
||||
if not detail["intrash"]
|
||||
and detail["kind"] == _PHOTOS_5_FOLDER_KIND
|
||||
and detail["parentfolder"] == self._pk
|
||||
]
|
||||
self._folders = folders
|
||||
return self._folders
|
||||
|
||||
def __len__(self):
|
||||
""" returns count of folders + albums contained in the folder """
|
||||
return len(self.subfolders) + len(self.album_info)
|
||||
@@ -28,6 +28,7 @@ from ._constants import (
|
||||
)
|
||||
from .exiftool import ExifTool
|
||||
from .placeinfo import PlaceInfo4, PlaceInfo5
|
||||
from .albuminfo import AlbumInfo
|
||||
from .utils import (
|
||||
_copy_file,
|
||||
_export_photo_uuid_applescript,
|
||||
@@ -253,7 +254,18 @@ class PhotoInfo:
|
||||
""" list of albums picture is contained in """
|
||||
albums = []
|
||||
for album in self._info["albums"]:
|
||||
albums.append(self._db._dbalbum_details[album]["title"])
|
||||
if not self._db._dbalbum_details[album]["intrash"]:
|
||||
albums.append(self._db._dbalbum_details[album]["title"])
|
||||
return albums
|
||||
|
||||
@property
|
||||
def album_info(self):
|
||||
""" list of AlbumInfo objects representing albums the photos is contained in """
|
||||
albums = []
|
||||
for album in self._info["albums"]:
|
||||
if not self._db._dbalbum_details[album]["intrash"]:
|
||||
albums.append(AlbumInfo(db=self._db, uuid=album))
|
||||
|
||||
return albums
|
||||
|
||||
@property
|
||||
|
||||
@@ -23,8 +23,13 @@ from ._constants import (
|
||||
_TESTED_DB_VERSIONS,
|
||||
_TESTED_OS_VERSIONS,
|
||||
_UNKNOWN_PERSON,
|
||||
_PHOTOS_5_ROOT_FOLDER_KIND,
|
||||
_PHOTOS_5_FOLDER_KIND,
|
||||
_PHOTOS_5_ALBUM_KIND,
|
||||
_PHOTOS_5_SHARED_ALBUM_KIND,
|
||||
)
|
||||
from ._version import __version__
|
||||
from .albuminfo import AlbumInfo, FolderInfo
|
||||
from .photoinfo import PhotoInfo
|
||||
from .utils import (
|
||||
_check_file_exists,
|
||||
@@ -151,6 +156,20 @@ class PhotosDB:
|
||||
# Used to find path of photos imported but not copied to the Photos library
|
||||
self._dbvolumes = {}
|
||||
|
||||
# Dict with information about parent folders for folders and albums
|
||||
# key is album or folder UUID and value is list of UUIDs of parent folder
|
||||
# e.g. {'0C514A98-7B77-4E4F-801B-364B7B65EAFA': ['92D68107-B6C7-453B-96D2-97B0F26D5B8B'],}
|
||||
self._dbalbum_parent_folders = {}
|
||||
|
||||
# Dict with information about folder hierarchy for each album / folder
|
||||
# key is uuid of album / folder, value is dict with uuid of descendant folder / album
|
||||
# structure is recursive as a descendant may itself have descendants
|
||||
# e.g. {'AA4145F5-098C-496E-9197-B7584958FF9B': {'99D24D3E-59E7-465F-B386-A48A94B00BC1': {'F2246D82-1A12-4994-9654-3DC6FE38A7A8': None}}, }
|
||||
self._dbalbum_folders = {}
|
||||
|
||||
# Will hold the primary key of root folder
|
||||
self._folder_root_pk = None
|
||||
|
||||
if _debug():
|
||||
logging.debug(f"dbfile = {dbfile}")
|
||||
|
||||
@@ -270,7 +289,7 @@ class PhotosDB:
|
||||
k
|
||||
for k in self._dbalbums_album.keys()
|
||||
if self._dbalbum_details[k]["cloudownerhashedpersonid"] is None
|
||||
and self._dbalbum_details[k]["intrash"] == 0
|
||||
and not self._dbalbum_details[k]["intrash"]
|
||||
]
|
||||
for k in album_keys:
|
||||
title = self._dbalbum_details[k]["title"]
|
||||
@@ -321,24 +340,86 @@ class PhotosDB:
|
||||
return list(persons)
|
||||
|
||||
@property
|
||||
def album_names(self):
|
||||
def folder_info(self):
|
||||
""" return list FolderInfo objects representing top-level folders in the photos database """
|
||||
if self._db_version < _PHOTOS_5_VERSION:
|
||||
logging.warning("Folders not yet implemented for this DB version")
|
||||
return []
|
||||
|
||||
folders = [
|
||||
FolderInfo(db=self, uuid=album)
|
||||
for album, detail in self._dbalbum_details.items()
|
||||
if not detail["intrash"]
|
||||
and detail["kind"] == _PHOTOS_5_FOLDER_KIND
|
||||
and detail["parentfolder"] == self._folder_root_pk
|
||||
]
|
||||
return folders
|
||||
|
||||
@property
|
||||
def folders(self):
|
||||
""" return list of top-level folder names in the photos database """
|
||||
if self._db_version < _PHOTOS_5_VERSION:
|
||||
logging.warning("Folders not yet implemented for this DB version")
|
||||
return []
|
||||
|
||||
folder_names = [
|
||||
detail["title"]
|
||||
for detail in self._dbalbum_details.values()
|
||||
if not detail["intrash"]
|
||||
and detail["kind"] == _PHOTOS_5_FOLDER_KIND
|
||||
and detail["parentfolder"] == self._folder_root_pk
|
||||
]
|
||||
return folder_names
|
||||
|
||||
@property
|
||||
def album_info(self):
|
||||
""" return list of AlbumInfo objects for each album in the photos database """
|
||||
|
||||
albums = [
|
||||
AlbumInfo(db=self, uuid=album)
|
||||
for album in self._dbalbums_album.keys()
|
||||
if self._dbalbum_details[album]["cloudownerhashedpersonid"] is None
|
||||
and not self._dbalbum_details[album]["intrash"]
|
||||
]
|
||||
return albums
|
||||
|
||||
@property
|
||||
def album_info_shared(self):
|
||||
""" return list of AlbumInfo objects for each shared album in the photos database
|
||||
only valid for Photos 5; on Photos <= 4, prints warning and returns empty list """
|
||||
# if _dbalbum_details[key]["cloudownerhashedpersonid"] is not None, then it's a shared album
|
||||
|
||||
if self._db_version < _PHOTOS_5_VERSION:
|
||||
logging.warning(
|
||||
f"albums_shared not implemented for Photos versions < {_PHOTOS_5_VERSION}"
|
||||
)
|
||||
return []
|
||||
|
||||
albums_shared = [
|
||||
AlbumInfo(db=self, uuid=album)
|
||||
for album in self._dbalbums_album.keys()
|
||||
if self._dbalbum_details[album]["cloudownerhashedpersonid"] is not None
|
||||
and not self._dbalbum_details[album]["intrash"]
|
||||
]
|
||||
return albums_shared
|
||||
|
||||
@property
|
||||
def albums(self):
|
||||
""" return list of albums found in photos database """
|
||||
|
||||
# Could be more than one album with same name
|
||||
# Right now, they are treated as same album and photos are combined from albums with same name
|
||||
|
||||
albums = set()
|
||||
album_keys = [
|
||||
k
|
||||
for k in self._dbalbums_album.keys()
|
||||
if self._dbalbum_details[k]["cloudownerhashedpersonid"] is None
|
||||
]
|
||||
for album in album_keys:
|
||||
albums.add(self._dbalbum_details[album]["title"])
|
||||
albums = {
|
||||
self._dbalbum_details[album]["title"]
|
||||
for album in self._dbalbums_album.keys()
|
||||
if self._dbalbum_details[album]["cloudownerhashedpersonid"] is None
|
||||
and not self._dbalbum_details[album]["intrash"]
|
||||
}
|
||||
return list(albums)
|
||||
|
||||
@property
|
||||
def album_names_shared(self):
|
||||
def albums_shared(self):
|
||||
""" return list of shared albums found in photos database
|
||||
only valid for Photos 5; on Photos <= 4, prints warning and returns empty list """
|
||||
|
||||
@@ -349,18 +430,16 @@ class PhotosDB:
|
||||
|
||||
if self._db_version < _PHOTOS_5_VERSION:
|
||||
logging.warning(
|
||||
f"albums_shared not implemented for Photos versions < {_PHOTOS_5_VERSION}"
|
||||
f"album_names_shared not implemented for Photos versions < {_PHOTOS_5_VERSION}"
|
||||
)
|
||||
return []
|
||||
|
||||
albums = set()
|
||||
album_keys = [
|
||||
k
|
||||
for k in self._dbalbums_album.keys()
|
||||
if self._dbalbum_details[k]["cloudownerhashedpersonid"] is not None
|
||||
]
|
||||
for album in album_keys:
|
||||
albums.add(self._dbalbum_details[album]["title"])
|
||||
albums = {
|
||||
self._dbalbum_details[album]["title"]
|
||||
for album in self._dbalbums_album.keys()
|
||||
if self._dbalbum_details[album]["cloudownerhashedpersonid"] is not None
|
||||
and not self._dbalbum_details[album]["intrash"]
|
||||
}
|
||||
return list(albums)
|
||||
|
||||
@property
|
||||
@@ -483,7 +562,7 @@ class PhotosDB:
|
||||
"title": album[1],
|
||||
"cloudlibrarystate": album[2],
|
||||
"cloudidentifier": album[3],
|
||||
"intrash": album[4],
|
||||
"intrash": False if album[4] == 0 else True,
|
||||
"cloudlocalstate": None, # Photos 5
|
||||
"cloudownerfirstname": None, # Photos 5
|
||||
"cloudownderlastname": None, # Photos 5
|
||||
@@ -1029,6 +1108,7 @@ class PhotosDB:
|
||||
)
|
||||
for album in c:
|
||||
self._dbalbum_details[album[0]] = {
|
||||
"_uuid": album[0],
|
||||
"title": album[1],
|
||||
"cloudlocalstate": album[2],
|
||||
"cloudownerfirstname": album[3],
|
||||
@@ -1039,7 +1119,7 @@ class PhotosDB:
|
||||
"kind": album[6],
|
||||
"parentfolder": album[7],
|
||||
"pk": album[8],
|
||||
"intrash": album[9],
|
||||
"intrash": False if album[9] == 0 else True,
|
||||
}
|
||||
|
||||
# add cross-reference by pk to uuid
|
||||
@@ -1047,6 +1127,42 @@ class PhotosDB:
|
||||
# in Photos >= 5, folders are special albums
|
||||
self._dbalbums_pk[album[8]] = album[0]
|
||||
|
||||
# get pk of root folder
|
||||
root_uuid = [
|
||||
album
|
||||
for album, details in self._dbalbum_details.items()
|
||||
if details["kind"] == _PHOTOS_5_ROOT_FOLDER_KIND
|
||||
]
|
||||
if len(root_uuid) != 1:
|
||||
raise ValueError(f"Error finding root folder: {root_uuid}")
|
||||
else:
|
||||
self._folder_root_pk = self._dbalbum_details[root_uuid[0]]["pk"]
|
||||
|
||||
# build _dbalbum_folders which is in form uuid: [list of parent uuids]
|
||||
for album, details in self._dbalbum_details.items():
|
||||
pk_parent = details["parentfolder"]
|
||||
if pk_parent is None:
|
||||
continue
|
||||
|
||||
try:
|
||||
parent = self._dbalbums_pk[pk_parent]
|
||||
except KeyError:
|
||||
raise ValueError(f"Did not find uuid for album {album} pk {pk_parent}")
|
||||
|
||||
try:
|
||||
self._dbalbum_parent_folders[album].append(parent)
|
||||
except KeyError:
|
||||
self._dbalbum_parent_folders[album] = [parent]
|
||||
|
||||
for album, details in self._dbalbum_details.items():
|
||||
# if details["kind"] in [_PHOTOS_5_ALBUM_KIND, _PHOTOS_5_FOLDER_KIND]:
|
||||
if details["kind"] == _PHOTOS_5_ALBUM_KIND:
|
||||
folder_hierarchy = self._build_album_folder_hierarchy(album)
|
||||
self._dbalbum_folders[album] = folder_hierarchy
|
||||
elif details["kind"] == _PHOTOS_5_SHARED_ALBUM_KIND:
|
||||
# shared albums can be in folders
|
||||
self._dbalbum_folders[album] = []
|
||||
|
||||
if _debug():
|
||||
logging.debug(f"Finished walking through albums")
|
||||
logging.debug(pformat(self._dbalbums_album))
|
||||
@@ -1482,6 +1598,15 @@ class PhotosDB:
|
||||
logging.debug("Album titles (_dbalbum_titles):")
|
||||
logging.debug(pformat(self._dbalbum_titles))
|
||||
|
||||
logging.debug("Album folders (_dbalbum_folders):")
|
||||
logging.debug(pformat(self._dbalbum_folders))
|
||||
|
||||
logging.debug("Album parent folders (_dbalbum_parent_folders):")
|
||||
logging.debug(pformat(self._dbalbum_parent_folders))
|
||||
|
||||
logging.debug("Albums pk (_dbalbums_pk):")
|
||||
logging.debug(pformat(self._dbalbums_pk))
|
||||
|
||||
logging.debug("Volumes (_dbvolumes):")
|
||||
logging.debug(pformat(self._dbvolumes))
|
||||
|
||||
@@ -1491,6 +1616,98 @@ class PhotosDB:
|
||||
logging.debug("Burst Photos (dbphotos_burst:")
|
||||
logging.debug(pformat(self._dbphotos_burst))
|
||||
|
||||
def _build_album_folder_hierarchy(self, uuid, folders=None):
|
||||
""" recursively build folder/album hierarchy
|
||||
uuid: uuid of the album/folder being processed
|
||||
folders: dict holding the folder hierarchy """
|
||||
|
||||
if self._db_version < _PHOTOS_5_VERSION:
|
||||
raise AttributeError("Not yet implemented for this DB version")
|
||||
|
||||
# get parent uuid
|
||||
parent = self._dbalbum_details[uuid]["parentfolder"]
|
||||
|
||||
if parent is not None:
|
||||
parent_uuid = self._dbalbums_pk[parent]
|
||||
else:
|
||||
# folder with no parent (e.g. shared iCloud folders)
|
||||
return folders
|
||||
|
||||
if self._db_version >= _PHOTOS_5_VERSION and parent == self._folder_root_pk:
|
||||
# at the top of the folder hierarchy, we're done
|
||||
return folders
|
||||
|
||||
# recurse to keep building
|
||||
folders = {parent_uuid: folders}
|
||||
folders = self._build_album_folder_hierarchy(parent_uuid, folders=folders)
|
||||
return folders
|
||||
|
||||
def _album_folder_hierarchy_list(self, album_uuid):
|
||||
""" return hierarchical list of folder names album_uuid is contained in
|
||||
the folder list is in form:
|
||||
["Top level folder", "sub folder 1", "sub folder 2"]
|
||||
returns empty list of album is not in any folders """
|
||||
# title = photosdb._dbalbum_details[album_uuid]["title"]
|
||||
folders = self._dbalbum_folders[album_uuid]
|
||||
|
||||
def _recurse_folder_hierarchy(folders, hierarchy=[]):
|
||||
""" recursively walk the folders dict to build list of folder hierarchy """
|
||||
|
||||
if not folders:
|
||||
# empty folder dict (album has no folder hierarchy)
|
||||
return []
|
||||
|
||||
if len(folders) != 1:
|
||||
raise ValueError("Expected only a single key in folders dict")
|
||||
|
||||
folder_uuid = list(folders)[0] # first and only key of dict
|
||||
parent_title = self._dbalbum_details[folder_uuid]["title"]
|
||||
hierarchy.append(parent_title)
|
||||
|
||||
folders = folders[folder_uuid]
|
||||
if folders:
|
||||
# still have elements left to recurse
|
||||
hierarchy = _recurse_folder_hierarchy(folders, hierarchy=hierarchy)
|
||||
return hierarchy
|
||||
|
||||
# no elements left to recurse
|
||||
return hierarchy
|
||||
|
||||
hierarchy = _recurse_folder_hierarchy(folders)
|
||||
return hierarchy
|
||||
|
||||
def _album_folder_hierarchy_folderinfo(self, album_uuid):
|
||||
""" return hierarchical list of FolderInfo objects album_uuid is contained in
|
||||
["Top level folder", "sub folder 1", "sub folder 2"]
|
||||
returns empty list of album is not in any folders """
|
||||
# title = photosdb._dbalbum_details[album_uuid]["title"]
|
||||
folders = self._dbalbum_folders[album_uuid]
|
||||
|
||||
def _recurse_folder_hierarchy(folders, hierarchy=[]):
|
||||
""" recursively walk the folders dict to build list of folder hierarchy """
|
||||
|
||||
if not folders:
|
||||
# empty folder dict (album has no folder hierarchy)
|
||||
return []
|
||||
|
||||
if len(folders) != 1:
|
||||
raise ValueError("Expected only a single key in folders dict")
|
||||
|
||||
folder_uuid = list(folders)[0] # first and only key of dict
|
||||
hierarchy.append(FolderInfo(db=self, uuid=folder_uuid))
|
||||
|
||||
folders = folders[folder_uuid]
|
||||
if folders:
|
||||
# still have elements left to recurse
|
||||
hierarchy = _recurse_folder_hierarchy(folders, hierarchy=hierarchy)
|
||||
return hierarchy
|
||||
|
||||
# no elements left to recurse
|
||||
return hierarchy
|
||||
|
||||
hierarchy = _recurse_folder_hierarchy(folders)
|
||||
return hierarchy
|
||||
|
||||
def _process_database5X(self):
|
||||
""" ALPHA: TESTING using SimpleNamespace to clean up code for info, DO NOT CALL THIS METHOD """
|
||||
""" Needs to be updated for changes in process_database5 due to adding PlaceInfo """
|
||||
@@ -2089,37 +2306,45 @@ class PhotosDB:
|
||||
photos_sets.append(set(self._dbphotos.keys()))
|
||||
else:
|
||||
if albums:
|
||||
album_set = set()
|
||||
for album in albums:
|
||||
# TODO: can have >1 album with same name. This globs them together.
|
||||
# Need a way to select which album?
|
||||
if album in self._dbalbum_titles:
|
||||
album_set = set()
|
||||
title_set = set()
|
||||
for album_id in self._dbalbum_titles[album]:
|
||||
album_set.update(self._dbalbums_album[album_id])
|
||||
photos_sets.append(album_set)
|
||||
title_set.update(self._dbalbums_album[album_id])
|
||||
album_set.update(title_set)
|
||||
else:
|
||||
logging.debug(f"Could not find album '{album}' in database")
|
||||
photos_sets.append(album_set)
|
||||
|
||||
if uuid:
|
||||
uuid_set = set()
|
||||
for u in uuid:
|
||||
if u in self._dbphotos:
|
||||
photos_sets.append(set([u]))
|
||||
uuid_set.update([u])
|
||||
else:
|
||||
logging.debug(f"Could not find uuid '{u}' in database")
|
||||
photos_sets.append(uuid_set)
|
||||
|
||||
if keywords:
|
||||
keyword_set = set()
|
||||
for keyword in keywords:
|
||||
if keyword in self._dbkeywords_keyword:
|
||||
photos_sets.append(set(self._dbkeywords_keyword[keyword]))
|
||||
keyword_set.update(self._dbkeywords_keyword[keyword])
|
||||
else:
|
||||
logging.debug(f"Could not find keyword '{keyword}' in database")
|
||||
photos_sets.append(keyword_set)
|
||||
|
||||
if persons:
|
||||
person_set = set()
|
||||
for person in persons:
|
||||
if person in self._dbfaces_person:
|
||||
photos_sets.append(set(self._dbfaces_person[person]))
|
||||
person_set.update(self._dbfaces_person[person])
|
||||
else:
|
||||
logging.debug(f"Could not find person '{person}' in database")
|
||||
photos_sets.append(person_set)
|
||||
|
||||
if from_date or to_date:
|
||||
dsel = self._dbphotos
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
# This code isn't elegant but it seems to work well. PRs gladly accepted.
|
||||
|
||||
import datetime
|
||||
import os
|
||||
import pathlib
|
||||
import re
|
||||
from typing import Tuple, List # pylint: disable=syntax-error
|
||||
@@ -55,6 +56,7 @@ TEMPLATE_SUBSTITUTIONS = {
|
||||
# Permitted multi-value substitutions (each of these returns None or 1 or more values)
|
||||
TEMPLATE_SUBSTITUTIONS_MULTI_VALUED = {
|
||||
"{album}": "Album(s) photo is contained in",
|
||||
"{folder_album}": "Folder path + album photo is contained in. e.g. 'Folder/Subfolder/Album' or just 'Album' if no enclosing folder",
|
||||
"{keyword}": "Keyword(s) assigned to photo",
|
||||
"{person}": "Person(s) / face(s) in a photo",
|
||||
}
|
||||
@@ -321,6 +323,19 @@ def render_filepath_template(template, photo, none_str="_"):
|
||||
values = photo.persons
|
||||
# remove any _UNKNOWN_PERSON values
|
||||
values = [val for val in values if val != _UNKNOWN_PERSON]
|
||||
elif field == "folder_album":
|
||||
values = []
|
||||
# photos must be in an album to be in a folder
|
||||
for album in photo.album_info:
|
||||
if album.folder_names:
|
||||
# album in folder
|
||||
folder = os.path.sep.join(album.folder_names)
|
||||
folder += os.path.sep + album.title
|
||||
values.append(folder)
|
||||
else:
|
||||
# album not in folder
|
||||
values.append(album.title)
|
||||
|
||||
else:
|
||||
raise ValueError(f"Unhandleded template value: {field}")
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -7,7 +7,7 @@
|
||||
<key>hostuuid</key>
|
||||
<string>9575E48B-8D5F-5654-ABAC-4431B1167324</string>
|
||||
<key>pid</key>
|
||||
<integer>1526</integer>
|
||||
<integer>1486</integer>
|
||||
<key>processname</key>
|
||||
<string>photolibraryd</string>
|
||||
<key>uid</key>
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -3,24 +3,24 @@
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>BackgroundHighlightCollection</key>
|
||||
<date>2020-04-04T17:34:35Z</date>
|
||||
<date>2020-04-12T19:50:22Z</date>
|
||||
<key>BackgroundHighlightEnrichment</key>
|
||||
<date>2020-04-04T17:34:34Z</date>
|
||||
<date>2020-04-12T19:50:22Z</date>
|
||||
<key>BackgroundJobAssetRevGeocode</key>
|
||||
<date>2020-04-04T20:16:33Z</date>
|
||||
<date>2020-04-12T21:11:06Z</date>
|
||||
<key>BackgroundJobSearch</key>
|
||||
<date>2020-04-04T17:34:35Z</date>
|
||||
<date>2020-04-12T19:50:22Z</date>
|
||||
<key>BackgroundPeopleSuggestion</key>
|
||||
<date>2020-04-04T17:34:34Z</date>
|
||||
<date>2020-04-12T19:50:22Z</date>
|
||||
<key>BackgroundUserBehaviorProcessor</key>
|
||||
<date>2020-04-04T13:56:56Z</date>
|
||||
<date>2020-04-12T15:18:31Z</date>
|
||||
<key>PhotoAnalysisGraphLastBackgroundGraphConsistencyUpdateJobDateKey</key>
|
||||
<date>2020-04-04T20:16:40Z</date>
|
||||
<date>2020-04-12T21:11:07Z</date>
|
||||
<key>PhotoAnalysisGraphLastBackgroundGraphRebuildJobDate</key>
|
||||
<date>2020-04-04T13:56:49Z</date>
|
||||
<date>2020-04-12T15:18:31Z</date>
|
||||
<key>PhotoAnalysisGraphLastBackgroundMemoryGenerationJobDate</key>
|
||||
<date>2020-04-04T20:16:33Z</date>
|
||||
<date>2020-04-12T19:50:23Z</date>
|
||||
<key>SiriPortraitDonation</key>
|
||||
<date>2020-04-04T13:56:56Z</date>
|
||||
<date>2020-04-12T15:18:31Z</date>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -3,8 +3,8 @@
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>FaceIDModelLastGenerationKey</key>
|
||||
<date>2020-04-04T13:56:59Z</date>
|
||||
<date>2020-04-12T15:18:32Z</date>
|
||||
<key>LastContactClassificationKey</key>
|
||||
<date>2020-04-04T13:57:02Z</date>
|
||||
<date>2020-04-12T15:18:34Z</date>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -3,7 +3,7 @@
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>coalesceDate</key>
|
||||
<date>2019-12-08T18:06:37Z</date>
|
||||
<date>2020-04-11T16:34:16Z</date>
|
||||
<key>coalescePayloadVersion</key>
|
||||
<integer>1</integer>
|
||||
<key>currentPayloadVersion</key>
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -7,7 +7,7 @@
|
||||
<key>hostuuid</key>
|
||||
<string>9575E48B-8D5F-5654-ABAC-4431B1167324</string>
|
||||
<key>pid</key>
|
||||
<integer>1440</integer>
|
||||
<integer>3212</integer>
|
||||
<key>processname</key>
|
||||
<string>photolibraryd</string>
|
||||
<key>uid</key>
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -6,7 +6,9 @@
|
||||
<array/>
|
||||
<key>ExpandedSidebarItemIdentifiers</key>
|
||||
<array>
|
||||
<string>88A5F8B8-5B9A-43C7-BB85-3952B81580EB/L0/020</string>
|
||||
<string>92D68107-B6C7-453B-96D2-97B0F26D5B8B/L0/020</string>
|
||||
<string>29EF7A97-7E76-4D5F-A5E0-CC0A93E8524C/L0/020</string>
|
||||
</array>
|
||||
<key>Photos</key>
|
||||
<dict>
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -3,24 +3,24 @@
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>BackgroundHighlightCollection</key>
|
||||
<date>2020-04-04T17:34:35Z</date>
|
||||
<date>2020-04-11T20:00:25Z</date>
|
||||
<key>BackgroundHighlightEnrichment</key>
|
||||
<date>2020-04-04T17:34:34Z</date>
|
||||
<date>2020-04-11T20:00:25Z</date>
|
||||
<key>BackgroundJobAssetRevGeocode</key>
|
||||
<date>2020-04-04T20:16:33Z</date>
|
||||
<date>2020-04-11T20:00:25Z</date>
|
||||
<key>BackgroundJobSearch</key>
|
||||
<date>2020-04-04T17:34:35Z</date>
|
||||
<date>2020-04-11T20:00:25Z</date>
|
||||
<key>BackgroundPeopleSuggestion</key>
|
||||
<date>2020-04-04T17:34:34Z</date>
|
||||
<date>2020-04-11T20:00:24Z</date>
|
||||
<key>BackgroundUserBehaviorProcessor</key>
|
||||
<date>2020-04-04T13:56:56Z</date>
|
||||
<date>2020-04-11T20:00:25Z</date>
|
||||
<key>PhotoAnalysisGraphLastBackgroundGraphConsistencyUpdateJobDateKey</key>
|
||||
<date>2020-04-04T20:16:40Z</date>
|
||||
<date>2020-04-11T20:10:27Z</date>
|
||||
<key>PhotoAnalysisGraphLastBackgroundGraphRebuildJobDate</key>
|
||||
<date>2020-04-04T13:56:49Z</date>
|
||||
<date>2020-04-11T20:00:24Z</date>
|
||||
<key>PhotoAnalysisGraphLastBackgroundMemoryGenerationJobDate</key>
|
||||
<date>2020-04-04T20:16:33Z</date>
|
||||
<date>2020-04-11T20:00:25Z</date>
|
||||
<key>SiriPortraitDonation</key>
|
||||
<date>2020-04-04T13:56:56Z</date>
|
||||
<date>2020-04-11T20:00:25Z</date>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -3,8 +3,8 @@
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>FaceIDModelLastGenerationKey</key>
|
||||
<date>2020-04-04T13:56:59Z</date>
|
||||
<date>2020-04-11T20:00:25Z</date>
|
||||
<key>LastContactClassificationKey</key>
|
||||
<date>2020-04-04T13:57:02Z</date>
|
||||
<date>2020-04-11T20:00:26Z</date>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -3,7 +3,7 @@
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>coalesceDate</key>
|
||||
<date>2019-12-08T18:06:37Z</date>
|
||||
<date>2020-04-11T19:26:12Z</date>
|
||||
<key>coalescePayloadVersion</key>
|
||||
<integer>1</integer>
|
||||
<key>currentPayloadVersion</key>
|
||||
|
||||
Binary file not shown.
@@ -3,7 +3,7 @@
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>coalesceDate</key>
|
||||
<date>2019-12-08T18:06:37Z</date>
|
||||
<date>2020-04-11T19:26:12Z</date>
|
||||
<key>coalescePayloadVersion</key>
|
||||
<integer>1</integer>
|
||||
<key>currentPayloadVersion</key>
|
||||
|
||||
Binary file not shown.
@@ -77,8 +77,8 @@ def test_album_names():
|
||||
import collections
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
assert "Pumpkin Farm" in photosdb.album_names
|
||||
assert collections.Counter(ALBUMS) == collections.Counter(photosdb.album_names)
|
||||
assert "Pumpkin Farm" in photosdb.albums
|
||||
assert collections.Counter(ALBUMS) == collections.Counter(photosdb.albums)
|
||||
|
||||
|
||||
def test_keywords_dict():
|
||||
|
||||
239
tests/test_albums_folders_catalina_10_15_4.py
Normal file
239
tests/test_albums_folders_catalina_10_15_4.py
Normal file
@@ -0,0 +1,239 @@
|
||||
import pytest
|
||||
|
||||
from osxphotos._constants import _UNKNOWN_PERSON
|
||||
|
||||
PHOTOS_DB = "./tests/Test-10.15.4.photoslibrary/database/photos.db"
|
||||
|
||||
TOP_LEVEL_FOLDERS = ["Folder1"]
|
||||
|
||||
TOP_LEVEL_CHILDREN = ["SubFolder1", "SubFolder2"]
|
||||
|
||||
FOLDER_ALBUM_DICT = {"Folder1": [], "SubFolder1": [], "SubFolder2": ["AlbumInFolder"]}
|
||||
|
||||
ALBUM_NAMES = ["Pumpkin Farm", "AlbumInFolder", "Test Album", "Test Album"]
|
||||
|
||||
ALBUM_PARENT_DICT = {
|
||||
"Pumpkin Farm": None,
|
||||
"AlbumInFolder": "SubFolder2",
|
||||
"Test Album": None,
|
||||
}
|
||||
|
||||
ALBUM_FOLDER_NAMES_DICT = {
|
||||
"Pumpkin Farm": [],
|
||||
"AlbumInFolder": ["Folder1", "SubFolder2"],
|
||||
"Test Album": [],
|
||||
}
|
||||
|
||||
ALBUM_LEN_DICT = {"Pumpkin Farm": 3, "AlbumInFolder": 2, "Test Album": 1}
|
||||
|
||||
ALBUM_PHOTO_UUID_DICT = {
|
||||
"Pumpkin Farm": [
|
||||
"F12384F6-CD17-4151-ACBA-AE0E3688539E",
|
||||
"D79B8D77-BFFC-460B-9312-034F2877D35B",
|
||||
"1EB2B765-0765-43BA-A90C-0D0580E6172C",
|
||||
],
|
||||
"Test Album": [
|
||||
"F12384F6-CD17-4151-ACBA-AE0E3688539E",
|
||||
"D79B8D77-BFFC-460B-9312-034F2877D35B",
|
||||
],
|
||||
"AlbumInFolder": [
|
||||
"3DD2C897-F19E-4CA6-8C22-B027D5A71907",
|
||||
"E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51",
|
||||
],
|
||||
}
|
||||
|
||||
UUID_DICT = {"two_albums": "F12384F6-CD17-4151-ACBA-AE0E3688539E"}
|
||||
|
||||
|
||||
def test_folders_1():
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
|
||||
# top level folders
|
||||
folders = photosdb.folder_info
|
||||
assert len(folders) == 1
|
||||
|
||||
# check folder names
|
||||
folder_names = [f.title for f in folders]
|
||||
assert sorted(folder_names) == sorted(TOP_LEVEL_FOLDERS)
|
||||
|
||||
|
||||
def test_folder_names():
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
|
||||
# check folder names
|
||||
folder_names = photosdb.folders
|
||||
assert sorted(folder_names) == sorted(TOP_LEVEL_FOLDERS)
|
||||
|
||||
|
||||
def test_folders_len():
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
|
||||
# top level folders
|
||||
folders = photosdb.folder_info
|
||||
assert len(folders[0]) == len(TOP_LEVEL_CHILDREN)
|
||||
|
||||
|
||||
def test_folders_children():
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
|
||||
# top level folders
|
||||
folders = photosdb.folder_info
|
||||
|
||||
# children of top level folder
|
||||
children = folders[0].subfolders
|
||||
children_names = [f.title for f in children]
|
||||
assert sorted(children_names) == sorted(TOP_LEVEL_CHILDREN)
|
||||
|
||||
for child in folders[0].subfolders:
|
||||
# check valid children FolderInfo
|
||||
assert child.parent
|
||||
assert child.parent.uuid == folders[0].uuid
|
||||
|
||||
# check folder names
|
||||
folder_names = [f.title for f in folders]
|
||||
assert sorted(folder_names) == sorted(TOP_LEVEL_FOLDERS)
|
||||
|
||||
|
||||
def test_folders_parent():
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
|
||||
# top level folders
|
||||
folders = photosdb.folder_info
|
||||
|
||||
# parent of top level folder should be none
|
||||
for folder in folders:
|
||||
assert folder.parent is None
|
||||
for child in folder.subfolders:
|
||||
# children's parent uuid should match folder uuid
|
||||
assert child.parent
|
||||
assert child.parent.uuid == folder.uuid
|
||||
|
||||
|
||||
def test_folders_albums():
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
|
||||
# top level folders
|
||||
folders = photosdb.folder_info
|
||||
|
||||
for folder in folders:
|
||||
name = folder.title
|
||||
albums = [a.title for a in folder.album_info]
|
||||
assert sorted(albums) == sorted(FOLDER_ALBUM_DICT[name])
|
||||
for child in folder.subfolders:
|
||||
name = child.title
|
||||
albums = [a.title for a in child.album_info]
|
||||
assert sorted(albums) == sorted(FOLDER_ALBUM_DICT[name])
|
||||
|
||||
|
||||
########## Test AlbumInfo ##########
|
||||
|
||||
|
||||
def test_albums_1():
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
|
||||
albums = photosdb.album_info
|
||||
assert len(albums) == 4
|
||||
|
||||
# check names
|
||||
album_names = [a.title for a in albums]
|
||||
assert sorted(album_names) == sorted(ALBUM_NAMES)
|
||||
|
||||
|
||||
def test_albums_parent():
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
|
||||
albums = photosdb.album_info
|
||||
|
||||
for album in albums:
|
||||
parent = album.parent.title if album.parent else None
|
||||
assert parent == ALBUM_PARENT_DICT[album.title]
|
||||
|
||||
|
||||
def test_albums_folder_names():
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
|
||||
albums = photosdb.album_info
|
||||
|
||||
for album in albums:
|
||||
folder_names = album.folder_names
|
||||
assert folder_names == ALBUM_FOLDER_NAMES_DICT[album.title]
|
||||
|
||||
|
||||
def test_albums_folders():
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
|
||||
albums = photosdb.album_info
|
||||
for album in albums:
|
||||
folders = album.folder_list
|
||||
folder_names = [f.title for f in folders]
|
||||
assert folder_names == ALBUM_FOLDER_NAMES_DICT[album.title]
|
||||
|
||||
|
||||
def test_albums_len():
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
|
||||
albums = photosdb.album_info
|
||||
|
||||
for album in albums:
|
||||
assert len(album) == ALBUM_LEN_DICT[album.title]
|
||||
|
||||
|
||||
def test_albums_photos():
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
|
||||
albums = photosdb.album_info
|
||||
|
||||
for album in albums:
|
||||
photos = album.photos
|
||||
assert len(photos) == ALBUM_LEN_DICT[album.title]
|
||||
assert len(photos) == len(album)
|
||||
for photo in photos:
|
||||
assert photo.uuid in ALBUM_PHOTO_UUID_DICT[album.title]
|
||||
|
||||
|
||||
def test_photoinfo_albums():
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
photos = photosdb.photos(uuid=ALBUM_PHOTO_UUID_DICT["Pumpkin Farm"])
|
||||
|
||||
albums = photos[0].albums
|
||||
assert "Pumpkin Farm" in albums
|
||||
|
||||
|
||||
def test_photoinfo_album_info():
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
photos = photosdb.photos(uuid=[UUID_DICT["two_albums"]])
|
||||
|
||||
album_info = photos[0].album_info
|
||||
assert len(album_info) == 2
|
||||
assert album_info[0].title in ["Pumpkin Farm", "Test Album"]
|
||||
assert album_info[1].title in ["Pumpkin Farm", "Test Album"]
|
||||
|
||||
assert photos[0] in album_info[0].photos
|
||||
259
tests/test_albums_folders_mojave_10_14_6.py
Normal file
259
tests/test_albums_folders_mojave_10_14_6.py
Normal file
@@ -0,0 +1,259 @@
|
||||
import pytest
|
||||
|
||||
from osxphotos._constants import _UNKNOWN_PERSON
|
||||
|
||||
PHOTOS_DB = "./tests/Test-10.14.6.photoslibrary/database/photos.db"
|
||||
|
||||
# TOP_LEVEL_FOLDERS = ["Folder1"]
|
||||
|
||||
# TOP_LEVEL_CHILDREN = ["SubFolder1", "SubFolder2"]
|
||||
|
||||
# FOLDER_ALBUM_DICT = {"Folder1": [], "SubFolder1": [], "SubFolder2": ["AlbumInFolder"]}
|
||||
|
||||
# ALBUM_NAMES = ["Pumpkin Farm", "AlbumInFolder", "Test Album", "Test Album"]
|
||||
ALBUM_NAMES = ["Pumpkin Farm", "Test Album", "Test Album (1)"]
|
||||
|
||||
# ALBUM_PARENT_DICT = {
|
||||
# "Pumpkin Farm": None,
|
||||
# "AlbumInFolder": "SubFolder2",
|
||||
# "Test Album": None,
|
||||
# }
|
||||
|
||||
# ALBUM_FOLDER_NAMES_DICT = {
|
||||
# "Pumpkin Farm": [],
|
||||
# "AlbumInFolder": ["Folder1", "SubFolder2"],
|
||||
# "Test Album": [],
|
||||
# }
|
||||
|
||||
ALBUM_LEN_DICT = {
|
||||
"Pumpkin Farm": 3,
|
||||
"Test Album": 1,
|
||||
"Test Album (1)": 1,
|
||||
# "AlbumInFolder": 2,
|
||||
}
|
||||
|
||||
ALBUM_PHOTO_UUID_DICT = {
|
||||
"Pumpkin Farm": [
|
||||
"HrK3ZQdlQ7qpDA0FgOYXLA",
|
||||
"15uNd7%8RguTEgNPKHfTWw",
|
||||
"8SOE9s0XQVGsuq4ONohTng",
|
||||
],
|
||||
"Test Album": ["8SOE9s0XQVGsuq4ONohTng"],
|
||||
"Test Album (1)": ["15uNd7%8RguTEgNPKHfTWw"],
|
||||
# "AlbumInFolder": [
|
||||
# "3DD2C897-F19E-4CA6-8C22-B027D5A71907",
|
||||
# "E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51",
|
||||
# ],
|
||||
}
|
||||
|
||||
UUID_DICT = {"two_albums": "8SOE9s0XQVGsuq4ONohTng"}
|
||||
|
||||
######### Test FolderInfo ##########
|
||||
|
||||
|
||||
def test_folders_1(caplog):
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
|
||||
folders = photosdb.folders
|
||||
assert folders == []
|
||||
assert "Folders not yet implemented for this DB version" in caplog.text
|
||||
|
||||
# # top level folders
|
||||
# folders = photosdb.folders
|
||||
# assert len(folders) == 1
|
||||
|
||||
# # check folder names
|
||||
# folder_names = [f.title for f in folders]
|
||||
# assert sorted(folder_names) == sorted(TOP_LEVEL_FOLDERS)
|
||||
|
||||
|
||||
def test_folder_names(caplog):
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
|
||||
# check folder names
|
||||
folder_names = photosdb.folders
|
||||
assert folder_names == []
|
||||
assert "Folders not yet implemented for this DB version" in caplog.text
|
||||
# assert sorted(folder_names) == sorted(TOP_LEVEL_FOLDERS)
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="Folders not yet impleted in Photos < 5")
|
||||
def test_folders_len():
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
|
||||
# top level folders
|
||||
folders = photosdb.folders
|
||||
assert len(folders[0]) == len(TOP_LEVEL_CHILDREN)
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="Folders not yet impleted in Photos < 5")
|
||||
def test_folders_children():
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
|
||||
# top level folders
|
||||
folders = photosdb.folders
|
||||
|
||||
# children of top level folder
|
||||
children = folders[0].folders
|
||||
children_names = [f.title for f in children]
|
||||
assert sorted(children_names) == sorted(TOP_LEVEL_CHILDREN)
|
||||
|
||||
for child in folders[0].folders:
|
||||
# check valid children FolderInfo
|
||||
assert child.parent
|
||||
assert child.parent.uuid == folders[0].uuid
|
||||
|
||||
# check folder names
|
||||
folder_names = [f.title for f in folders]
|
||||
assert sorted(folder_names) == sorted(TOP_LEVEL_FOLDERS)
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="Folders not yet impleted in Photos < 5")
|
||||
def test_folders_parent():
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
|
||||
# top level folders
|
||||
folders = photosdb.folders
|
||||
|
||||
# parent of top level folder should be none
|
||||
for folder in folders:
|
||||
assert folder.parent is None
|
||||
for child in folder.folders:
|
||||
# children's parent uuid should match folder uuid
|
||||
assert child.parent
|
||||
assert child.parent.uuid == folder.uuid
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="Folders not yet impleted in Photos < 5")
|
||||
def test_folders_albums():
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
|
||||
# top level folders
|
||||
folders = photosdb.folders
|
||||
|
||||
for folder in folders:
|
||||
name = folder.title
|
||||
albums = [a.title for a in folder.album_info]
|
||||
assert sorted(albums) == sorted(FOLDER_ALBUM_DICT[name])
|
||||
for child in folder.folders:
|
||||
name = child.title
|
||||
albums = [a.title for a in child.album_info]
|
||||
assert sorted(albums) == sorted(FOLDER_ALBUM_DICT[name])
|
||||
|
||||
|
||||
########## Test AlbumInfo ##########
|
||||
|
||||
|
||||
def test_albums_1():
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
|
||||
albums = photosdb.album_info
|
||||
assert len(albums) == 3
|
||||
|
||||
# check names
|
||||
album_names = [a.title for a in albums]
|
||||
assert sorted(album_names) == sorted(ALBUM_NAMES)
|
||||
|
||||
|
||||
def test_albums_parent(caplog):
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
|
||||
albums = photosdb.album_info
|
||||
|
||||
for album in albums:
|
||||
parent = album.parent.title if album.parent else None
|
||||
assert "Folders not yet implemented for this DB version" in caplog.text
|
||||
# assert parent == ALBUM_PARENT_DICT[album.title]
|
||||
|
||||
|
||||
def test_albums_folder_names(caplog):
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
|
||||
albums = photosdb.album_info
|
||||
|
||||
for album in albums:
|
||||
folder_names = album.folder_names
|
||||
assert "Folders not yet implemented for this DB version" in caplog.text
|
||||
# assert folder_names == ALBUM_FOLDER_NAMES_DICT[album.title]
|
||||
|
||||
|
||||
def test_albums_folders(caplog):
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
|
||||
albums = photosdb.album_info
|
||||
|
||||
for album in albums:
|
||||
folders = album.folder_list
|
||||
assert "Folders not yet implemented for this DB version" in caplog.text
|
||||
# folder_names = [f.title for f in folders]
|
||||
# assert folder_names == ALBUM_FOLDER_NAMES_DICT[album.title]
|
||||
|
||||
|
||||
def test_albums_len():
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
|
||||
albums = photosdb.album_info
|
||||
|
||||
for album in albums:
|
||||
assert len(album) == ALBUM_LEN_DICT[album.title]
|
||||
|
||||
|
||||
def test_albums_photos():
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
|
||||
albums = photosdb.album_info
|
||||
|
||||
for album in albums:
|
||||
photos = album.photos
|
||||
assert len(photos) == ALBUM_LEN_DICT[album.title]
|
||||
assert len(photos) == len(album)
|
||||
for photo in photos:
|
||||
assert photo.uuid in ALBUM_PHOTO_UUID_DICT[album.title]
|
||||
|
||||
|
||||
def test_photoinfo_albums():
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
photos = photosdb.photos(uuid=ALBUM_PHOTO_UUID_DICT["Pumpkin Farm"])
|
||||
|
||||
albums = photos[0].albums
|
||||
assert "Pumpkin Farm" in albums
|
||||
|
||||
|
||||
def test_photoinfo_album_info():
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
photos = photosdb.photos(uuid=[UUID_DICT["two_albums"]])
|
||||
|
||||
album_info = photos[0].album_info
|
||||
assert len(album_info) == 2
|
||||
assert album_info[0].title in ["Pumpkin Farm", "Test Album"]
|
||||
assert album_info[1].title in ["Pumpkin Farm", "Test Album"]
|
||||
|
||||
assert photos[0] in album_info[0].photos
|
||||
@@ -24,6 +24,7 @@ PERSONS = ["Katie", "Suzy", "Maria", _UNKNOWN_PERSON]
|
||||
ALBUMS = [
|
||||
"Pumpkin Farm",
|
||||
"Test Album",
|
||||
"Multi Keyword",
|
||||
] # Note: there are 2 albums named "Test Album" for testing duplicate album names
|
||||
KEYWORDS_DICT = {
|
||||
"Kids": 4,
|
||||
@@ -40,6 +41,7 @@ PERSONS_DICT = {"Katie": 3, "Suzy": 2, "Maria": 1, _UNKNOWN_PERSON: 1}
|
||||
ALBUM_DICT = {
|
||||
"Pumpkin Farm": 3,
|
||||
"Test Album": 2,
|
||||
"Multi Keyword": 2,
|
||||
} # Note: there are 2 albums named "Test Album" for testing duplicate album names
|
||||
|
||||
UUID_DICT = {
|
||||
@@ -55,6 +57,8 @@ UUID_DICT = {
|
||||
"external_edit": "DC99FBDD-7A52-4100-A5BB-344131646C30",
|
||||
"no_external_edit": "E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51",
|
||||
"export": "D79B8D77-BFFC-460B-9312-034F2877D35B", # "Pumkins2.jpg"
|
||||
"multi_query_1": "D79B8D77-BFFC-460B-9312-034F2877D35B",
|
||||
"multi_query_2": "E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51",
|
||||
}
|
||||
|
||||
|
||||
@@ -155,8 +159,8 @@ def test_album_names():
|
||||
import collections
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
assert "Pumpkin Farm" in photosdb.album_names
|
||||
assert collections.Counter(ALBUMS) == collections.Counter(photosdb.album_names)
|
||||
assert "Pumpkin Farm" in photosdb.albums
|
||||
assert collections.Counter(ALBUMS) == collections.Counter(photosdb.albums)
|
||||
|
||||
|
||||
def test_keywords_dict():
|
||||
@@ -202,7 +206,7 @@ def test_attributes():
|
||||
)
|
||||
assert p.description == "Girl holding pumpkin"
|
||||
assert p.title == "I found one!"
|
||||
assert p.albums == ["Pumpkin Farm", "Test Album"]
|
||||
assert p.albums == ["Pumpkin Farm", "Test Album", "Multi Keyword"]
|
||||
assert p.persons == ["Katie"]
|
||||
assert p.path.endswith(
|
||||
"tests/Test-10.15.1.photoslibrary/originals/D/D79B8D77-BFFC-460B-9312-034F2877D35B.jpeg"
|
||||
@@ -778,3 +782,50 @@ def test_from_to_date():
|
||||
from_date=dt.datetime(2018, 9, 28), to_date=dt.datetime(2018, 9, 29)
|
||||
)
|
||||
assert len(photos) == 4
|
||||
|
||||
|
||||
def test_multi_uuid():
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(PHOTOS_DB)
|
||||
photos = photosdb.photos(uuid=[UUID_DICT["favorite"], UUID_DICT["not_favorite"]])
|
||||
|
||||
assert len(photos) == 2
|
||||
|
||||
|
||||
def test_multi_keyword():
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(PHOTOS_DB)
|
||||
photos = photosdb.photos(keywords=["Kids", "wedding"])
|
||||
|
||||
assert len(photos) == 6
|
||||
|
||||
|
||||
def test_multi_album():
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(PHOTOS_DB)
|
||||
photos = photosdb.photos(albums=["Pumpkin Farm", "Test Album"])
|
||||
|
||||
assert len(photos) == 3
|
||||
|
||||
|
||||
def test_multi_person():
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(PHOTOS_DB)
|
||||
photos = photosdb.photos(persons=["Katie", "Suzy"])
|
||||
|
||||
assert len(photos) == 3
|
||||
|
||||
|
||||
def test_compound_query():
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(PHOTOS_DB)
|
||||
photos = photosdb.photos(persons=["Katie", "Maria"], albums=["Multi Keyword"])
|
||||
|
||||
assert len(photos) == 2
|
||||
assert UUID_DICT["multi_query_1"] in [p.uuid for p in photos]
|
||||
assert UUID_DICT["multi_query_2"] in [p.uuid for p in photos]
|
||||
|
||||
@@ -24,6 +24,7 @@ PERSONS = ["Katie", "Suzy", "Maria", _UNKNOWN_PERSON]
|
||||
ALBUMS = [
|
||||
"Pumpkin Farm",
|
||||
"Test Album",
|
||||
"AlbumInFolder",
|
||||
] # Note: there are 2 albums named "Test Album" for testing duplicate album names
|
||||
KEYWORDS_DICT = {
|
||||
"Kids": 4,
|
||||
@@ -40,6 +41,7 @@ PERSONS_DICT = {"Katie": 3, "Suzy": 2, "Maria": 1, _UNKNOWN_PERSON: 1}
|
||||
ALBUM_DICT = {
|
||||
"Pumpkin Farm": 3,
|
||||
"Test Album": 2,
|
||||
"AlbumInFolder": 2,
|
||||
} # Note: there are 2 albums named "Test Album" for testing duplicate album names
|
||||
|
||||
UUID_DICT = {
|
||||
@@ -155,8 +157,8 @@ def test_album_names():
|
||||
import collections
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
assert "Pumpkin Farm" in photosdb.album_names
|
||||
assert collections.Counter(ALBUMS) == collections.Counter(photosdb.album_names)
|
||||
assert "Pumpkin Farm" in photosdb.albums
|
||||
assert collections.Counter(ALBUMS) == collections.Counter(photosdb.albums)
|
||||
|
||||
|
||||
def test_keywords_dict():
|
||||
|
||||
@@ -2,10 +2,12 @@ import pytest
|
||||
from click.testing import CliRunner
|
||||
|
||||
CLI_PHOTOS_DB = "tests/Test-10.15.1.photoslibrary"
|
||||
LIVE_PHOTOS_DB = "tests/Test-Cloud-10.15.1.photoslibrary/database/photos.db"
|
||||
LIVE_PHOTOS_DB = "tests/Test-Cloud-10.15.1.photoslibrary"
|
||||
RAW_PHOTOS_DB = "tests/Test-RAW-10.15.1.photoslibrary"
|
||||
PLACES_PHOTOS_DB = "tests/Test-Places-Catalina-10_15_1.photoslibrary"
|
||||
PLACES_PHOTOS_DB_13 = "tests/Test-Places-High-Sierra-10.13.6.photoslibrary"
|
||||
PHOTOS_DB_15_4 = "tests/Test-10.15.4.photoslibrary"
|
||||
PHOTOS_DB_14_6 = "tests/Test-10.14.6.photoslibrary"
|
||||
|
||||
CLI_OUTPUT_NO_SUBCOMMAND = [
|
||||
"Options:",
|
||||
@@ -29,7 +31,7 @@ CLI_OUTPUT_NO_SUBCOMMAND = [
|
||||
" query Query the Photos database using 1 or more search options; if",
|
||||
]
|
||||
|
||||
CLI_OUTPUT_QUERY_UUID = '[{"uuid": "D79B8D77-BFFC-460B-9312-034F2877D35B", "filename": "D79B8D77-BFFC-460B-9312-034F2877D35B.jpeg", "original_filename": "Pumkins2.jpg", "date": "2018-09-28T16:07:07-04:00", "description": "Girl holding pumpkin", "title": "I found one!", "keywords": ["Kids"], "albums": ["Pumpkin Farm", "Test Album"], "persons": ["Katie"], "path": "/tests/Test-10.15.1.photoslibrary/originals/D/D79B8D77-BFFC-460B-9312-034F2877D35B.jpeg", "ismissing": false, "hasadjustments": false, "external_edit": false, "favorite": false, "hidden": false, "latitude": null, "longitude": null, "path_edited": null, "shared": false, "isphoto": true, "ismovie": false, "uti": "public.jpeg", "burst": false, "live_photo": false, "path_live_photo": null, "iscloudasset": false, "incloud": null}]'
|
||||
CLI_OUTPUT_QUERY_UUID = '[{"uuid": "D79B8D77-BFFC-460B-9312-034F2877D35B", "filename": "D79B8D77-BFFC-460B-9312-034F2877D35B.jpeg", "original_filename": "Pumkins2.jpg", "date": "2018-09-28T16:07:07-04:00", "description": "Girl holding pumpkin", "title": "I found one!", "keywords": ["Kids"], "albums": ["Pumpkin Farm", "Test Album", "Multi Keyword"], "persons": ["Katie"], "path": "/tests/Test-10.15.1.photoslibrary/originals/D/D79B8D77-BFFC-460B-9312-034F2877D35B.jpeg", "ismissing": false, "hasadjustments": false, "external_edit": false, "favorite": false, "hidden": false, "latitude": null, "longitude": null, "path_edited": null, "shared": false, "isphoto": true, "ismovie": false, "uti": "public.jpeg", "burst": false, "live_photo": false, "path_live_photo": null, "iscloudasset": false, "incloud": null}]'
|
||||
|
||||
CLI_EXPORT_FILENAMES = [
|
||||
"Pumkins1.jpg",
|
||||
@@ -52,7 +54,7 @@ CLI_EXPORTED_DIRECTORY_TEMPLATE_FILENAMES1 = [
|
||||
]
|
||||
|
||||
CLI_EXPORTED_DIRECTORY_TEMPLATE_FILENAMES_ALBUM1 = [
|
||||
"_/wedding.jpg",
|
||||
"Multi Keyword/wedding.jpg",
|
||||
"_/Tulips.jpg",
|
||||
"_/St James Park.jpg",
|
||||
"Pumpkin Farm/Pumpkins3.jpg",
|
||||
@@ -62,7 +64,7 @@ CLI_EXPORTED_DIRECTORY_TEMPLATE_FILENAMES_ALBUM1 = [
|
||||
]
|
||||
|
||||
CLI_EXPORTED_DIRECTORY_TEMPLATE_FILENAMES_ALBUM2 = [
|
||||
"NOALBUM/wedding.jpg",
|
||||
"Multi Keyword/wedding.jpg",
|
||||
"NOALBUM/Tulips.jpg",
|
||||
"NOALBUM/St James Park.jpg",
|
||||
"Pumpkin Farm/Pumpkins3.jpg",
|
||||
@@ -658,3 +660,84 @@ def test_no_place_15():
|
||||
|
||||
assert len(json_got) == 1 # single element
|
||||
assert json_got[0]["uuid"] == "A9B73E13-A6F2-4915-8D67-7213B39BAE9F"
|
||||
|
||||
|
||||
def test_no_folder_1_15():
|
||||
# test --folder on 10.15
|
||||
import json
|
||||
import os
|
||||
import os.path
|
||||
import osxphotos
|
||||
from osxphotos.__main__ import query
|
||||
|
||||
runner = CliRunner()
|
||||
cwd = os.getcwd()
|
||||
# pylint: disable=not-context-manager
|
||||
with runner.isolated_filesystem():
|
||||
result = runner.invoke(
|
||||
query, [os.path.join(cwd, PHOTOS_DB_15_4), "--json", "--folder", "Folder1"]
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
json_got = json.loads(result.output)
|
||||
|
||||
assert len(json_got) == 2 # single element
|
||||
for item in json_got:
|
||||
assert item["uuid"] in [
|
||||
"3DD2C897-F19E-4CA6-8C22-B027D5A71907",
|
||||
"E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51",
|
||||
]
|
||||
assert item["albums"] == ["AlbumInFolder"]
|
||||
|
||||
|
||||
def test_no_folder_2_15():
|
||||
# test --folder with --uuid on 10.15
|
||||
import json
|
||||
import os
|
||||
import os.path
|
||||
import osxphotos
|
||||
from osxphotos.__main__ import query
|
||||
|
||||
runner = CliRunner()
|
||||
cwd = os.getcwd()
|
||||
# pylint: disable=not-context-manager
|
||||
with runner.isolated_filesystem():
|
||||
result = runner.invoke(
|
||||
query,
|
||||
[
|
||||
os.path.join(cwd, PHOTOS_DB_15_4),
|
||||
"--json",
|
||||
"--folder",
|
||||
"Folder1",
|
||||
"--uuid",
|
||||
"E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51",
|
||||
],
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
json_got = json.loads(result.output)
|
||||
|
||||
assert len(json_got) == 1 # single element
|
||||
for item in json_got:
|
||||
assert item["uuid"] == "E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51"
|
||||
assert item["albums"] == ["AlbumInFolder"]
|
||||
|
||||
|
||||
def test_no_folder_1_14(caplog):
|
||||
# test --folder on 10.14
|
||||
import json
|
||||
import os
|
||||
import os.path
|
||||
import osxphotos
|
||||
from osxphotos.__main__ import query
|
||||
|
||||
runner = CliRunner()
|
||||
cwd = os.getcwd()
|
||||
# pylint: disable=not-context-manager
|
||||
with runner.isolated_filesystem():
|
||||
result = runner.invoke(
|
||||
query, [os.path.join(cwd, PHOTOS_DB_14_6), "--json", "--folder", "Folder1"]
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
json_got = json.loads(result.output)
|
||||
|
||||
assert len(json_got) == 0 # single element
|
||||
assert "not yet implemented" in caplog.text
|
||||
|
||||
@@ -57,7 +57,7 @@ def test_album_names():
|
||||
import collections
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
assert photosdb.album_names == []
|
||||
assert photosdb.albums == []
|
||||
|
||||
|
||||
def test_keywords_dict():
|
||||
|
||||
@@ -76,8 +76,8 @@ def test_album_names():
|
||||
import collections
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
assert "Pumpkin Farm" in photosdb.album_names
|
||||
assert collections.Counter(ALBUMS) == collections.Counter(photosdb.album_names)
|
||||
assert "Pumpkin Farm" in photosdb.albums
|
||||
assert collections.Counter(ALBUMS) == collections.Counter(photosdb.albums)
|
||||
|
||||
|
||||
def test_keywords_dict():
|
||||
|
||||
@@ -76,8 +76,8 @@ def test_album_names():
|
||||
import collections
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
assert "Pumpkin Farm" in photosdb.album_names
|
||||
assert collections.Counter(ALBUMS) == collections.Counter(photosdb.album_names)
|
||||
assert "Pumpkin Farm" in photosdb.albums
|
||||
assert collections.Counter(ALBUMS) == collections.Counter(photosdb.albums)
|
||||
|
||||
|
||||
def test_keywords_dict():
|
||||
|
||||
@@ -33,7 +33,10 @@ KEYWORDS_DICT = {
|
||||
PERSONS_DICT = {"Katie": 3, "Suzy": 2, "Maria": 1}
|
||||
ALBUM_DICT = {"Pumpkin Farm": 3, "Test Album": 1, "Test Album (1)": 1}
|
||||
|
||||
UUID_DICT = {"favorite": "6bxcNnzRQKGnK4uPrCJ9UQ"}
|
||||
UUID_DICT = {
|
||||
"favorite": "6bxcNnzRQKGnK4uPrCJ9UQ",
|
||||
"not_favorite": "8SOE9s0XQVGsuq4ONohTng",
|
||||
}
|
||||
|
||||
|
||||
def test_init():
|
||||
@@ -81,8 +84,8 @@ def test_album_names():
|
||||
import collections
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
assert "Pumpkin Farm" in photosdb.album_names
|
||||
assert collections.Counter(ALBUMS) == collections.Counter(photosdb.album_names)
|
||||
assert "Pumpkin Farm" in photosdb.albums
|
||||
assert collections.Counter(ALBUMS) == collections.Counter(photosdb.albums)
|
||||
|
||||
|
||||
def test_keywords_dict():
|
||||
@@ -354,3 +357,39 @@ def test_photosinfo_repr():
|
||||
assert {k: str(v).encode("utf-8") for k, v in photo.__dict__.items()} == {
|
||||
k: str(v).encode("utf-8") for k, v in photo2.__dict__.items()
|
||||
}
|
||||
|
||||
|
||||
def test_multi_uuid():
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(PHOTOS_DB)
|
||||
photos = photosdb.photos(uuid=[UUID_DICT["favorite"], UUID_DICT["not_favorite"]])
|
||||
|
||||
assert len(photos) == 2
|
||||
|
||||
|
||||
def test_multi_keyword():
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(PHOTOS_DB)
|
||||
photos = photosdb.photos(keywords=["Kids", "wedding"])
|
||||
|
||||
assert len(photos) == 6
|
||||
|
||||
|
||||
def test_multi_album():
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(PHOTOS_DB)
|
||||
photos = photosdb.photos(albums=["Pumpkin Farm", "Test Album"])
|
||||
|
||||
assert len(photos) == 3
|
||||
|
||||
|
||||
def test_multi_person():
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(PHOTOS_DB)
|
||||
photos = photosdb.photos(persons=["Katie", "Suzy"])
|
||||
|
||||
assert len(photos) == 3
|
||||
|
||||
@@ -30,7 +30,7 @@ def test_album_names():
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
albums = photosdb.album_names
|
||||
albums = photosdb.albums
|
||||
|
||||
assert len(albums) == 1
|
||||
assert albums[0] == ALBUMS[0]
|
||||
@@ -40,7 +40,7 @@ def test_albums_shared():
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
albums_shared = photosdb.album_names_shared
|
||||
albums_shared = photosdb.albums_shared
|
||||
|
||||
assert len(albums_shared) == 1
|
||||
assert albums_shared[0] == ALBUMS_SHARED[0]
|
||||
|
||||
@@ -15,7 +15,7 @@ def test_album_names():
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
albums = photosdb.album_names
|
||||
albums = photosdb.albums
|
||||
|
||||
assert len(albums) == len(ALBUMS)
|
||||
for album in albums:
|
||||
@@ -26,7 +26,7 @@ def test_albums_names_shared():
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
albums_shared = photosdb.album_names_shared
|
||||
albums_shared = photosdb.albums_shared
|
||||
|
||||
assert len(albums_shared) == 0
|
||||
|
||||
|
||||
@@ -1,13 +1,21 @@
|
||||
""" Test template.py """
|
||||
import pytest
|
||||
|
||||
PHOTOS_DB_1 = "./tests/Test-Places-Catalina-10_15_1.photoslibrary/database/photos.db"
|
||||
PHOTOS_DB_2 = "./tests/Test-10.15.1.photoslibrary/database/photos.db"
|
||||
PHOTOS_DB_PLACES = (
|
||||
"./tests/Test-Places-Catalina-10_15_1.photoslibrary/database/photos.db"
|
||||
)
|
||||
PHOTOS_DB_15_1 = "./tests/Test-10.15.1.photoslibrary/database/photos.db"
|
||||
PHOTOS_DB_15_4 = "./tests/Test-10.15.4.photoslibrary/database/photos.db"
|
||||
PHOTOS_DB_14_6 = "./tests/Test-10.14.6.photoslibrary/database/photos.db"
|
||||
|
||||
UUID_DICT = {
|
||||
"place_dc": "128FB4C6-0B16-4E7D-9108-FB2E90DA1546",
|
||||
"1_1_2": "1EB2B765-0765-43BA-A90C-0D0580E6172C",
|
||||
"2_1_1": "D79B8D77-BFFC-460B-9312-034F2877D35B",
|
||||
"0_2_0": "6191423D-8DB8-4D4C-92BE-9BBBA308AAC4",
|
||||
"folder_album_1": "3DD2C897-F19E-4CA6-8C22-B027D5A71907",
|
||||
"folder_album_no_folder": "D79B8D77-BFFC-460B-9312-034F2877D35B",
|
||||
"mojave_no_folder": "15uNd7%8RguTEgNPKHfTWw",
|
||||
}
|
||||
|
||||
TEMPLATE_VALUES = {
|
||||
@@ -55,7 +63,7 @@ def test_lookup():
|
||||
TEMPLATE_SUBSTITUTIONS,
|
||||
)
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_1)
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_PLACES)
|
||||
photo = photosdb.photos(uuid=[UUID_DICT["place_dc"]])[0]
|
||||
|
||||
for subst in TEMPLATE_SUBSTITUTIONS:
|
||||
@@ -71,7 +79,7 @@ def test_subst():
|
||||
from osxphotos.template import render_filepath_template
|
||||
|
||||
locale.setlocale(locale.LC_ALL, "en_US")
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_1)
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_PLACES)
|
||||
photo = photosdb.photos(uuid=[UUID_DICT["place_dc"]])[0]
|
||||
|
||||
for template in TEMPLATE_VALUES:
|
||||
@@ -86,7 +94,7 @@ def test_subst_default_val():
|
||||
from osxphotos.template import render_filepath_template
|
||||
|
||||
locale.setlocale(locale.LC_ALL, "en_US")
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_1)
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_PLACES)
|
||||
photo = photosdb.photos(uuid=[UUID_DICT["place_dc"]])[0]
|
||||
|
||||
template = "{place.name.area_of_interest,UNKNOWN}"
|
||||
@@ -101,7 +109,7 @@ def test_subst_default_val_2():
|
||||
from osxphotos.template import render_filepath_template
|
||||
|
||||
locale.setlocale(locale.LC_ALL, "en_US")
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_1)
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_PLACES)
|
||||
photo = photosdb.photos(uuid=[UUID_DICT["place_dc"]])[0]
|
||||
|
||||
template = "{place.name.area_of_interest,}"
|
||||
@@ -116,7 +124,7 @@ def test_subst_unknown_val():
|
||||
from osxphotos.template import render_filepath_template
|
||||
|
||||
locale.setlocale(locale.LC_ALL, "en_US")
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_1)
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_PLACES)
|
||||
photo = photosdb.photos(uuid=[UUID_DICT["place_dc"]])[0]
|
||||
|
||||
template = "{created.year}/{foo}"
|
||||
@@ -134,7 +142,7 @@ def test_subst_double_brace():
|
||||
import osxphotos
|
||||
from osxphotos.template import render_filepath_template
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_1)
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_PLACES)
|
||||
photo = photosdb.photos(uuid=[UUID_DICT["place_dc"]])[0]
|
||||
|
||||
template = "{created.year}/{{foo}}"
|
||||
@@ -150,7 +158,7 @@ def test_subst_unknown_val_with_default():
|
||||
from osxphotos.template import render_filepath_template
|
||||
|
||||
locale.setlocale(locale.LC_ALL, "en_US")
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_1)
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_PLACES)
|
||||
photo = photosdb.photos(uuid=[UUID_DICT["place_dc"]])[0]
|
||||
|
||||
template = "{created.year}/{foo,bar}"
|
||||
@@ -165,7 +173,7 @@ def test_subst_multi_1_1_2():
|
||||
import osxphotos
|
||||
from osxphotos.template import render_filepath_template
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_2)
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_15_1)
|
||||
photo = photosdb.photos(uuid=[UUID_DICT["1_1_2"]])[0]
|
||||
|
||||
template = "{created.year}/{album}/{keyword}/{person}"
|
||||
@@ -180,12 +188,16 @@ def test_subst_multi_2_1_1():
|
||||
import osxphotos
|
||||
from osxphotos.template import render_filepath_template
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_2)
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_15_1)
|
||||
# one album, one keyword, two persons
|
||||
photo = photosdb.photos(uuid=[UUID_DICT["2_1_1"]])[0]
|
||||
|
||||
template = "{created.year}/{album}/{keyword}/{person}"
|
||||
expected = ["2018/Pumpkin Farm/Kids/Katie", "2018/Test Album/Kids/Katie"]
|
||||
expected = [
|
||||
"2018/Pumpkin Farm/Kids/Katie",
|
||||
"2018/Test Album/Kids/Katie",
|
||||
"2018/Multi Keyword/Kids/Katie",
|
||||
]
|
||||
rendered, _ = render_filepath_template(template, photo)
|
||||
assert sorted(rendered) == sorted(expected)
|
||||
|
||||
@@ -196,7 +208,7 @@ def test_subst_multi_2_1_1_single():
|
||||
import osxphotos
|
||||
from osxphotos.template import render_filepath_template
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_2)
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_15_1)
|
||||
# one album, one keyword, two persons
|
||||
photo = photosdb.photos(uuid=[UUID_DICT["2_1_1"]])[0]
|
||||
|
||||
@@ -212,7 +224,7 @@ def test_subst_multi_0_2_0():
|
||||
import osxphotos
|
||||
from osxphotos.template import render_filepath_template
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_2)
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_15_1)
|
||||
# one album, one keyword, two persons
|
||||
photo = photosdb.photos(uuid=[UUID_DICT["0_2_0"]])[0]
|
||||
|
||||
@@ -228,7 +240,7 @@ def test_subst_multi_0_2_0_single():
|
||||
import osxphotos
|
||||
from osxphotos.template import render_filepath_template
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_2)
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_15_1)
|
||||
# one album, one keyword, two persons
|
||||
photo = photosdb.photos(uuid=[UUID_DICT["0_2_0"]])[0]
|
||||
|
||||
@@ -244,7 +256,7 @@ def test_subst_multi_0_2_0_default_val():
|
||||
import osxphotos
|
||||
from osxphotos.template import render_filepath_template
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_2)
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_15_1)
|
||||
# one album, one keyword, two persons
|
||||
photo = photosdb.photos(uuid=[UUID_DICT["0_2_0"]])[0]
|
||||
|
||||
@@ -260,7 +272,7 @@ def test_subst_multi_0_2_0_default_val_unknown_val():
|
||||
import osxphotos
|
||||
from osxphotos.template import render_filepath_template
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_2)
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_15_1)
|
||||
# one album, one keyword, two persons
|
||||
photo = photosdb.photos(uuid=[UUID_DICT["0_2_0"]])[0]
|
||||
|
||||
@@ -282,7 +294,7 @@ def test_subst_multi_0_2_0_default_val_unknown_val_2():
|
||||
import osxphotos
|
||||
from osxphotos.template import render_filepath_template
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_2)
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_15_1)
|
||||
# one album, one keyword, two persons
|
||||
photo = photosdb.photos(uuid=[UUID_DICT["0_2_0"]])[0]
|
||||
|
||||
@@ -294,3 +306,52 @@ def test_subst_multi_0_2_0_default_val_unknown_val_2():
|
||||
rendered, unknown = render_filepath_template(template, photo)
|
||||
assert sorted(rendered) == sorted(expected)
|
||||
assert unknown == ["foo"]
|
||||
|
||||
|
||||
def test_subst_multi_folder_albums_1():
|
||||
""" Test substitutions for folder_album are correct """
|
||||
import osxphotos
|
||||
from osxphotos.template import render_filepath_template
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_15_4)
|
||||
|
||||
# photo in an album in a folder
|
||||
photo = photosdb.photos(uuid=[UUID_DICT["folder_album_1"]])[0]
|
||||
template = "{folder_album}"
|
||||
expected = ["Folder1/SubFolder2/AlbumInFolder"]
|
||||
rendered, unknown = render_filepath_template(template, photo)
|
||||
assert sorted(rendered) == sorted(expected)
|
||||
assert unknown == []
|
||||
|
||||
|
||||
def test_subst_multi_folder_albums_2():
|
||||
""" Test substitutions for folder_album are correct """
|
||||
import osxphotos
|
||||
from osxphotos.template import render_filepath_template
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_15_4)
|
||||
|
||||
# photo in an album in a folder
|
||||
photo = photosdb.photos(uuid=[UUID_DICT["folder_album_no_folder"]])[0]
|
||||
template = "{folder_album}"
|
||||
expected = ["Pumpkin Farm", "Test Album"]
|
||||
rendered, unknown = render_filepath_template(template, photo)
|
||||
assert sorted(rendered) == sorted(expected)
|
||||
assert unknown == []
|
||||
|
||||
|
||||
def test_subst_multi_folder_albums_3(caplog):
|
||||
""" Test substitutions for folder_album on < Photos 5 (not implemented) """
|
||||
import osxphotos
|
||||
from osxphotos.template import render_filepath_template
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_14_6)
|
||||
|
||||
# photo in an album in a folder
|
||||
photo = photosdb.photos(uuid=[UUID_DICT["mojave_no_folder"]])[0]
|
||||
template = "{folder_album}"
|
||||
expected = ["Pumpkin Farm", "Test Album (1)"]
|
||||
rendered, unknown = render_filepath_template(template, photo)
|
||||
assert sorted(rendered) == sorted(expected)
|
||||
assert unknown == []
|
||||
assert "not yet implemented" in caplog.text
|
||||
|
||||
Reference in New Issue
Block a user