Merge pull request #1 from RhetTbull/master

Jan 20 Updates
This commit is contained in:
hshore29
2020-01-21 22:03:07 -05:00
committed by GitHub
9 changed files with 678 additions and 660 deletions

View File

@@ -4,6 +4,18 @@ 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.22.4](https://github.com/RhetTbull/osxphotos/compare/v0.22.0...v0.22.4)
> 20 January 2020
- Add --from-date and --to-date to query and export command [`#57`](https://github.com/RhetTbull/osxphotos/pull/57)
- Refactor CLI [`#55`](https://github.com/RhetTbull/osxphotos/pull/55)
- Refactor cli: singular --db, --json and query options. [`e214746`](https://github.com/RhetTbull/osxphotos/commit/e214746063271e6f9f586286103ed051ada49d85)
- Implement from_date and to_date in PhotosDB as well as query and export command. Some refactoring of CLI as well. [`cfa2b4a`](https://github.com/RhetTbull/osxphotos/commit/cfa2b4a828facf0aff5bc19f777457ad776c4a05)
- Refactored _query. Still hairy, but less so. [`b9dee49`](https://github.com/RhetTbull/osxphotos/commit/b9dee4995c6d89fadb3d2482374b7098f2ab5ed9)
- Updated README.md [`0aff83f`](https://github.com/RhetTbull/osxphotos/commit/0aff83ff21c20e293c0b75bacf2863090a0fb725)
- Started adding tests for CLI [`f0b18c3`](https://github.com/RhetTbull/osxphotos/commit/f0b18c3d29b2141d348be0495013c51c072c6251)
#### [v0.22.0](https://github.com/RhetTbull/osxphotos/compare/v0.21.5...v0.22.0)
> 18 January 2020

226
README.md
View File

@@ -11,63 +11,14 @@
* [Example uses of the module](#example-uses-of-the-module)
* [Module Interface](#module-interface)
+ [PhotosDB](#photosdb)
- [Read a Photos library database](#read-a-photos-library-database)
- [Open System Photos library](#open-system-photos-library)
- [Open a specific Photos library](#open-a-specific-photos-library)
- [`keywords`](#keywords)
- [`albums`](#albums)
- [`albums_shared`](#albums_shared)
- [`persons`](#persons)
- [`keywords_as_dict`](#keywords_as_dict)
- [`persons_as_dict`](#persons_as_dict)
- [`albums_as_dict`](#albums_as_dict)
- [`albums_shared_as_dict`](#albums_shared_as_dict)
- [`library_path`](#library_path)
- [`db_path`](#db_path)
- [`db_version`](#db_version)
- [` photos(keywords=None, uuid=None, persons=None, albums=None, images=True, movies=False)`](#-photoskeywordsnone-uuidnone-personsnone-albumsnone-imagestrue-moviesfalse)
+ [PhotoInfo](#photoinfo)
- [`uuid`](#uuid)
- [`filename`](#filename)
- [`original_filename`](#original_filename)
- [`date`](#date)
- [`description`](#description)
- [`title`](#title)
- [`keywords`](#keywords-1)
- [`albums`](#albums-1)
- [`persons`](#persons-1)
- [`path`](#path)
- [`path_edited`](#path_edited)
- [`ismissing`](#ismissing)
- [`hasadjustments`](#hasadjustments)
- [`external_edit`](#external_edit)
- [`favorite`](#favorite)
- [`hidden`](#hidden)
- [`location`](#location)
- [`shared`](#shared)
- [`isphoto`](#isphoto)
- [`ismovie`](#ismovie)
- [`iscloudasset`](#iscloudasset)
- [`incloud`](#incloud)
- [`uti`](#uti)
- [`burst`](#burst)
- [`burst_photos`](#burst_photos)
- [`live_photo`](#live_photo)
- [`path_live_photo`](#path_live_photo)
- [`json()`](#json)
- [`export(dest, *filename, edited=False, overwrite=False, increment=True, sidecar=False, use_photos_export=False, timeout=120)`](#exportdest-filename-editedfalse-overwritefalse-incrementtrue-sidecarfalse-use_photos_exportfalse-timeout120)
+ [Utility Functions](#utility-functions)
- [```get_system_library_path()```](#get_system_library_path)
- [```get_last_library_path()```](#get_last_library_path)
- [```list_photo_libraries()```](#list_photo_libraries)
- [```dd_to_dms_str(lat, lon)```](#dd_to_dms_strlat-lon)
- [```create_path_by_date(dest, dt)```](#create_path_by_datedest-dt)
+ [Examples](#examples)
* [Related Projects](#related-projects)
* [Contributing](#contributing)
* [Implementation Notes](#implementation-notes)
* [Dependencies](#dependencies)
* [Acknowledgements](#acknowledgements)
* [Acknowledgements](#acknowledgements)
## What is osxphotos?
@@ -133,79 +84,98 @@ Usage: osxphotos export [OPTIONS] [PHOTOS_LIBRARY]... DEST
photos will be exported.
Options:
--db <Photos database path> Specify Photos database path.
--keyword TEXT Search for keyword(s).
--person TEXT Search for person(s).
--album TEXT Search for album(s).
--uuid TEXT Search for UUID(s).
--title TEXT Search for TEXT in title of photo.
--no-title Search for photos with no title.
--description TEXT Search for TEXT in description of photo.
--no-description Search for photos with no description.
--uti TEXT Search for photos whose uniform type identifier
(UTI) matches TEXT
-i, --ignore-case Case insensitive search for title or
description. Does not apply to keyword, person,
or album.
--edited Search for photos that have been edited.
--external-edit Search for photos edited in external editor.
--favorite Search for photos marked favorite.
--not-favorite Search for photos not marked favorite.
--hidden Search for photos marked hidden.
--not-hidden Search for photos not marked hidden.
--burst Search for photos that were taken in a burst.
--not-burst Search for photos that are not part of a burst.
--live Search for Apple live photos
--not-live Search for photos that are not Apple live
photos
--shared Search for photos in shared iCloud album
(Photos 5 only).
--not-shared Search for photos not in shared iCloud album
(Photos 5 only).
-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 may
create name collisions on export. (e.g. if two
files happen to have the same name)
--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.
--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.
--sidecar Create JSON sidecar for each photo exported in
format useable by exiftool
(https://exiftool.org/) The sidecar file can be
used to apply metadata to the file with
exiftool, for example: "exiftool
-j=photoname.jpg.json photoname.jpg" The
sidecar file is named in format
photoname.ext.json where ext is extension of
the photo (e.g. jpg). Note: this does not
create an XMP sidecar as used by Lightroom,
etc.
--only-movies Search only for movies (default searches both
images and movies).
--only-photos Search only for photos/images (default searches
both images and movies).
--download-missing Attempt to download missing photos from iCloud.
The current implementation uses Applescript 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.
-h, --help Show this message and exit.
--db <Photos database path> Specify Photos database path. Path to Photos
library/database can be specified using
either --db or directly as PHOTOS_LIBRARY
positional argument. If neither --db or
PHOTOS_LIBRARY provided, will attempt to
find the library to use in the following
order: 1. last opened library, 2. system
library, 3. ~/Pictures/Photos
Library.photoslibrary
--keyword TEXT Search for keyword(s).
--person TEXT Search for person(s).
--album TEXT Search for album(s).
--uuid TEXT Search for UUID(s).
--title TEXT Search for TEXT in title of photo.
--no-title Search for photos with no title.
--description TEXT Search for TEXT in description of photo.
--no-description Search for photos with no description.
--uti TEXT Search for photos whose uniform type
identifier (UTI) matches TEXT
-i, --ignore-case Case insensitive search for title or
description. Does not apply to keyword,
person, or album.
--edited Search for photos that have been edited.
--external-edit Search for photos edited in external editor.
--favorite Search for photos marked favorite.
--not-favorite Search for photos not marked favorite.
--hidden Search for photos marked hidden.
--not-hidden Search for photos not marked hidden.
--shared Search for photos in shared iCloud album
(Photos 5 only).
--not-shared Search for photos not in shared iCloud album
(Photos 5 only).
--burst Search for photos that were taken in a
burst.
--not-burst Search for photos that are not part of a
burst.
--live Search for Apple live photos
--not-live Search for photos that are not Apple live
photos
--only-movies Search only for movies (default searches
both images and movies).
--only-photos Search only for photos/images (default
searches both images and movies).
--from-date [%Y-%m-%d|%Y-%m-%dT%H:%M:%S|%Y-%m-%d %H:%M:%S]
Search by start item date, e.g.
2000-01-12T12:00:00 or 2000-12-31 (ISO 8601
w/o TZ).
--to-date [%Y-%m-%d|%Y-%m-%dT%H:%M:%S|%Y-%m-%d %H:%M:%S]
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
may create name collisions on export. (e.g.
if two files happen to have the same name)
--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.
--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.
--sidecar Create JSON sidecar for each photo exported
in format useable by exiftool
(https://exiftool.org/) The sidecar file can
be used to apply metadata to the file with
exiftool, for example: "exiftool
-j=photoname.jpg.json photoname.jpg" The
sidecar file is named in format
photoname.ext.json where ext is extension of
the photo (e.g. jpg). Note: this does not
create an XMP sidecar as used by Lightroom,
etc.
--download-missing Attempt to download missing photos from
iCloud. The current implementation uses
Applescript 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.
-h, --help Show this message and exit.
```
Example: export all photos to ~/Desktop/export, including edited versions and live photo movies, group in folders by date created
@@ -449,11 +419,11 @@ photosdb.db_version
Returns the version number for Photos library database. You likely won't need this but it's provided in case needed for debugging. PhotosDB will print a warning to `sys.stderr` if you open a database version that has not been tested.
#### ` photos(keywords=None, uuid=None, persons=None, albums=None, images=True, movies=False)`
#### ` photos(keywords=None, uuid=None, persons=None, albums=None, images=True, movies=False, from_date=None, to_date=None)`
```python
# assumes photosdb is a PhotosDB object (see above)
photos = photosdb.photos([keywords=['keyword',]], [uuid=['uuid',]], [persons=['person',]], [albums=['album',]])
photos = photosdb.photos([keywords=['keyword',]], [uuid=['uuid',]], [persons=['person',]], [albums=['album',]],[from_date=datetime.datetime],[to_date=datetime.datetime])
```
Returns a list of [PhotoInfo](#PhotoInfo) objects. Each PhotoInfo object represents a photo in the Photos Libary.
@@ -469,6 +439,8 @@ photos = photosdb.photos(
albums = [],
images = bool,
movies = bool,
from_date = datetime.datetime,
to_date = datetime.datetime
)
```
@@ -478,8 +450,10 @@ photos = photosdb.photos(
- ```albums```: list of one or more album names. Returns only photos contained in the album(s). If more than one album name is provided, returns photos contained in any of the albums (.e.g. treated as "or")
- ```images```: bool; if True, returns photos/images; default is True
- ```movies```: bool; if True, returns movies/videos; default is False
- ```from_date```: datetime.datetime; if provided, finds photos where creation date >= from_date; default is None
- ```to_date```: datetime.datetime; if provided, finds photos where creation date <= to_date; default is None
If more than one of (keywords, uuid, persons, albums) is provided, they are treated as "and" criteria. E.g.
If more than one of (keywords, uuid, persons, albums,from_date, to_date) is provided, they are treated as "and" criteria. E.g.
Finds all photos with (keyword = "wedding" or "birthday") and (persons = "Juan Rodriguez")

34
examples/photos_repl.py Executable file
View File

@@ -0,0 +1,34 @@
#!/usr/bin/env python3 -i
# open an interactive REPL with photosdb and photos defined
# as osxphotos.PhotosDB() and PhotosDB.photos respectively
# useful for debugging or exploring the Photos database
import sys
# click needed since this uses a couple of functions from CLI (__main__.py)
import click
import osxphotos
from osxphotos.__main__ import get_photos_db, _list_libraries
def main():
db = None
if len(sys.argv) > 1:
db = sys.argv[1]
else:
db = get_photos_db()
if db:
return osxphotos.PhotosDB(dbfile=db)
else:
_list_libraries()
sys.exit()
if __name__ == "__main__":
print(f"Version: {osxphotos._version.__version__}")
photosdb = main()
photos = photosdb.photos(images=True, movies=True)

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,3 @@
""" version info """
__version__ = "0.22.1"
__version__ = "0.22.5"

View File

@@ -1318,6 +1318,8 @@ class PhotosDB:
albums=None,
images=True,
movies=False,
from_date=None,
to_date=None,
):
"""
Return a list of PhotoInfo objects
@@ -1326,9 +1328,11 @@ class PhotosDB:
If more than one arg, returns photos matching all the criteria (e.g. keywords AND persons)
images: if True, returns image files, if False, does not return images; default is True
movies: if True, returns movie files, if False, does not return movies; default is False
from_date: return photos with creation date >= from_date (datetime.datetime object, default None)
to_date: return photos with creation date <= to_date (datetime.datetime object, default None)
"""
photos_sets = [] # list of photo sets to perform intersection of
if not keywords and not uuid and not persons and not albums:
if not any([keywords, uuid, persons, albums, from_date, to_date]):
# return all the photos, filtering for images and movies
# append keys of all photos as a single set to photos_sets
photos_sets.append(set(self._dbphotos.keys()))
@@ -1372,6 +1376,19 @@ class PhotosDB:
photos_sets.append(set(self._dbfaces_person[person]))
else:
logging.debug(f"Could not find person '{person}' in database")
if from_date or to_date:
dsel = self._dbphotos
if from_date:
dsel = {
k: v for k, v in dsel.items() if v["imageDate"] >= from_date
}
logging.debug(
f"Found %i items with from_date {from_date}" % len(dsel)
)
if to_date:
dsel = {k: v for k, v in dsel.items() if v["imageDate"] <= to_date}
logging.debug(f"Found %i items with to_date {to_date}" % len(dsel))
photos_sets.append(set(dsel.keys()))
photoinfo = []
if photos_sets: # found some photos

View File

@@ -289,6 +289,24 @@ def create_path_by_date(dest, dt):
return new_dest
# 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):
# """ Force Photos to open a specific library
# library_path: path to the Photos library """
# open_scpt = AppleScript(
# f"""
# on openLibrary
# tell application "Photos"
# activate
# open POSIX file "{library_path}"
# end tell
# end openLibrary
# """
# )
# open_scpt.run()
def _export_photo_uuid_applescript(
uuid, dest, original=True, edited=False, timeout=120
):

View File

@@ -786,3 +786,20 @@ def test_photosinfo_repr():
k: str(v).encode("utf-8")
for k, v in photo2.__dict__.items()
}
def test_from_to_date():
import osxphotos
import datetime as dt
photosdb = osxphotos.PhotosDB(PHOTOS_DB)
photos = photosdb.photos(from_date=dt.datetime(2018, 10, 28))
assert len(photos) == 2
photos = photosdb.photos(to_date=dt.datetime(2018, 10, 28))
assert len(photos) == 5
photos = photosdb.photos(from_date=dt.datetime(2018, 9, 28),
to_date=dt.datetime(2018, 9, 29))
assert len(photos) == 4

127
tests/test_cli.py Normal file
View File

@@ -0,0 +1,127 @@
import pytest
from click.testing import CliRunner
CLI_OUTPUT_NO_SUBCOMMAND = [
"Options:",
"--db <Photos database path> Specify Photos database path. Path to Photos",
"library/database can be specified using either",
"--db or directly as PHOTOS_LIBRARY positional",
"argument.",
"--json Print output in JSON format.",
"-v, --version Show the version and exit.",
"-h, --help Show this message and exit.",
"Commands:",
" albums Print out albums found in the Photos library.",
" dump Print list of all photos & associated info from the Photos",
" export Export photos from the Photos database.",
" help Print help; for help on commands: help <command>.",
" info Print out descriptive info of the Photos library database.",
" keywords Print out keywords found in the Photos library.",
" list Print list of Photos libraries found on the system.",
" persons Print out persons (faces) found in the Photos library.",
" 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_EXPORT_FILENAMES = [
"Pumkins1.jpg",
"Pumkins2.jpg",
"Pumpkins3.jpg",
"St James Park.jpg",
"St James Park_edited.jpg",
"Tulips.jpg",
"wedding.jpg",
"wedding_edited.jpg",
]
def test_osxphotos():
import osxphotos
from osxphotos.__main__ import cli
runner = CliRunner()
result = runner.invoke(cli, [])
output = result.output
assert result.exit_code == 0
for line in CLI_OUTPUT_NO_SUBCOMMAND:
assert line in output
def test_query_uuid():
import json
import osxphotos
from osxphotos.__main__ import query
runner = CliRunner()
result = runner.invoke(
query,
[
"--json",
"--db",
"./tests/Test-10.15.1.photoslibrary",
"--uuid",
"D79B8D77-BFFC-460B-9312-034F2877D35B",
],
)
assert result.exit_code == 0
json_expected = json.loads(CLI_OUTPUT_QUERY_UUID)[0]
json_got = json.loads(result.output)[0]
assert list(json_expected.keys()).sort() == list(json_got.keys()).sort()
# check values expected vs got
# path needs special handling as path is set to full path which will differ system to system
for key_ in json_expected:
assert key_ in json_got
if key_ != "path":
assert json_expected[key_] == json_got[key_]
else:
assert json_expected[key_] in json_got[key_]
def test_export():
import glob
import os
import os.path
import osxphotos
from osxphotos.__main__ import export
runner = CliRunner()
cwd = os.getcwd()
with runner.isolated_filesystem():
result = runner.invoke(
export,
[
os.path.join(cwd, "tests/Test-10.15.1.photoslibrary"),
".",
"--original-name",
"--export-edited",
"-V",
],
)
files = glob.glob("*.jpg")
assert files.sort() == CLI_EXPORT_FILENAMES.sort()
def test_query_date():
import json
import osxphotos
from osxphotos.__main__ import query
runner = CliRunner()
result = runner.invoke(
query,
[
"--json",
"--db",
"./tests/Test-10.15.1.photoslibrary",
"--from-date=2018-09-28",
"--to-date=2018-09-28T23:00:00"
],
)
assert result.exit_code == 0
json_got = json.loads(result.output)
assert len(json_got) == 4