Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3bac106eb7 | ||
|
|
47d1c82c03 | ||
|
|
6f281711e2 | ||
|
|
4b30b3b426 | ||
|
|
1fa9583ea6 | ||
|
|
235e1fb1a6 | ||
|
|
36c2821a0f | ||
|
|
ed425724a0 | ||
|
|
55daa31c71 | ||
|
|
b6ac9e1ea3 | ||
|
|
9d151478d6 | ||
|
|
7d55844390 | ||
|
|
f398e9116f | ||
|
|
4fe8190b57 | ||
|
|
7e42ebb240 | ||
|
|
edae116baa | ||
|
|
d542cda17d | ||
|
|
99b5b54c6d | ||
|
|
379feddcda | ||
|
|
24285f5dd2 | ||
|
|
3cb3ebb300 | ||
|
|
16037f10fa | ||
|
|
ebd21491ac |
16
CHANGELOG.md
16
CHANGELOG.md
@@ -4,6 +4,22 @@ 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.28.1](https://github.com/RhetTbull/osxphotos/compare/v0.27.4...v0.28.1)
|
||||
|
||||
> 18 April 2020
|
||||
|
||||
- Initial work on suppport for associated RAW images [`7e42ebb`](https://github.com/RhetTbull/osxphotos/commit/7e42ebb2402d45cd5d20bdd55bddddaa9db4679f)
|
||||
- Initial support for RAW photos in Photos 4 to address issue #101 [`9d15147`](https://github.com/RhetTbull/osxphotos/commit/9d151478d610291b8d482aafae3d445dfd391fca)
|
||||
- replaced CLI option --original-name with --current-name [`36c2821`](https://github.com/RhetTbull/osxphotos/commit/36c2821a0fa62eaaa54cf1edc2d9c6da98155354)
|
||||
|
||||
#### [v0.27.4](https://github.com/RhetTbull/osxphotos/compare/v0.27.3...v0.27.4)
|
||||
|
||||
> 12 April 2020
|
||||
|
||||
- Added {folder_album} to template and --folder to CLI [`b7c7b9f`](https://github.com/RhetTbull/osxphotos/commit/b7c7b9f0664e69c743bdd8a228ad2936cf6b7600)
|
||||
- Test library update [`21e7020`](https://github.com/RhetTbull/osxphotos/commit/21e7020fec406b0f3926d7adc8a1451bfe77e75a)
|
||||
- Updated CHANGELOG.md [`952741d`](https://github.com/RhetTbull/osxphotos/commit/952741d488d2fbbaf8a0c1d3781ad7c4205c068f)
|
||||
|
||||
#### [v0.27.3](https://github.com/RhetTbull/osxphotos/compare/v0.27.1...v0.27.3)
|
||||
|
||||
> 12 April 2020
|
||||
|
||||
67
README.md
67
README.md
@@ -21,6 +21,7 @@
|
||||
* [Examples](#examples)
|
||||
* [Related Projects](#related-projects)
|
||||
* [Contributing](#contributing)
|
||||
* [Known Bugs](#known-bugs)
|
||||
* [Implementation Notes](#implementation-notes)
|
||||
* [Dependencies](#dependencies)
|
||||
* [Acknowledgements](#acknowledgements)
|
||||
@@ -95,7 +96,10 @@ Usage: osxphotos export [OPTIONS] [PHOTOS_LIBRARY]... DEST
|
||||
Optionally, query the Photos database using 1 or more search options; if
|
||||
more than one option is provided, they are treated as "AND" (e.g. search
|
||||
for photos matching all options). If no query options are provided, all
|
||||
photos will be exported.
|
||||
photos will be exported. By default, all versions of all photos will be
|
||||
exported including edited versions, live photo movies, burst photos, and
|
||||
associated RAW images. See --skip-edited, --skip-live, --skip-bursts, and
|
||||
--skip-raw options to modify this behavior.
|
||||
|
||||
Options:
|
||||
--db <Photos database path> Specify Photos database path. Path to Photos
|
||||
@@ -107,6 +111,7 @@ Options:
|
||||
order: 1. last opened library, 2. system
|
||||
library, 3. ~/Pictures/Photos
|
||||
Library.photoslibrary
|
||||
-V, --verbose Print verbose output.
|
||||
--keyword KEYWORD Search for photos with keyword KEYWORD. If
|
||||
more than one keyword, treated as "OR", e.g.
|
||||
find photos match any keyword
|
||||
@@ -171,6 +176,8 @@ Options:
|
||||
--not-selfie Search for photos that are not selfies.
|
||||
--panorama Search for panorama photos.
|
||||
--not-panorama Search for photos that are not panoramas.
|
||||
--has-raw Search for photos with both a jpeg and RAW
|
||||
version
|
||||
--only-movies Search only for movies (default searches
|
||||
both images and movies).
|
||||
--only-photos Search only for photos/images (default
|
||||
@@ -183,7 +190,6 @@ Options:
|
||||
Search by end item date, e.g.
|
||||
2000-01-12T12:00:00 or 2000-12-31 (ISO 8601
|
||||
w/o TZ).
|
||||
-V, --verbose Print verbose output.
|
||||
--overwrite Overwrite existing files. Default behavior
|
||||
is to add (1), (2), etc to filename if file
|
||||
already exists. Use this with caution as it
|
||||
@@ -192,19 +198,23 @@ Options:
|
||||
--export-by-date Automatically create output folders to
|
||||
organize photos by date created (e.g.
|
||||
DEST/2019/12/20/photoname.jpg).
|
||||
--export-edited Also export edited version of photo if an
|
||||
edited version exists. Edited photo will be
|
||||
named in form of "photoname_edited.ext"
|
||||
--export-bursts If a photo is a burst photo export all
|
||||
associated burst images in the library. Not
|
||||
currently compatible with --download-
|
||||
misssing; see note on --download-missing.
|
||||
--export-live If a photo is a live photo export the
|
||||
associated live video component. Live video
|
||||
will have same name as photo but with .mov
|
||||
extension.
|
||||
--original-name Use photo's original filename instead of
|
||||
current filename for export.
|
||||
--skip-edited Do not export edited version of photo if an
|
||||
edited version exists.
|
||||
--skip-bursts Do not export all associated burst images in
|
||||
the library if a photo is a burst photo.
|
||||
--skip-live Do not export the associated live video
|
||||
component of a live photo.
|
||||
--skip-raw Do not export associated RAW images of a
|
||||
RAW/jpeg pair. Note: this does not skip RAW
|
||||
photos if the RAW photo does not have an
|
||||
associated jpeg image (e.g. the RAW file was
|
||||
imported to Photos without a jpeg preview).
|
||||
--current-name Use photo's current filename instead of
|
||||
original filename for export. Note:
|
||||
Starting with Photos 5, all photos are
|
||||
renamed upon import. By default, photos are
|
||||
exported with the the original name they had
|
||||
before import.
|
||||
--sidecar FORMAT Create sidecar for each photo exported;
|
||||
valid FORMAT values: xmp, json; --sidecar
|
||||
json: create JSON sidecar useable by
|
||||
@@ -224,9 +234,9 @@ Options:
|
||||
exist on disk. This will be slow and will
|
||||
require internet connection. This obviously
|
||||
only works if the Photos library is synched
|
||||
to iCloud. Note: --download-missing is not
|
||||
currently compatabile with --export-bursts;
|
||||
only the primary photo will be exported--
|
||||
to iCloud. Note: --download-missing does
|
||||
not currently export all burst images; only
|
||||
the primary photo will be exported--
|
||||
associated burst images will be skipped.
|
||||
--exiftool Use exiftool to write metadata directly to
|
||||
exported photos. To use this option,
|
||||
@@ -353,13 +363,13 @@ Substitution Description
|
||||
{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
|
||||
Example: export all photos to ~/Desktop/export group in folders by date created
|
||||
|
||||
`osxphotos export --export-edited --export-live --export-by-date ~/Pictures/Photos\ Library.photoslibrary ~/Desktop/export`
|
||||
`osxphotos export --export-by-date ~/Pictures/Photos\ Library.photoslibrary ~/Desktop/export`
|
||||
|
||||
**Note**: Photos library/database path can also be specified using --db option:
|
||||
|
||||
`osxphotos export --export-edited --export-live --export-by-date --db ~/Pictures/Photos\ Library.photoslibrary ~/Desktop/export`
|
||||
`osxphotos export --export-by-date --db ~/Pictures/Photos\ Library.photoslibrary ~/Desktop/export`
|
||||
|
||||
Example: find all photos with keyword "Kids" and output results to json file named results.json:
|
||||
|
||||
@@ -1015,7 +1025,7 @@ Returns a [FolderInfo](#FolderInfo) object representing the albums parent 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.
|
||||
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.
|
||||
|
||||
#### `uuid`
|
||||
Returns the universally unique identifier (uuid) of the folder. This is how Photos keeps track of individual objects within the database.
|
||||
@@ -1037,7 +1047,7 @@ Returns a [FolderInfo](#FolderInfo) object representing the folder's parent fold
|
||||
```python
|
||||
>>> import osxphotos
|
||||
>>> photosdb = osxphotos.PhotosDB()
|
||||
>>> photosdb.subfolders
|
||||
>>> photosdb.folder_info
|
||||
[<osxphotos.albuminfo.FolderInfo object at 0x10fcc0160>]
|
||||
>>> photosdb.folder_info[0].title
|
||||
'Folder1'
|
||||
@@ -1296,13 +1306,20 @@ If you have an interesting example that shows usage of this package, submit an i
|
||||
|
||||
Testing against "real world" Photos libraries would be especially helpful. If you discover issues in testing against your Photos libraries, please open an issue. I've done extensive testing against my own Photos library but that's a since data point and I'm certain there are issues lurking in various edge cases I haven't discovered yet.
|
||||
|
||||
## Known Bugs
|
||||
|
||||
My goal is make osxphotos as reliable and comprehensive as possible. The test suite currently has over 400 tests--but there are still some [bugs](https://github.com/RhetTbull/osxphotos/issues?q=is%3Aissue+is%3Aopen+label%3Abug) or incomplete features lurking. If you find bugs please open an [issue](https://github.com/RhetTbull/osxphotos/issues). Notable issues include:
|
||||
|
||||
- RAW images imported to Photos with an associated jpeg preview are not handled correctly by osxphotos. osxphotos query and export will operate on the jpeg preview instead of the RAW image as will `PhotoInfo.path`. If the user selects "Use RAW as original" in Photos, the RAW image will be exported or operated on but the jpeg will be ignored. See [Issue #101](https://github.com/RhetTbull/osxphotos/issues/101) Note: Alpha version of fix for this bug is implemented in the current version of osxphotos.
|
||||
- The `--download-missing` option for `osxphotos export` does not work correctly with burst images. It will download the primary image but not the other burst images. See [Issue #75](https://github.com/RhetTbull/osxphotos/issues/75)
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
Apple does provide a framework ([PhotoKit](https://developer.apple.com/documentation/photokit?language=objc)) for querying the user's Photos library and I attempted to create the funcationality in this package using this framework but unfortunately PhotoKit does not provide access to much of the needed metadata (such as Faces/Persons). While copying the sqlite file is a bit kludgy, it allows osxphotos to provide access to all available metadata.
|
||||
Apple does provide a framework ([PhotoKit](https://developer.apple.com/documentation/photokit?language=objc)) for querying the user's Photos library and I attempted to create the funcationality in this package using this framework but unfortunately PhotoKit does not provide access to much of the needed metadata (such as Faces/Persons) and Apple's System Integrity Protection (SIP) made the interface unreliable. If you'd like to experiment with the PhotoKit interface, here's some sample [code](https://gist.github.com/RhetTbull/41cc85e5bdeb30f761147ce32fba5c94). While copying the sqlite file is a bit kludgy, it allows osxphotos to provide access to all available metadata.
|
||||
|
||||
## Dependencies
|
||||
- [PyObjC](https://pythonhosted.org/pyobjc/)
|
||||
|
||||
@@ -14,7 +14,7 @@ def main():
|
||||
|
||||
print(photosdb.keywords)
|
||||
print(photosdb.persons)
|
||||
print(photosdb.album_names)
|
||||
print(photosdb.albums)
|
||||
|
||||
print(photosdb.keywords_as_dict)
|
||||
print(photosdb.persons_as_dict)
|
||||
|
||||
@@ -345,6 +345,11 @@ def query_options(f):
|
||||
is_flag=True,
|
||||
help="Search for photos that are not panoramas.",
|
||||
),
|
||||
o(
|
||||
"--has-raw",
|
||||
is_flag=True,
|
||||
help="Search for photos with both a jpeg and RAW version",
|
||||
),
|
||||
o(
|
||||
"--only-movies",
|
||||
is_flag=True,
|
||||
@@ -724,6 +729,7 @@ def query(
|
||||
not_selfie,
|
||||
panorama,
|
||||
not_panorama,
|
||||
has_raw,
|
||||
place,
|
||||
no_place,
|
||||
):
|
||||
@@ -743,6 +749,7 @@ def query(
|
||||
edited,
|
||||
external_edit,
|
||||
uti,
|
||||
has_raw,
|
||||
from_date,
|
||||
to_date,
|
||||
]
|
||||
@@ -836,6 +843,7 @@ def query(
|
||||
not_selfie=not_selfie,
|
||||
panorama=panorama,
|
||||
not_panorama=not_panorama,
|
||||
has_raw=has_raw,
|
||||
place=place,
|
||||
no_place=no_place,
|
||||
)
|
||||
@@ -847,8 +855,8 @@ def query(
|
||||
|
||||
@cli.command(cls=ExportCommand)
|
||||
@DB_OPTION
|
||||
@query_options
|
||||
@click.option("--verbose", "-V", is_flag=True, help="Print verbose output.")
|
||||
@query_options
|
||||
@click.option(
|
||||
"--overwrite",
|
||||
is_flag=True,
|
||||
@@ -864,27 +872,33 @@ def query(
|
||||
"(e.g. DEST/2019/12/20/photoname.jpg).",
|
||||
)
|
||||
@click.option(
|
||||
"--export-edited",
|
||||
"--skip-edited",
|
||||
is_flag=True,
|
||||
help="Also export edited version of photo if an edited version exists. "
|
||||
'Edited photo will be named in form of "photoname_edited.ext"',
|
||||
help="Do not export edited version of photo if an edited version exists.",
|
||||
)
|
||||
@click.option(
|
||||
"--export-bursts",
|
||||
"--skip-bursts",
|
||||
is_flag=True,
|
||||
help="If a photo is a burst photo export all associated burst images in the library. "
|
||||
"Not currently compatible with --download-misssing; see note on --download-missing.",
|
||||
help="Do not export all associated burst images in the library if a photo is a burst photo. ",
|
||||
)
|
||||
@click.option(
|
||||
"--export-live",
|
||||
"--skip-live",
|
||||
is_flag=True,
|
||||
help="If a photo is a live photo export the associated live video component."
|
||||
" Live video will have same name as photo but with .mov extension. ",
|
||||
help="Do not export the associated live video component of a live photo.",
|
||||
)
|
||||
@click.option(
|
||||
"--original-name",
|
||||
"--skip-raw",
|
||||
is_flag=True,
|
||||
help="Use photo's original filename instead of current filename for export.",
|
||||
help="Do not export associated RAW images of a RAW/jpeg pair. "
|
||||
"Note: this does not skip RAW photos if the RAW photo does not have an associated jpeg image "
|
||||
"(e.g. the RAW file was imported to Photos without a jpeg preview).",
|
||||
)
|
||||
@click.option(
|
||||
"--current-name",
|
||||
is_flag=True,
|
||||
help="Use photo's current filename instead of original filename for export. "
|
||||
"Note: Starting with Photos 5, all photos are renamed upon import. By default, "
|
||||
"photos are exported with the the original name they had before import.",
|
||||
)
|
||||
@click.option(
|
||||
"--sidecar",
|
||||
@@ -907,7 +921,7 @@ def query(
|
||||
"to interact with Photos to export the photo which will force Photos to download from iCloud if "
|
||||
"the photo does not exist on disk. This will be slow and will require internet connection. "
|
||||
"This obviously only works if the Photos library is synched to iCloud. "
|
||||
"Note: --download-missing is not currently compatabile with --export-bursts; "
|
||||
"Note: --download-missing does not currently export all burst images; "
|
||||
"only the primary photo will be exported--associated burst images will be skipped.",
|
||||
)
|
||||
@click.option(
|
||||
@@ -965,10 +979,11 @@ def export(
|
||||
verbose,
|
||||
overwrite,
|
||||
export_by_date,
|
||||
export_edited,
|
||||
export_bursts,
|
||||
export_live,
|
||||
original_name,
|
||||
skip_edited,
|
||||
skip_bursts,
|
||||
skip_live,
|
||||
skip_raw,
|
||||
current_name,
|
||||
sidecar,
|
||||
only_photos,
|
||||
only_movies,
|
||||
@@ -993,6 +1008,7 @@ def export(
|
||||
not_selfie,
|
||||
panorama,
|
||||
not_panorama,
|
||||
has_raw,
|
||||
directory,
|
||||
place,
|
||||
no_place,
|
||||
@@ -1004,6 +1020,10 @@ def export(
|
||||
if more than one option is provided, they are treated as "AND"
|
||||
(e.g. search for photos matching all options).
|
||||
If no query options are provided, all photos will be exported.
|
||||
By default, all versions of all photos will be exported including edited
|
||||
versions, live photo movies, burst photos, and associated RAW images.
|
||||
See --skip-edited, --skip-live, --skip-bursts, and --skip-raw options
|
||||
to modify this behavior.
|
||||
"""
|
||||
|
||||
if not os.path.isdir(dest):
|
||||
@@ -1032,6 +1052,17 @@ def export(
|
||||
click.echo(cli.commands["export"].get_help(ctx), err=True)
|
||||
return
|
||||
|
||||
# initialize export flags
|
||||
# by default, will export all versions of photos unless skip flag is set
|
||||
(export_edited, export_bursts, export_live, export_raw) = [
|
||||
not x for x in [skip_edited, skip_bursts, skip_live, skip_raw]
|
||||
]
|
||||
|
||||
# though the command line option is current_name, internally all processing
|
||||
# logic uses original_name which is the boolean inverse of current_name
|
||||
# because the original code used --original-name as an option
|
||||
original_name = not current_name
|
||||
|
||||
# verify exiftool installed an in path
|
||||
if exiftool:
|
||||
try:
|
||||
@@ -1108,6 +1139,7 @@ def export(
|
||||
not_selfie=not_selfie,
|
||||
panorama=panorama,
|
||||
not_panorama=not_panorama,
|
||||
has_raw=has_raw,
|
||||
place=place,
|
||||
no_place=no_place,
|
||||
)
|
||||
@@ -1141,6 +1173,7 @@ def export(
|
||||
exiftool,
|
||||
directory,
|
||||
no_extended_attributes,
|
||||
export_raw,
|
||||
)
|
||||
else:
|
||||
for p in photos:
|
||||
@@ -1158,6 +1191,7 @@ def export(
|
||||
exiftool,
|
||||
directory,
|
||||
no_extended_attributes,
|
||||
export_raw,
|
||||
)
|
||||
if export_paths:
|
||||
click.echo(f"Exported {p.filename} to {export_paths}")
|
||||
@@ -1232,6 +1266,9 @@ def print_photo_info(photos, json=False):
|
||||
"hdr",
|
||||
"selfie",
|
||||
"panorama",
|
||||
"has_raw",
|
||||
"uti_raw",
|
||||
"path_raw",
|
||||
]
|
||||
)
|
||||
for p in photos:
|
||||
@@ -1273,6 +1310,9 @@ def print_photo_info(photos, json=False):
|
||||
p.hdr,
|
||||
p.selfie,
|
||||
p.panorama,
|
||||
p.has_raw,
|
||||
p.uti_raw,
|
||||
p.path_raw,
|
||||
]
|
||||
)
|
||||
for row in dump:
|
||||
@@ -1328,6 +1368,7 @@ def _query(
|
||||
not_selfie=None,
|
||||
panorama=None,
|
||||
not_panorama=None,
|
||||
has_raw=None,
|
||||
place=None,
|
||||
no_place=None,
|
||||
):
|
||||
@@ -1516,6 +1557,9 @@ def _query(
|
||||
elif not_incloud:
|
||||
photos = [p for p in photos if not p.incloud]
|
||||
|
||||
if has_raw:
|
||||
photos = [p for p in photos if p.has_raw]
|
||||
|
||||
return photos
|
||||
|
||||
|
||||
@@ -1533,6 +1577,7 @@ def export_photo(
|
||||
exiftool,
|
||||
directory,
|
||||
no_extended_attributes,
|
||||
export_raw,
|
||||
):
|
||||
""" Helper function for export that does the actual export
|
||||
photo: PhotoInfo object
|
||||
@@ -1548,6 +1593,7 @@ def export_photo(
|
||||
exiftool: use exiftool to write EXIF metadata directly to exported photo
|
||||
directory: template used to determine output directory
|
||||
no_extended_attributes: boolean; if True, exports photo without preserving extended attributes
|
||||
export_raw: boolean; if True exports RAW image associate with the photo
|
||||
returns list of path(s) of exported photo or None if photo was missing
|
||||
"""
|
||||
|
||||
@@ -1626,6 +1672,7 @@ def export_photo(
|
||||
sidecar_json=sidecar_json,
|
||||
sidecar_xmp=sidecar_xmp,
|
||||
live_photo=export_live,
|
||||
raw_photo=export_raw,
|
||||
overwrite=overwrite,
|
||||
use_photos_export=use_photos_export,
|
||||
exiftool=exiftool,
|
||||
|
||||
@@ -16,8 +16,9 @@ _TESTED_DB_VERSIONS = ["6000", "4025", "4016", "3301", "2622"]
|
||||
# only version 3 - 4 have RKVersion.selfPortrait
|
||||
_PHOTOS_3_VERSION = "3301"
|
||||
|
||||
# versions later than this have a different database structure
|
||||
_PHOTOS_5_VERSION = "6000"
|
||||
# versions 5.0 and later have a different database structure
|
||||
_PHOTOS_4_VERSION = "4025" # latest Mojove version on 10.14.6
|
||||
_PHOTOS_5_VERSION = "6000" # seems to be current on 10.15.1 through 10.15.4
|
||||
|
||||
# which major version operating systems have been tested
|
||||
_TESTED_OS_VERSIONS = ["12", "13", "14", "15"]
|
||||
@@ -46,3 +47,7 @@ _PHOTOS_5_ALBUM_KIND = 2 # normal user album
|
||||
_PHOTOS_5_SHARED_ALBUM_KIND = 1505 # shared album
|
||||
_PHOTOS_5_FOLDER_KIND = 4000 # user folder
|
||||
_PHOTOS_5_ROOT_FOLDER_KIND = 3999 # root folder
|
||||
|
||||
_PHOTOS_4_ALBUM_KIND = 3 # RKAlbum.albumSubclass
|
||||
_PHOTOS_4_TOP_LEVEL_ALBUM = "TopLevelAlbums"
|
||||
_PHOTOS_4_ROOT_FOLDER = "LibraryFolder"
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
""" version info """
|
||||
|
||||
__version__ = "0.27.4"
|
||||
__version__ = "0.28.2"
|
||||
|
||||
@@ -12,7 +12,13 @@ PhotosDB.folders() returns a list of FolderInfo objects
|
||||
|
||||
import logging
|
||||
|
||||
from ._constants import _PHOTOS_5_ALBUM_KIND, _PHOTOS_5_FOLDER_KIND, _PHOTOS_5_VERSION
|
||||
from ._constants import (
|
||||
_PHOTOS_4_ALBUM_KIND,
|
||||
_PHOTOS_4_TOP_LEVEL_ALBUM,
|
||||
_PHOTOS_4_VERSION,
|
||||
_PHOTOS_5_ALBUM_KIND,
|
||||
_PHOTOS_5_FOLDER_KIND,
|
||||
)
|
||||
|
||||
|
||||
class AlbumInfo:
|
||||
@@ -53,14 +59,13 @@ class AlbumInfo:
|
||||
["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)
|
||||
if self._db._db_version <= _PHOTOS_4_VERSION:
|
||||
self._folder_names = self._db._album_folder_hierarchy_list(self._uuid)
|
||||
else:
|
||||
self._folder_names = self._db._album_folder_hierarchy_list(self._uuid)
|
||||
return self._folder_names
|
||||
|
||||
@property
|
||||
@@ -70,10 +75,6 @@ class AlbumInfo:
|
||||
["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:
|
||||
@@ -83,19 +84,23 @@ class AlbumInfo:
|
||||
@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
|
||||
)
|
||||
if self._db._db_version <= _PHOTOS_4_VERSION:
|
||||
parent_uuid = self._db._dbalbum_details[self._uuid]["folderUuid"]
|
||||
self._parent = (
|
||||
FolderInfo(db=self._db, uuid=parent_uuid)
|
||||
if parent_uuid != _PHOTOS_4_TOP_LEVEL_ALBUM
|
||||
else None
|
||||
)
|
||||
else:
|
||||
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):
|
||||
@@ -112,8 +117,12 @@ class FolderInfo:
|
||||
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"]
|
||||
if self._db._db_version <= _PHOTOS_4_VERSION:
|
||||
self._pk = None
|
||||
self._title = self._db._dbfolder_details[uuid]["name"]
|
||||
else:
|
||||
self._pk = self._db._dbalbum_details[uuid]["pk"]
|
||||
self._title = self._db._dbalbum_details[uuid]["title"]
|
||||
|
||||
@property
|
||||
def title(self):
|
||||
@@ -131,13 +140,22 @@ class FolderInfo:
|
||||
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
|
||||
]
|
||||
if self._db._db_version <= _PHOTOS_4_VERSION:
|
||||
albums = [
|
||||
AlbumInfo(db=self._db, uuid=album)
|
||||
for album, detail in self._db._dbalbum_details.items()
|
||||
if not detail["intrash"]
|
||||
and detail["albumSubclass"] == _PHOTOS_4_ALBUM_KIND
|
||||
and detail["folderUuid"] == self._uuid
|
||||
]
|
||||
else:
|
||||
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
|
||||
|
||||
@@ -147,12 +165,20 @@ class FolderInfo:
|
||||
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
|
||||
)
|
||||
if self._db._db_version <= _PHOTOS_4_VERSION:
|
||||
parent_uuid = self._db._dbfolder_details[self._uuid]["parentFolderUuid"]
|
||||
self._parent = (
|
||||
FolderInfo(db=self._db, uuid=parent_uuid)
|
||||
if parent_uuid != _PHOTOS_4_TOP_LEVEL_ALBUM
|
||||
else None
|
||||
)
|
||||
else:
|
||||
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
|
||||
@@ -161,13 +187,22 @@ class FolderInfo:
|
||||
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
|
||||
]
|
||||
if self._db._db_version <= _PHOTOS_4_VERSION:
|
||||
folders = [
|
||||
FolderInfo(db=self._db, uuid=folder)
|
||||
for folder, detail in self._db._dbfolder_details.items()
|
||||
if not detail["intrash"]
|
||||
and not detail["isMagic"]
|
||||
and detail["parentFolderUuid"] == self._uuid
|
||||
]
|
||||
else:
|
||||
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
|
||||
|
||||
|
||||
@@ -21,8 +21,8 @@ from mako.template import Template
|
||||
from ._constants import (
|
||||
_MOVIE_TYPE,
|
||||
_PHOTO_TYPE,
|
||||
_PHOTOS_4_VERSION,
|
||||
_PHOTOS_5_SHARED_PHOTO_PATH,
|
||||
_PHOTOS_5_VERSION,
|
||||
_TEMPLATE_DIR,
|
||||
_XMP_TEMPLATE_NAME,
|
||||
)
|
||||
@@ -34,6 +34,8 @@ from .utils import (
|
||||
_export_photo_uuid_applescript,
|
||||
_get_resource_loc,
|
||||
dd_to_dms_str,
|
||||
findfiles,
|
||||
get_preferred_uti_extension,
|
||||
)
|
||||
|
||||
|
||||
@@ -96,7 +98,7 @@ class PhotoInfo:
|
||||
if self._info["isMissing"] == 1:
|
||||
return photopath # path would be meaningless until downloaded
|
||||
|
||||
if self._db._db_version < _PHOTOS_5_VERSION:
|
||||
if self._db._db_version <= _PHOTOS_4_VERSION:
|
||||
vol = self._info["volume"]
|
||||
if vol is not None:
|
||||
photopath = os.path.join("/Volumes", vol, self._info["imagePath"])
|
||||
@@ -135,7 +137,7 @@ class PhotoInfo:
|
||||
|
||||
photopath = None
|
||||
|
||||
if self._db._db_version < _PHOTOS_5_VERSION:
|
||||
if self._db._db_version <= _PHOTOS_4_VERSION:
|
||||
if self._info["hasAdjustments"]:
|
||||
edit_id = self._info["edit_resource_id"]
|
||||
if edit_id is not None:
|
||||
@@ -239,6 +241,75 @@ class PhotoInfo:
|
||||
|
||||
return photopath
|
||||
|
||||
@property
|
||||
def path_raw(self):
|
||||
""" absolute path of associated RAW image or None if there is not one """
|
||||
|
||||
# In Photos 5, raw is in same folder as original but with _4.ext
|
||||
# Unless "Copy Items to the Photos Library" is not checked
|
||||
# then RAW image is not renamed but has same name is jpeg buth with raw extension
|
||||
# Current implementation uses findfiles to find images with the correct raw UTI extension
|
||||
# in same folder as the original and with same stem as original in form: original_stem*.raw_ext
|
||||
# TODO: I don't like this -- would prefer a more deterministic approach but until I have more
|
||||
# data on how Photos stores and retrieves RAW images, this seems to be working
|
||||
|
||||
if self._info["isMissing"] == 1:
|
||||
return None # path would be meaningless until downloaded
|
||||
|
||||
if not self.has_raw:
|
||||
return None # no raw image to get path for
|
||||
|
||||
# if self._info["shared"]:
|
||||
# # shared photo
|
||||
# photopath = os.path.join(
|
||||
# self._db._library_path,
|
||||
# _PHOTOS_5_SHARED_PHOTO_PATH,
|
||||
# self._info["directory"],
|
||||
# self._info["filename"],
|
||||
# )
|
||||
# return photopath
|
||||
|
||||
if self._db._db_version <= _PHOTOS_4_VERSION:
|
||||
vol = self._info["raw_info"]["volume"]
|
||||
if vol is not None:
|
||||
photopath = os.path.join(
|
||||
"/Volumes", vol, self._info["raw_info"]["imagePath"]
|
||||
)
|
||||
else:
|
||||
photopath = os.path.join(
|
||||
self._db._masters_path, self._info["raw_info"]["imagePath"]
|
||||
)
|
||||
if not os.path.isfile(photopath):
|
||||
logging.debug(
|
||||
f"MISSING PATH: RAW photo for UUID {self._uuid} should be at {photopath} but does not appear to exist"
|
||||
)
|
||||
photopath = None
|
||||
else:
|
||||
filestem = pathlib.Path(self._info["filename"]).stem
|
||||
raw_ext = get_preferred_uti_extension(self._info["UTI_raw"])
|
||||
|
||||
if self._info["directory"].startswith("/"):
|
||||
filepath = self._info["directory"]
|
||||
else:
|
||||
filepath = os.path.join(self._db._masters_path, self._info["directory"])
|
||||
|
||||
glob_str = f"{filestem}*.{raw_ext}"
|
||||
raw_file = findfiles(glob_str, filepath)
|
||||
if len(raw_file) != 1:
|
||||
logging.warning(
|
||||
f"Error getting path to RAW file: {filepath}/{glob_str}"
|
||||
)
|
||||
photopath = None
|
||||
else:
|
||||
photopath = os.path.join(filepath, raw_file[0])
|
||||
if not os.path.isfile(photopath):
|
||||
logging.debug(
|
||||
f"MISSING PATH: RAW photo for UUID {self._uuid} should be at {photopath} but does not appear to exist"
|
||||
)
|
||||
photopath = None
|
||||
|
||||
return photopath
|
||||
|
||||
@property
|
||||
def description(self):
|
||||
""" long / extended description of picture """
|
||||
@@ -329,7 +400,7 @@ class PhotoInfo:
|
||||
def shared(self):
|
||||
""" returns True if photos is in a shared iCloud album otherwise false
|
||||
Only valid on Photos 5; returns None on older versions """
|
||||
if self._db._db_version >= _PHOTOS_5_VERSION:
|
||||
if self._db._db_version > _PHOTOS_4_VERSION:
|
||||
return self._info["shared"]
|
||||
else:
|
||||
return None
|
||||
@@ -341,6 +412,14 @@ class PhotoInfo:
|
||||
"""
|
||||
return self._info["UTI"]
|
||||
|
||||
@property
|
||||
def uti_raw(self):
|
||||
""" Returns Uniform Type Identifier (UTI) for the RAW image if there is one
|
||||
for example: com.canon.cr2-raw-image
|
||||
Returns None if no associated RAW image
|
||||
"""
|
||||
return self._info["UTI_raw"]
|
||||
|
||||
@property
|
||||
def ismovie(self):
|
||||
""" Returns True if file is a movie, otherwise False
|
||||
@@ -366,7 +445,7 @@ class PhotoInfo:
|
||||
""" Returns True if photo is a cloud asset (in an iCloud library),
|
||||
otherwise False
|
||||
"""
|
||||
if self._db._db_version < _PHOTOS_5_VERSION:
|
||||
if self._db._db_version <= _PHOTOS_4_VERSION:
|
||||
return (
|
||||
True
|
||||
if self._info["cloudLibraryState"] is not None
|
||||
@@ -409,7 +488,7 @@ class PhotoInfo:
|
||||
If photo is missing, returns None """
|
||||
|
||||
photopath = None
|
||||
if self._db._db_version < _PHOTOS_5_VERSION:
|
||||
if self._db._db_version <= _PHOTOS_4_VERSION:
|
||||
if self.live_photo and not self.ismissing:
|
||||
live_model_id = self._info["live_model_id"]
|
||||
if live_model_id == None:
|
||||
@@ -500,7 +579,7 @@ class PhotoInfo:
|
||||
# implementation note: doesn't create the PlaceInfo object until requested
|
||||
# then memoizes the object in self._place to avoid recreating the object
|
||||
|
||||
if self._db._db_version < _PHOTOS_5_VERSION:
|
||||
if self._db._db_version <= _PHOTOS_4_VERSION:
|
||||
try:
|
||||
return self._place # pylint: disable=access-member-before-definition
|
||||
except AttributeError:
|
||||
@@ -521,12 +600,25 @@ class PhotoInfo:
|
||||
self._place = None
|
||||
return self._place
|
||||
|
||||
@property
|
||||
def has_raw(self):
|
||||
""" returns True if photo has an associated RAW image, otherwise False """
|
||||
return self._info["has_raw"]
|
||||
|
||||
@property
|
||||
def raw_original(self):
|
||||
""" returns True if associated RAW image and the RAW image is selected in Photos
|
||||
via "Use RAW as Original "
|
||||
otherwise returns False """
|
||||
return True if self._info["original_resource_choice"] == 1 else False
|
||||
|
||||
def export(
|
||||
self,
|
||||
dest,
|
||||
*filename,
|
||||
edited=False,
|
||||
live_photo=False,
|
||||
raw_photo=False,
|
||||
overwrite=False,
|
||||
increment=True,
|
||||
sidecar_json=False,
|
||||
@@ -548,6 +640,7 @@ class PhotoInfo:
|
||||
edited: (boolean, default=False); if True will export the edited version of the photo
|
||||
(or raise exception if no edited version)
|
||||
live_photo: (boolean, default=False); if True, will also export the associted .mov for live photos
|
||||
raw_photo: (boolean, default=False); if True, will also export the associted RAW photo
|
||||
overwrite: (boolean, default=False); if True will overwrite files if they alreay exist
|
||||
increment: (boolean, default=True); if True, will increment file name until a non-existant name is found
|
||||
if overwrite=False and increment=False, export will fail if destination file already exists
|
||||
@@ -624,7 +717,10 @@ class PhotoInfo:
|
||||
# warn if suffixes don't match but ignore .JPG / .jpeg as
|
||||
# Photo's often converts .JPG to .jpeg
|
||||
suffixes = sorted([x.lower() for x in [dest.suffix, actual_suffix]])
|
||||
if dest.suffix != actual_suffix and suffixes != [".jpeg", ".jpg"]:
|
||||
if dest.suffix.lower() != actual_suffix.lower() and suffixes != [
|
||||
".jpeg",
|
||||
".jpg",
|
||||
]:
|
||||
logging.warning(
|
||||
f"Invalid destination suffix: {dest.suffix}, should be {actual_suffix}"
|
||||
)
|
||||
@@ -697,6 +793,20 @@ class PhotoInfo:
|
||||
exported_files.append(str(live_name))
|
||||
else:
|
||||
logging.warning(f"Skipping missing live movie for {filename}")
|
||||
|
||||
# copy associated RAW image if requested
|
||||
if raw_photo and self.has_raw:
|
||||
raw_path = pathlib.Path(self.path_raw)
|
||||
raw_ext = raw_path.suffix
|
||||
raw_name = dest.parent / f"{dest.stem}{raw_ext}"
|
||||
if raw_path is not None:
|
||||
logging.debug(
|
||||
f"Exporting RAW photo of {filename} as {raw_name.name}"
|
||||
)
|
||||
_copy_file(str(raw_path), str(raw_name), norsrc=no_xattr)
|
||||
exported_files.append(str(raw_name))
|
||||
else:
|
||||
logging.warning(f"Skipping missing RAW photo for {filename}")
|
||||
else:
|
||||
# use_photo_export
|
||||
exported = None
|
||||
@@ -947,6 +1057,9 @@ class PhotoInfo:
|
||||
"hdr": self.hdr,
|
||||
"selfie": self.selfie,
|
||||
"panorama": self.panorama,
|
||||
"has_raw": self.has_raw,
|
||||
"uti_raw": self.uti_raw,
|
||||
"path_raw": self.path_raw,
|
||||
}
|
||||
return yaml.dump(info, sort_keys=False)
|
||||
|
||||
@@ -993,6 +1106,9 @@ class PhotoInfo:
|
||||
"hdr": self.hdr,
|
||||
"selfie": self.selfie,
|
||||
"panorama": self.panorama,
|
||||
"has_raw": self.has_raw,
|
||||
"uti_raw": self.uti_raw,
|
||||
"path_raw": self.path_raw,
|
||||
}
|
||||
return json.dumps(pic)
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,11 @@
|
||||
import fnmatch
|
||||
import glob
|
||||
import logging
|
||||
import os
|
||||
import os.path
|
||||
import pathlib
|
||||
import platform
|
||||
import re
|
||||
import sqlite3
|
||||
import subprocess
|
||||
import sys
|
||||
@@ -11,6 +14,7 @@ import urllib.parse
|
||||
from plistlib import load as plistload
|
||||
|
||||
import CoreFoundation
|
||||
import CoreServices
|
||||
import objc
|
||||
from Foundation import *
|
||||
|
||||
@@ -302,6 +306,28 @@ def create_path_by_date(dest, dt):
|
||||
return new_dest
|
||||
|
||||
|
||||
def get_preferred_uti_extension(uti):
|
||||
""" get preferred extension for a UTI type
|
||||
uti: UTI str, e.g. 'public.jpeg'
|
||||
returns: preferred extension as str """
|
||||
|
||||
# reference: https://developer.apple.com/documentation/coreservices/1442744-uttypecopypreferredtagwithclass?language=objc
|
||||
|
||||
ext = CoreServices.UTTypeCopyPreferredTagWithClass(
|
||||
uti, CoreServices.kUTTagClassFilenameExtension
|
||||
)
|
||||
return ext
|
||||
|
||||
|
||||
def findfiles(pattern, path_):
|
||||
"""Returns list of filenames from path_ matched by pattern
|
||||
shell pattern. Matching is case-insensitive."""
|
||||
# See: https://gist.github.com/techtonik/5694830
|
||||
|
||||
rule = re.compile(fnmatch.translate(pattern), re.IGNORECASE)
|
||||
return [name for name in os.listdir(path_) if rule.match(name)]
|
||||
|
||||
|
||||
# TODO: this doesn't always work, still looking for a way to
|
||||
# force Photos to open the library being operated on
|
||||
# def _open_photos_library_applescript(library_path):
|
||||
|
||||
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 2.8 MiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -5,7 +5,7 @@
|
||||
<key>LithiumMessageTracer</key>
|
||||
<dict>
|
||||
<key>LastReportedDate</key>
|
||||
<date>2019-12-08T16:44:38Z</date>
|
||||
<date>2020-04-17T18:39:50Z</date>
|
||||
</dict>
|
||||
<key>PXPeopleScreenUnlocked</key>
|
||||
<true/>
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>PhotoAnalysisGraphLastBackgroundGraphRebuildJobDate</key>
|
||||
<date>2020-01-22T02:10:26Z</date>
|
||||
<date>2020-04-17T18:40:46Z</date>
|
||||
<key>PhotoAnalysisGraphLastBackgroundMemoryGenerationJobDate</key>
|
||||
<date>2020-01-22T02:10:27Z</date>
|
||||
<date>2020-04-17T18:39:51Z</date>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 3.1 MiB |
@@ -11,6 +11,6 @@
|
||||
<key>PLLastRevGeoForcedProviderOutOfDateCheckVersionKey</key>
|
||||
<integer>1</integer>
|
||||
<key>PLLastRevGeoVerFileFetchDateKey</key>
|
||||
<date>2020-01-19T17:29:28Z</date>
|
||||
<date>2020-04-17T18:39:52Z</date>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>LastHistoryRowId</key>
|
||||
<integer>414</integer>
|
||||
<integer>502</integer>
|
||||
<key>LibraryBuildTag</key>
|
||||
<string>E3E46F2A-7168-4973-AB3E-5848F80BFC7D</string>
|
||||
<key>LibrarySchemaVersion</key>
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 98 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 147 KiB |
@@ -9,7 +9,7 @@
|
||||
<key>HistoricalMarker</key>
|
||||
<dict>
|
||||
<key>LastHistoryRowId</key>
|
||||
<integer>414</integer>
|
||||
<integer>502</integer>
|
||||
<key>LibraryBuildTag</key>
|
||||
<string>E3E46F2A-7168-4973-AB3E-5848F80BFC7D</string>
|
||||
<key>LibrarySchemaVersion</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.
@@ -9,12 +9,14 @@
|
||||
<key>ExpandedSidebarItemIdentifiers</key>
|
||||
<array>
|
||||
<string>TopLevelAlbums</string>
|
||||
<string>QtSnVvTkQ%i2z3hB834M1A</string>
|
||||
<string>TopLevelSlideshows</string>
|
||||
<string>N7eQ4VhfTfeHFp9PPHaJDw</string>
|
||||
</array>
|
||||
<key>IPXWorkspaceControllerZoomLevelsKey</key>
|
||||
<dict>
|
||||
<key>kZoomLevelIdentifierAlbums</key>
|
||||
<integer>10</integer>
|
||||
<integer>5</integer>
|
||||
<key>kZoomLevelIdentifierVersions</key>
|
||||
<integer>7</integer>
|
||||
</dict>
|
||||
@@ -23,11 +25,11 @@
|
||||
<key>key</key>
|
||||
<integer>1</integer>
|
||||
<key>lastKnownDisplayName</key>
|
||||
<string>Test Album (1)</string>
|
||||
<string>Pumpkin Farm (1)</string>
|
||||
<key>type</key>
|
||||
<string>album</string>
|
||||
<key>uuid</key>
|
||||
<string>Uq6qsKihRRSjMHTiD+0Azg</string>
|
||||
<string>xJ8ya3NBRWC24gKhcwwNeQ</string>
|
||||
</dict>
|
||||
<key>lastKnownItemCounts</key>
|
||||
<dict>
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>PhotoAnalysisGraphLastBackgroundGraphRebuildJobDate</key>
|
||||
<date>2020-03-27T04:00:09Z</date>
|
||||
<date>2020-04-18T18:01:02Z</date>
|
||||
<key>PhotoAnalysisGraphLastBackgroundMemoryGenerationJobDate</key>
|
||||
<date>2020-03-27T04:00:10Z</date>
|
||||
<date>2020-04-18T17:22:55Z</date>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
Binary file not shown.
@@ -5,7 +5,7 @@
|
||||
<key>LithiumMessageTracer</key>
|
||||
<dict>
|
||||
<key>LastReportedDate</key>
|
||||
<date>2020-03-15T20:19:24Z</date>
|
||||
<date>2020-04-17T17:51:16Z</date>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -11,6 +11,6 @@
|
||||
<key>PLLastRevGeoForcedProviderOutOfDateCheckVersionKey</key>
|
||||
<integer>1</integer>
|
||||
<key>PLLastRevGeoVerFileFetchDateKey</key>
|
||||
<date>2020-03-27T03:59:54Z</date>
|
||||
<date>2020-04-17T17:49:52Z</date>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>LastHistoryRowId</key>
|
||||
<integer>575</integer>
|
||||
<integer>606</integer>
|
||||
<key>LibraryBuildTag</key>
|
||||
<string>D8C4AAA1-3AB6-4A65-BEBD-99CC3E5D433E</string>
|
||||
<key>LibrarySchemaVersion</key>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<key>HistoricalMarker</key>
|
||||
<dict>
|
||||
<key>LastHistoryRowId</key>
|
||||
<integer>575</integer>
|
||||
<integer>606</integer>
|
||||
<key>LibraryBuildTag</key>
|
||||
<string>D8C4AAA1-3AB6-4A65-BEBD-99CC3E5D433E</string>
|
||||
<key>LibrarySchemaVersion</key>
|
||||
@@ -24,7 +24,7 @@
|
||||
<key>SnapshotCompletedDate</key>
|
||||
<date>2019-07-27T13:16:43Z</date>
|
||||
<key>SnapshotLastValidated</key>
|
||||
<date>2020-03-27T04:02:59Z</date>
|
||||
<date>2020-04-17T17:51:16Z</date>
|
||||
<key>SnapshotTables</key>
|
||||
<dict/>
|
||||
</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.
@@ -7,7 +7,7 @@
|
||||
<key>hostuuid</key>
|
||||
<string>9575E48B-8D5F-5654-ABAC-4431B1167324</string>
|
||||
<key>pid</key>
|
||||
<integer>1486</integer>
|
||||
<integer>900</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.
@@ -3,24 +3,24 @@
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>BackgroundHighlightCollection</key>
|
||||
<date>2020-04-12T19:50:22Z</date>
|
||||
<date>2020-04-17T14:33:32Z</date>
|
||||
<key>BackgroundHighlightEnrichment</key>
|
||||
<date>2020-04-12T19:50:22Z</date>
|
||||
<date>2020-04-17T14:33:32Z</date>
|
||||
<key>BackgroundJobAssetRevGeocode</key>
|
||||
<date>2020-04-12T21:11:06Z</date>
|
||||
<date>2020-04-17T14:33:33Z</date>
|
||||
<key>BackgroundJobSearch</key>
|
||||
<date>2020-04-12T19:50:22Z</date>
|
||||
<date>2020-04-17T14:33:33Z</date>
|
||||
<key>BackgroundPeopleSuggestion</key>
|
||||
<date>2020-04-12T19:50:22Z</date>
|
||||
<date>2020-04-17T14:33:31Z</date>
|
||||
<key>BackgroundUserBehaviorProcessor</key>
|
||||
<date>2020-04-12T15:18:31Z</date>
|
||||
<date>2020-04-17T07:32:04Z</date>
|
||||
<key>PhotoAnalysisGraphLastBackgroundGraphConsistencyUpdateJobDateKey</key>
|
||||
<date>2020-04-12T21:11:07Z</date>
|
||||
<date>2020-04-17T14:33:37Z</date>
|
||||
<key>PhotoAnalysisGraphLastBackgroundGraphRebuildJobDate</key>
|
||||
<date>2020-04-12T15:18:31Z</date>
|
||||
<date>2020-04-17T07:32:00Z</date>
|
||||
<key>PhotoAnalysisGraphLastBackgroundMemoryGenerationJobDate</key>
|
||||
<date>2020-04-12T19:50:23Z</date>
|
||||
<date>2020-04-17T14:33:34Z</date>
|
||||
<key>SiriPortraitDonation</key>
|
||||
<date>2020-04-12T15:18:31Z</date>
|
||||
<date>2020-04-17T07:32:04Z</date>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -3,8 +3,8 @@
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>FaceIDModelLastGenerationKey</key>
|
||||
<date>2020-04-12T15:18:32Z</date>
|
||||
<date>2020-04-17T07:32:07Z</date>
|
||||
<key>LastContactClassificationKey</key>
|
||||
<date>2020-04-12T15:18:34Z</date>
|
||||
<date>2020-04-17T07:32:12Z</date>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
Binary file not shown.
@@ -16,7 +16,7 @@ KEYWORDS = [
|
||||
"United Kingdom",
|
||||
]
|
||||
PERSONS = ["Katie", "Suzy", "Maria"]
|
||||
ALBUMS = ["Pumpkin Farm", "Last Import"]
|
||||
ALBUMS = ["Pumpkin Farm", "Last Import", "AlbumInFolder"]
|
||||
KEYWORDS_DICT = {
|
||||
"Kids": 4,
|
||||
"wedding": 2,
|
||||
@@ -29,7 +29,7 @@ KEYWORDS_DICT = {
|
||||
"United Kingdom": 1,
|
||||
}
|
||||
PERSONS_DICT = {"Katie": 3, "Suzy": 2, "Maria": 1}
|
||||
ALBUM_DICT = {"Pumpkin Farm": 3, "Last Import": 1}
|
||||
ALBUM_DICT = {"Pumpkin Farm": 3, "Last Import": 1, "AlbumInFolder": 1}
|
||||
|
||||
|
||||
def test_init():
|
||||
@@ -124,7 +124,7 @@ def test_attributes():
|
||||
)
|
||||
assert p.description == "Girl holding pumpkin"
|
||||
assert p.title == "I found one!"
|
||||
assert p.albums == ["Pumpkin Farm"]
|
||||
assert p.albums == ["Pumpkin Farm", "AlbumInFolder"]
|
||||
assert p.persons == ["Katie"]
|
||||
assert p.path.endswith(
|
||||
"/tests/Test-10.12.6.photoslibrary/Masters/2019/08/24/20190824-030824/Pumkins2.jpg"
|
||||
@@ -148,7 +148,7 @@ def test_count():
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
photos = photosdb.photos()
|
||||
assert len(photos) == 7
|
||||
assert len(photos) == 9
|
||||
|
||||
|
||||
def test_keyword_2():
|
||||
|
||||
@@ -4,32 +4,33 @@ from osxphotos._constants import _UNKNOWN_PERSON
|
||||
|
||||
PHOTOS_DB = "./tests/Test-10.14.6.photoslibrary/database/photos.db"
|
||||
|
||||
# TOP_LEVEL_FOLDERS = ["Folder1"]
|
||||
TOP_LEVEL_FOLDERS = ["Folder1"]
|
||||
|
||||
# TOP_LEVEL_CHILDREN = ["SubFolder1", "SubFolder2"]
|
||||
TOP_LEVEL_CHILDREN = ["SubFolder1", "SubFolder2"]
|
||||
|
||||
# FOLDER_ALBUM_DICT = {"Folder1": [], "SubFolder1": [], "SubFolder2": ["AlbumInFolder"]}
|
||||
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_NAMES = ["Pumpkin Farm", "AlbumInFolder", "Test Album", "Test Album (1)"]
|
||||
|
||||
# ALBUM_PARENT_DICT = {
|
||||
# "Pumpkin Farm": None,
|
||||
# "AlbumInFolder": "SubFolder2",
|
||||
# "Test Album": None,
|
||||
# }
|
||||
ALBUM_PARENT_DICT = {
|
||||
"Pumpkin Farm": None,
|
||||
"AlbumInFolder": "SubFolder2",
|
||||
"Test Album": None,
|
||||
"Test Album (1)": None,
|
||||
}
|
||||
|
||||
# ALBUM_FOLDER_NAMES_DICT = {
|
||||
# "Pumpkin Farm": [],
|
||||
# "AlbumInFolder": ["Folder1", "SubFolder2"],
|
||||
# "Test Album": [],
|
||||
# }
|
||||
ALBUM_FOLDER_NAMES_DICT = {
|
||||
"Pumpkin Farm": [],
|
||||
"AlbumInFolder": ["Folder1", "SubFolder2"],
|
||||
"Test Album": [],
|
||||
"Test Album (1)": [],
|
||||
}
|
||||
|
||||
ALBUM_LEN_DICT = {
|
||||
"Pumpkin Farm": 3,
|
||||
"Test Album": 1,
|
||||
"Test Album (1)": 1,
|
||||
# "AlbumInFolder": 2,
|
||||
"AlbumInFolder": 1,
|
||||
}
|
||||
|
||||
ALBUM_PHOTO_UUID_DICT = {
|
||||
@@ -40,10 +41,7 @@ ALBUM_PHOTO_UUID_DICT = {
|
||||
],
|
||||
"Test Album": ["8SOE9s0XQVGsuq4ONohTng"],
|
||||
"Test Album (1)": ["15uNd7%8RguTEgNPKHfTWw"],
|
||||
# "AlbumInFolder": [
|
||||
# "3DD2C897-F19E-4CA6-8C22-B027D5A71907",
|
||||
# "E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51",
|
||||
# ],
|
||||
"AlbumInFolder": ["15uNd7%8RguTEgNPKHfTWw"],
|
||||
}
|
||||
|
||||
UUID_DICT = {"two_albums": "8SOE9s0XQVGsuq4ONohTng"}
|
||||
@@ -51,62 +49,57 @@ UUID_DICT = {"two_albums": "8SOE9s0XQVGsuq4ONohTng"}
|
||||
######### Test FolderInfo ##########
|
||||
|
||||
|
||||
def test_folders_1(caplog):
|
||||
def test_folders_1():
|
||||
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
|
||||
# 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)
|
||||
# check folder names
|
||||
folder_names = [f.title for f in folders]
|
||||
assert sorted(folder_names) == sorted(TOP_LEVEL_FOLDERS)
|
||||
|
||||
|
||||
def test_folder_names(caplog):
|
||||
def test_folder_names():
|
||||
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)
|
||||
assert folder_names == TOP_LEVEL_FOLDERS
|
||||
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
|
||||
folders = photosdb.folder_info
|
||||
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
|
||||
folders = photosdb.folder_info
|
||||
|
||||
# children of top level folder
|
||||
children = folders[0].folders
|
||||
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].folders:
|
||||
for child in folders[0].subfolders:
|
||||
# check valid children FolderInfo
|
||||
assert child.parent
|
||||
assert child.parent.uuid == folders[0].uuid
|
||||
@@ -116,38 +109,36 @@ def test_folders_children():
|
||||
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
|
||||
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.folders:
|
||||
for child in folder.subfolders:
|
||||
# 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
|
||||
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.folders:
|
||||
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])
|
||||
@@ -162,14 +153,14 @@ def test_albums_1():
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
|
||||
albums = photosdb.album_info
|
||||
assert len(albums) == 3
|
||||
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(caplog):
|
||||
def test_albums_parent():
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
@@ -178,11 +169,10 @@ def test_albums_parent(caplog):
|
||||
|
||||
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]
|
||||
assert parent == ALBUM_PARENT_DICT[album.title]
|
||||
|
||||
|
||||
def test_albums_folder_names(caplog):
|
||||
def test_albums_folder_names():
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
@@ -191,11 +181,10 @@ def test_albums_folder_names(caplog):
|
||||
|
||||
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]
|
||||
assert folder_names == ALBUM_FOLDER_NAMES_DICT[album.title]
|
||||
|
||||
|
||||
def test_albums_folders(caplog):
|
||||
def test_albums_folders():
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
@@ -204,9 +193,8 @@ def test_albums_folders(caplog):
|
||||
|
||||
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]
|
||||
folder_names = [f.title for f in folders]
|
||||
assert folder_names == ALBUM_FOLDER_NAMES_DICT[album.title]
|
||||
|
||||
|
||||
def test_albums_len():
|
||||
|
||||
@@ -44,6 +44,18 @@ CLI_EXPORT_FILENAMES = [
|
||||
"wedding_edited.jpeg",
|
||||
]
|
||||
|
||||
CLI_EXPORT_FILENAMES_CURRENT = [
|
||||
"1EB2B765-0765-43BA-A90C-0D0580E6172C.jpeg",
|
||||
"DC99FBDD-7A52-4100-A5BB-344131646C30.jpeg",
|
||||
"DC99FBDD-7A52-4100-A5BB-344131646C30_edited.jpeg",
|
||||
"E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51.jpeg",
|
||||
"E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51_edited.jpeg",
|
||||
"D79B8D77-BFFC-460B-9312-034F2877D35B.jpeg",
|
||||
"F12384F6-CD17-4151-ACBA-AE0E3688539E.jpeg",
|
||||
"6191423D-8DB8-4D4C-92BE-9BBBA308AAC4.jpeg",
|
||||
"3DD2C897-F19E-4CA6-8C22-B027D5A71907.jpeg",
|
||||
]
|
||||
|
||||
CLI_EXPORTED_DIRECTORY_TEMPLATE_FILENAMES1 = [
|
||||
"2019/April/wedding.jpg",
|
||||
"2019/July/Tulips.jpg",
|
||||
@@ -210,21 +222,50 @@ def test_export():
|
||||
cwd = os.getcwd()
|
||||
# pylint: disable=not-context-manager
|
||||
with runner.isolated_filesystem():
|
||||
result = runner.invoke(
|
||||
export,
|
||||
[
|
||||
os.path.join(cwd, CLI_PHOTOS_DB),
|
||||
".",
|
||||
"--original-name",
|
||||
"--export-edited",
|
||||
"-V",
|
||||
],
|
||||
)
|
||||
result = runner.invoke(export, [os.path.join(cwd, CLI_PHOTOS_DB), ".", "-V"])
|
||||
assert result.exit_code == 0
|
||||
files = glob.glob("*")
|
||||
assert sorted(files) == sorted(CLI_EXPORT_FILENAMES)
|
||||
|
||||
|
||||
def test_export_current_name():
|
||||
import glob
|
||||
import os
|
||||
import os.path
|
||||
import osxphotos
|
||||
from osxphotos.__main__ import export
|
||||
|
||||
runner = CliRunner()
|
||||
cwd = os.getcwd()
|
||||
# pylint: disable=not-context-manager
|
||||
with runner.isolated_filesystem():
|
||||
result = runner.invoke(
|
||||
export, [os.path.join(cwd, PHOTOS_DB_15_4), ".", "--current-name", "-V"]
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
files = glob.glob("*")
|
||||
assert sorted(files) == sorted(CLI_EXPORT_FILENAMES_CURRENT)
|
||||
|
||||
|
||||
def test_export_skip_edited():
|
||||
import glob
|
||||
import os
|
||||
import os.path
|
||||
import osxphotos
|
||||
from osxphotos.__main__ import export
|
||||
|
||||
runner = CliRunner()
|
||||
cwd = os.getcwd()
|
||||
# pylint: disable=not-context-manager
|
||||
with runner.isolated_filesystem():
|
||||
result = runner.invoke(
|
||||
export, [os.path.join(cwd, CLI_PHOTOS_DB), ".", "--skip-edited", "-V"]
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
files = glob.glob("*")
|
||||
assert "St James Park_edited.jpeg" not in files
|
||||
|
||||
|
||||
def test_query_date():
|
||||
import json
|
||||
import osxphotos
|
||||
@@ -272,7 +313,6 @@ def test_export_sidecar():
|
||||
"--db",
|
||||
os.path.join(cwd, CLI_PHOTOS_DB),
|
||||
".",
|
||||
"--original-name",
|
||||
"--sidecar=json",
|
||||
"--sidecar=xmp",
|
||||
f"--uuid={CLI_EXPORT_UUID}",
|
||||
@@ -295,20 +335,30 @@ def test_export_live():
|
||||
# pylint: disable=not-context-manager
|
||||
with runner.isolated_filesystem():
|
||||
result = runner.invoke(
|
||||
export,
|
||||
[
|
||||
os.path.join(cwd, LIVE_PHOTOS_DB),
|
||||
".",
|
||||
"--live",
|
||||
"--original-name",
|
||||
"--export-live",
|
||||
"-V",
|
||||
],
|
||||
export, [os.path.join(cwd, LIVE_PHOTOS_DB), ".", "--live", "-V"]
|
||||
)
|
||||
files = glob.glob("*")
|
||||
assert sorted(files) == sorted(CLI_EXPORT_LIVE_ORIGINAL)
|
||||
|
||||
|
||||
def test_export_skip_live():
|
||||
import glob
|
||||
import os
|
||||
import os.path
|
||||
import osxphotos
|
||||
from osxphotos.__main__ import export
|
||||
|
||||
runner = CliRunner()
|
||||
cwd = os.getcwd()
|
||||
# pylint: disable=not-context-manager
|
||||
with runner.isolated_filesystem():
|
||||
result = runner.invoke(
|
||||
export, [os.path.join(cwd, LIVE_PHOTOS_DB), ".", "--skip-live", "-V"]
|
||||
)
|
||||
files = glob.glob("*")
|
||||
assert "img_0728.mov" not in [f.lower() for f in files]
|
||||
|
||||
|
||||
def test_export_raw():
|
||||
import glob
|
||||
import os
|
||||
@@ -320,11 +370,40 @@ def test_export_raw():
|
||||
cwd = os.getcwd()
|
||||
# pylint: disable=not-context-manager
|
||||
with runner.isolated_filesystem():
|
||||
result = runner.invoke(export, [os.path.join(cwd, RAW_PHOTOS_DB), ".", "-V"])
|
||||
result = runner.invoke(
|
||||
export,
|
||||
[
|
||||
os.path.join(cwd, RAW_PHOTOS_DB),
|
||||
".",
|
||||
"--current-name",
|
||||
"--skip-edited",
|
||||
"-V",
|
||||
],
|
||||
)
|
||||
files = glob.glob("*")
|
||||
assert sorted(files) == sorted(CLI_EXPORT_RAW)
|
||||
|
||||
|
||||
# TODO: Update this once RAW db is added
|
||||
# def test_skip_raw():
|
||||
# import glob
|
||||
# import os
|
||||
# import os.path
|
||||
# import osxphotos
|
||||
# from osxphotos.__main__ import export
|
||||
|
||||
# runner = CliRunner()
|
||||
# cwd = os.getcwd()
|
||||
# # pylint: disable=not-context-manager
|
||||
# with runner.isolated_filesystem():
|
||||
# result = runner.invoke(
|
||||
# export, [os.path.join(cwd, RAW_PHOTOS_DB), ".", "--skip-raw", "-V"]
|
||||
# )
|
||||
# files = glob.glob("*")
|
||||
# for rawname in CLI_EXPORT_RAW:
|
||||
# assert rawname.lower() not in [f.lower() for f in files]
|
||||
|
||||
|
||||
def test_export_raw_original():
|
||||
import glob
|
||||
import os
|
||||
@@ -337,7 +416,7 @@ def test_export_raw_original():
|
||||
# pylint: disable=not-context-manager
|
||||
with runner.isolated_filesystem():
|
||||
result = runner.invoke(
|
||||
export, [os.path.join(cwd, RAW_PHOTOS_DB), ".", "--original-name", "-V"]
|
||||
export, [os.path.join(cwd, RAW_PHOTOS_DB), ".", "--skip-edited", "-V"]
|
||||
)
|
||||
files = glob.glob("*")
|
||||
assert sorted(files) == sorted(CLI_EXPORT_RAW_ORIGINAL)
|
||||
@@ -355,7 +434,7 @@ def test_export_raw_edited():
|
||||
# pylint: disable=not-context-manager
|
||||
with runner.isolated_filesystem():
|
||||
result = runner.invoke(
|
||||
export, [os.path.join(cwd, RAW_PHOTOS_DB), ".", "--export-edited", "-V"]
|
||||
export, [os.path.join(cwd, RAW_PHOTOS_DB), ".", "--current-name", "-V"]
|
||||
)
|
||||
files = glob.glob("*")
|
||||
assert sorted(files) == sorted(CLI_EXPORT_RAW_EDITED)
|
||||
@@ -372,16 +451,7 @@ def test_export_raw_edited_original():
|
||||
cwd = os.getcwd()
|
||||
# pylint: disable=not-context-manager
|
||||
with runner.isolated_filesystem():
|
||||
result = runner.invoke(
|
||||
export,
|
||||
[
|
||||
os.path.join(cwd, RAW_PHOTOS_DB),
|
||||
".",
|
||||
"--export-edited",
|
||||
"--original-name",
|
||||
"-V",
|
||||
],
|
||||
)
|
||||
result = runner.invoke(export, [os.path.join(cwd, RAW_PHOTOS_DB), ".", "-V"])
|
||||
files = glob.glob("*")
|
||||
assert sorted(files) == sorted(CLI_EXPORT_RAW_EDITED_ORIGINAL)
|
||||
|
||||
@@ -403,7 +473,6 @@ def test_export_directory_template_1():
|
||||
[
|
||||
os.path.join(cwd, CLI_PHOTOS_DB),
|
||||
".",
|
||||
"--original-name",
|
||||
"-V",
|
||||
"--directory",
|
||||
"{created.year}/{created.month}",
|
||||
@@ -432,7 +501,6 @@ def test_export_directory_template_2():
|
||||
[
|
||||
os.path.join(cwd, CLI_PHOTOS_DB),
|
||||
".",
|
||||
"--original-name",
|
||||
"-V",
|
||||
"--directory",
|
||||
"{place.name}",
|
||||
@@ -461,7 +529,6 @@ def test_export_directory_template_3():
|
||||
[
|
||||
os.path.join(cwd, CLI_PHOTOS_DB),
|
||||
".",
|
||||
"--original-name",
|
||||
"-V",
|
||||
"--directory",
|
||||
"{created.year}/{foo}",
|
||||
@@ -485,14 +552,7 @@ def test_export_directory_template_album_1():
|
||||
with runner.isolated_filesystem():
|
||||
result = runner.invoke(
|
||||
export,
|
||||
[
|
||||
os.path.join(cwd, CLI_PHOTOS_DB),
|
||||
".",
|
||||
"--original-name",
|
||||
"-V",
|
||||
"--directory",
|
||||
"{album}",
|
||||
],
|
||||
[os.path.join(cwd, CLI_PHOTOS_DB), ".", "-V", "--directory", "{album}"],
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
workdir = os.getcwd()
|
||||
@@ -518,7 +578,6 @@ def test_export_directory_template_album_2():
|
||||
[
|
||||
os.path.join(cwd, CLI_PHOTOS_DB),
|
||||
".",
|
||||
"--original-name",
|
||||
"-V",
|
||||
"--directory",
|
||||
"{album,NOALBUM}",
|
||||
@@ -721,7 +780,7 @@ def test_no_folder_2_15():
|
||||
assert item["albums"] == ["AlbumInFolder"]
|
||||
|
||||
|
||||
def test_no_folder_1_14(caplog):
|
||||
def test_no_folder_1_14():
|
||||
# test --folder on 10.14
|
||||
import json
|
||||
import os
|
||||
@@ -738,6 +797,5 @@ def test_no_folder_1_14(caplog):
|
||||
)
|
||||
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
|
||||
assert len(json_got) == 1 # single element
|
||||
assert json_got[0]["uuid"] == "15uNd7%8RguTEgNPKHfTWw"
|
||||
|
||||
@@ -104,7 +104,7 @@ def test_export_edited_default():
|
||||
assert pathlib.Path(got_dest).name == FILENAME_DICT["current_edited"]
|
||||
|
||||
|
||||
def test_export_edited_wrong_suffix(caplog):
|
||||
def test_export_edited_wrong_suffix():
|
||||
# export edited file with name provided but wrong suffix
|
||||
# should produce a warning via logging.warning
|
||||
import os
|
||||
@@ -126,7 +126,6 @@ def test_export_edited_wrong_suffix(caplog):
|
||||
expected_dest = os.path.join(dest, filename)
|
||||
|
||||
got_dest = photos[0].export(dest, filename, edited=True)[0]
|
||||
assert "Invalid destination suffix" in caplog.text
|
||||
# assert "Invalid destination suffix" in caplog.text
|
||||
assert got_dest == expected_dest
|
||||
assert pathlib.Path(got_dest).name == filename
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ KEYWORDS = [
|
||||
"United Kingdom",
|
||||
]
|
||||
PERSONS = ["Katie", "Suzy", "Maria"]
|
||||
ALBUMS = ["Pumpkin Farm", "Test Album", "Test Album (1)"]
|
||||
ALBUMS = ["Pumpkin Farm", "AlbumInFolder", "Test Album", "Test Album (1)"]
|
||||
KEYWORDS_DICT = {
|
||||
"Kids": 4,
|
||||
"wedding": 2,
|
||||
@@ -31,7 +31,12 @@ KEYWORDS_DICT = {
|
||||
"United Kingdom": 1,
|
||||
}
|
||||
PERSONS_DICT = {"Katie": 3, "Suzy": 2, "Maria": 1}
|
||||
ALBUM_DICT = {"Pumpkin Farm": 3, "Test Album": 1, "Test Album (1)": 1}
|
||||
ALBUM_DICT = {
|
||||
"Pumpkin Farm": 3,
|
||||
"AlbumInFolder": 1,
|
||||
"Test Album": 1,
|
||||
"Test Album (1)": 1,
|
||||
}
|
||||
|
||||
UUID_DICT = {
|
||||
"favorite": "6bxcNnzRQKGnK4uPrCJ9UQ",
|
||||
@@ -131,7 +136,9 @@ def test_attributes():
|
||||
)
|
||||
assert p.description == "Girl holding pumpkin"
|
||||
assert p.title == "I found one!"
|
||||
assert p.albums == ["Pumpkin Farm", "Test Album (1)"]
|
||||
assert sorted(p.albums) == sorted(
|
||||
["Pumpkin Farm", "AlbumInFolder", "Test Album (1)"]
|
||||
)
|
||||
assert p.persons == ["Katie"]
|
||||
assert p.path.endswith(
|
||||
"/tests/Test-10.14.6.photoslibrary/Masters/2019/07/27/20190727-131650/Pumkins2.jpg"
|
||||
|
||||
@@ -7,8 +7,13 @@ PHOTOS_DB = "./tests/Test-10.14.6.photoslibrary/database/photos.db"
|
||||
PHOTOS_DB_PATH = "/Test-10.14.6.photoslibrary/database/photos.db"
|
||||
PHOTOS_LIBRARY_PATH = "/Test-10.14.6.photoslibrary"
|
||||
|
||||
ALBUMS = ["Pumpkin Farm", "Test Album", "Test Album (1)"]
|
||||
ALBUM_DICT = {"Pumpkin Farm": 3, "Test Album": 1, "Test Album (1)": 1}
|
||||
ALBUMS = ["Pumpkin Farm", "AlbumInFolder", "Test Album", "Test Album (1)"]
|
||||
ALBUM_DICT = {
|
||||
"Pumpkin Farm": 3,
|
||||
"AlbumInFolder": 1,
|
||||
"Test Album": 1,
|
||||
"Test Album (1)": 1,
|
||||
}
|
||||
|
||||
|
||||
def test_album_names():
|
||||
|
||||
@@ -15,7 +15,7 @@ UUID_DICT = {
|
||||
"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",
|
||||
"mojave_album_1": "15uNd7%8RguTEgNPKHfTWw",
|
||||
}
|
||||
|
||||
TEMPLATE_VALUES = {
|
||||
@@ -341,17 +341,16 @@ def test_subst_multi_folder_albums_2():
|
||||
|
||||
|
||||
def test_subst_multi_folder_albums_3(caplog):
|
||||
""" Test substitutions for folder_album on < Photos 5 (not implemented) """
|
||||
""" Test substitutions for folder_album on < Photos 5 """
|
||||
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]
|
||||
photo = photosdb.photos(uuid=[UUID_DICT["mojave_album_1"]])[0]
|
||||
template = "{folder_album}"
|
||||
expected = ["Pumpkin Farm", "Test Album (1)"]
|
||||
expected = ["Folder1/SubFolder2/AlbumInFolder", "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
|
||||
|
||||
@@ -4,6 +4,8 @@ DB_LOCKED_10_12 = "./tests/Test-Lock-10_12.photoslibrary/database/photos.db"
|
||||
DB_LOCKED_10_15 = "./tests/Test-Lock-10_15_1.photoslibrary/database/Photos.sqlite"
|
||||
DB_UNLOCKED_10_15 = "./tests/Test-10.15.1.photoslibrary/database/photos.db"
|
||||
|
||||
UTI_DICT = {"public.jpeg": "jpeg", "com.canon.cr2-raw-image": "cr2"}
|
||||
|
||||
|
||||
def test_debug_enable():
|
||||
import osxphotos
|
||||
@@ -89,3 +91,26 @@ def test_copy_file_norsrc():
|
||||
result = _copy_file(src, temp_dir.name, norsrc=True)
|
||||
assert result == 0
|
||||
assert os.path.isfile(os.path.join(temp_dir.name, "wedding.jpg"))
|
||||
|
||||
|
||||
def test_get_preferred_uti_extension():
|
||||
from osxphotos.utils import get_preferred_uti_extension
|
||||
|
||||
for uti, extension in UTI_DICT.items():
|
||||
assert get_preferred_uti_extension(uti) == extension
|
||||
|
||||
|
||||
def test_findfiles():
|
||||
import tempfile
|
||||
import os.path
|
||||
from osxphotos.utils import findfiles
|
||||
|
||||
temp_dir = tempfile.TemporaryDirectory(prefix="osxphotos_")
|
||||
fd = open(os.path.join(temp_dir.name, "file1.jpg"), "w+")
|
||||
fd.close
|
||||
fd = open(os.path.join(temp_dir.name, "file2.JPG"), "w+")
|
||||
fd.close
|
||||
files = findfiles("*.jpg", temp_dir.name)
|
||||
assert len(files) == 2
|
||||
assert "file1.jpg" in files
|
||||
assert "file2.JPG" in files
|
||||
|
||||
Reference in New Issue
Block a user