Added {folder_album} to template and --folder to CLI

This commit is contained in:
Rhet Turnbull
2020-04-12 14:53:53 -07:00
parent 21e7020fec
commit b7c7b9f066
5 changed files with 224 additions and 41 deletions

View File

@@ -107,16 +107,21 @@ Options:
order: 1. last opened library, 2. system
library, 3. ~/Pictures/Photos
Library.photoslibrary
--keyword KEYWORD Search for keyword KEYWORD. If more than one
keyword, treated as "OR", e.g. find photos
match any keyword
--person PERSON Search for person PERSON. If more than one
person, treated as "OR", e.g. find photos
match any person
--album ALBUM Search for album ALBUM. If more than one
album, treated as "OR", e.g. find photos
match any album
--uuid UUID Search for UUID(s).
--keyword KEYWORD Search for photos with keyword KEYWORD. If
more than one keyword, treated as "OR", e.g.
find photos match any keyword
--person PERSON Search for photos with person PERSON. If
more than one person, treated as "OR", e.g.
find photos match any person
--album ALBUM Search for photos in album ALBUM. If more
than one album, treated as "OR", e.g. find
photos match any album
--folder FOLDER Search for photos in an album in folder
FOLDER. If more than one folder, treated as
"OR", e.g. find photos in any FOLDER. Only
searches top level folders (e.g. does not
look at subfolders)
--uuid UUID Search for photos with UUID(s).
--title TITLE Search for TITLE in title of photo.
--no-title Search for photos with no title.
--description DESC Search for DESC in description of photo.
@@ -232,6 +237,11 @@ Options:
output directory in the form
'{name,DEFAULT}'. See below for additional
details on templating system.
--no-extended-attributes Don't copy extended attributes when
exporting. You only need this if exporting
to a filesystem that doesn't support Mac OS
extended attributes. Only use this if you
get an error while exporting.
-h, --help Show this message and exit.
**Templating System**
@@ -334,10 +344,13 @@ exported, one to each directory. For example: --directory
of the following directories if the photos were created in 2019 and were in
albums 'Vacation' and 'Family': 2019/Vacation, 2019/Family
Substitution Description
{album} Album(s) photo is contained in
{keyword} Keyword(s) assigned to photo
{person} Person(s) / face(s) in a photo
Substitution Description
{album} Album(s) photo is contained in
{folder_album} Folder path + album photo is contained in. e.g.
'Folder/Subfolder/Album' or just 'Album' if no enclosing
folder
{keyword} Keyword(s) assigned to photo
{person} Person(s) / face(s) in a photo
```
Example: export all photos to ~/Desktop/export, including edited versions and live photo movies, group in folders by date created

View File

@@ -187,7 +187,7 @@ def query_options(f):
metavar="KEYWORD",
default=None,
multiple=True,
help="Search for keyword KEYWORD. "
help="Search for photos with keyword KEYWORD. "
'If more than one keyword, treated as "OR", e.g. find photos match any keyword',
),
o(
@@ -195,7 +195,7 @@ def query_options(f):
metavar="PERSON",
default=None,
multiple=True,
help="Search for person PERSON. "
help="Search for photos with person PERSON. "
'If more than one person, treated as "OR", e.g. find photos match any person',
),
o(
@@ -203,15 +203,24 @@ def query_options(f):
metavar="ALBUM",
default=None,
multiple=True,
help="Search for album ALBUM. "
help="Search for photos in album ALBUM. "
'If more than one album, treated as "OR", e.g. find photos match any album',
),
o(
"--folder",
metavar="FOLDER",
default=None,
multiple=True,
help="Search for photos in an album in folder FOLDER. "
'If more than one folder, treated as "OR", e.g. find photos in any FOLDER. '
"Only searches top level folders (e.g. does not look at subfolders)",
),
o(
"--uuid",
metavar="UUID",
default=None,
multiple=True,
help="Search for UUID(s).",
help="Search for photos with UUID(s).",
),
o(
"--title",
@@ -670,6 +679,7 @@ def query(
keyword,
person,
album,
folder,
uuid,
title,
no_title,
@@ -728,6 +738,7 @@ def query(
keyword,
person,
album,
folder,
uuid,
edited,
external_edit,
@@ -781,6 +792,7 @@ def query(
keyword=keyword,
person=person,
album=album,
folder=folder,
uuid=uuid,
title=title,
no_title=no_title,
@@ -932,6 +944,7 @@ def export(
keyword,
person,
album,
folder,
uuid,
title,
no_title,
@@ -1051,6 +1064,7 @@ def export(
keyword=keyword,
person=person,
album=album,
folder=folder,
uuid=uuid,
title=title,
no_title=no_title,
@@ -1270,6 +1284,7 @@ def _query(
keyword=None,
person=None,
album=None,
folder=None,
uuid=None,
title=None,
no_title=None,
@@ -1316,10 +1331,10 @@ def _query(
place=None,
no_place=None,
):
""" run a query against PhotosDB to extract the photos based on user supply criteria """
""" used by query and export commands """
""" arguments must be passed in same order as query and export """
""" if either is modified, need to ensure all three functions are updated """
""" run a query against PhotosDB to extract the photos based on user supply criteria
used by query and export commands
arguments must be passed in same order as query and export
if either is modified, need to ensure all three functions are updated """
photosdb = osxphotos.PhotosDB(dbfile=db)
photos = photosdb.photos(
@@ -1333,6 +1348,21 @@ def _query(
to_date=to_date,
)
if folder:
# search for photos in an album in folder
# finds photos that have albums whose top level folder matches folder
photo_list = []
for f in folder:
photo_list.extend(
[
p
for p in photos
if p.album_info
and f in [a.folder_names[0] for a in p.album_info if a.folder_names]
]
)
photos = photo_list
if title:
# search title field for text
# if more than one, find photos with all title values in title

View File

@@ -1,3 +1,3 @@
""" version info """
__version__ = "0.27.3"
__version__ = "0.27.4"

View File

@@ -2,10 +2,12 @@ import pytest
from click.testing import CliRunner
CLI_PHOTOS_DB = "tests/Test-10.15.1.photoslibrary"
LIVE_PHOTOS_DB = "tests/Test-Cloud-10.15.1.photoslibrary/database/photos.db"
LIVE_PHOTOS_DB = "tests/Test-Cloud-10.15.1.photoslibrary"
RAW_PHOTOS_DB = "tests/Test-RAW-10.15.1.photoslibrary"
PLACES_PHOTOS_DB = "tests/Test-Places-Catalina-10_15_1.photoslibrary"
PLACES_PHOTOS_DB_13 = "tests/Test-Places-High-Sierra-10.13.6.photoslibrary"
PHOTOS_DB_15_4 = "tests/Test-10.15.4.photoslibrary"
PHOTOS_DB_14_6 = "tests/Test-10.14.6.photoslibrary"
CLI_OUTPUT_NO_SUBCOMMAND = [
"Options:",
@@ -658,3 +660,84 @@ def test_no_place_15():
assert len(json_got) == 1 # single element
assert json_got[0]["uuid"] == "A9B73E13-A6F2-4915-8D67-7213B39BAE9F"
def test_no_folder_1_15():
# test --folder on 10.15
import json
import os
import os.path
import osxphotos
from osxphotos.__main__ import query
runner = CliRunner()
cwd = os.getcwd()
# pylint: disable=not-context-manager
with runner.isolated_filesystem():
result = runner.invoke(
query, [os.path.join(cwd, PHOTOS_DB_15_4), "--json", "--folder", "Folder1"]
)
assert result.exit_code == 0
json_got = json.loads(result.output)
assert len(json_got) == 2 # single element
for item in json_got:
assert item["uuid"] in [
"3DD2C897-F19E-4CA6-8C22-B027D5A71907",
"E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51",
]
assert item["albums"] == ["AlbumInFolder"]
def test_no_folder_2_15():
# test --folder with --uuid on 10.15
import json
import os
import os.path
import osxphotos
from osxphotos.__main__ import query
runner = CliRunner()
cwd = os.getcwd()
# pylint: disable=not-context-manager
with runner.isolated_filesystem():
result = runner.invoke(
query,
[
os.path.join(cwd, PHOTOS_DB_15_4),
"--json",
"--folder",
"Folder1",
"--uuid",
"E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51",
],
)
assert result.exit_code == 0
json_got = json.loads(result.output)
assert len(json_got) == 1 # single element
for item in json_got:
assert item["uuid"] == "E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51"
assert item["albums"] == ["AlbumInFolder"]
def test_no_folder_1_14(caplog):
# test --folder on 10.14
import json
import os
import os.path
import osxphotos
from osxphotos.__main__ import query
runner = CliRunner()
cwd = os.getcwd()
# pylint: disable=not-context-manager
with runner.isolated_filesystem():
result = runner.invoke(
query, [os.path.join(cwd, PHOTOS_DB_14_6), "--json", "--folder", "Folder1"]
)
assert result.exit_code == 0
json_got = json.loads(result.output)
assert len(json_got) == 0 # single element
assert "not yet implemented" in caplog.text

View File

@@ -1,13 +1,21 @@
""" Test template.py """
import pytest
PHOTOS_DB_1 = "./tests/Test-Places-Catalina-10_15_1.photoslibrary/database/photos.db"
PHOTOS_DB_2 = "./tests/Test-10.15.1.photoslibrary/database/photos.db"
PHOTOS_DB_PLACES = (
"./tests/Test-Places-Catalina-10_15_1.photoslibrary/database/photos.db"
)
PHOTOS_DB_15_1 = "./tests/Test-10.15.1.photoslibrary/database/photos.db"
PHOTOS_DB_15_4 = "./tests/Test-10.15.4.photoslibrary/database/photos.db"
PHOTOS_DB_14_6 = "./tests/Test-10.14.6.photoslibrary/database/photos.db"
UUID_DICT = {
"place_dc": "128FB4C6-0B16-4E7D-9108-FB2E90DA1546",
"1_1_2": "1EB2B765-0765-43BA-A90C-0D0580E6172C",
"2_1_1": "D79B8D77-BFFC-460B-9312-034F2877D35B",
"0_2_0": "6191423D-8DB8-4D4C-92BE-9BBBA308AAC4",
"folder_album_1": "3DD2C897-F19E-4CA6-8C22-B027D5A71907",
"folder_album_no_folder": "D79B8D77-BFFC-460B-9312-034F2877D35B",
"mojave_no_folder": "15uNd7%8RguTEgNPKHfTWw",
}
TEMPLATE_VALUES = {
@@ -55,7 +63,7 @@ def test_lookup():
TEMPLATE_SUBSTITUTIONS,
)
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_1)
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_PLACES)
photo = photosdb.photos(uuid=[UUID_DICT["place_dc"]])[0]
for subst in TEMPLATE_SUBSTITUTIONS:
@@ -71,7 +79,7 @@ def test_subst():
from osxphotos.template import render_filepath_template
locale.setlocale(locale.LC_ALL, "en_US")
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_1)
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_PLACES)
photo = photosdb.photos(uuid=[UUID_DICT["place_dc"]])[0]
for template in TEMPLATE_VALUES:
@@ -86,7 +94,7 @@ def test_subst_default_val():
from osxphotos.template import render_filepath_template
locale.setlocale(locale.LC_ALL, "en_US")
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_1)
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_PLACES)
photo = photosdb.photos(uuid=[UUID_DICT["place_dc"]])[0]
template = "{place.name.area_of_interest,UNKNOWN}"
@@ -101,7 +109,7 @@ def test_subst_default_val_2():
from osxphotos.template import render_filepath_template
locale.setlocale(locale.LC_ALL, "en_US")
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_1)
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_PLACES)
photo = photosdb.photos(uuid=[UUID_DICT["place_dc"]])[0]
template = "{place.name.area_of_interest,}"
@@ -116,7 +124,7 @@ def test_subst_unknown_val():
from osxphotos.template import render_filepath_template
locale.setlocale(locale.LC_ALL, "en_US")
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_1)
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_PLACES)
photo = photosdb.photos(uuid=[UUID_DICT["place_dc"]])[0]
template = "{created.year}/{foo}"
@@ -134,7 +142,7 @@ def test_subst_double_brace():
import osxphotos
from osxphotos.template import render_filepath_template
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_1)
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_PLACES)
photo = photosdb.photos(uuid=[UUID_DICT["place_dc"]])[0]
template = "{created.year}/{{foo}}"
@@ -150,7 +158,7 @@ def test_subst_unknown_val_with_default():
from osxphotos.template import render_filepath_template
locale.setlocale(locale.LC_ALL, "en_US")
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_1)
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_PLACES)
photo = photosdb.photos(uuid=[UUID_DICT["place_dc"]])[0]
template = "{created.year}/{foo,bar}"
@@ -165,7 +173,7 @@ def test_subst_multi_1_1_2():
import osxphotos
from osxphotos.template import render_filepath_template
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_2)
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_15_1)
photo = photosdb.photos(uuid=[UUID_DICT["1_1_2"]])[0]
template = "{created.year}/{album}/{keyword}/{person}"
@@ -180,7 +188,7 @@ def test_subst_multi_2_1_1():
import osxphotos
from osxphotos.template import render_filepath_template
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_2)
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_15_1)
# one album, one keyword, two persons
photo = photosdb.photos(uuid=[UUID_DICT["2_1_1"]])[0]
@@ -200,7 +208,7 @@ def test_subst_multi_2_1_1_single():
import osxphotos
from osxphotos.template import render_filepath_template
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_2)
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_15_1)
# one album, one keyword, two persons
photo = photosdb.photos(uuid=[UUID_DICT["2_1_1"]])[0]
@@ -216,7 +224,7 @@ def test_subst_multi_0_2_0():
import osxphotos
from osxphotos.template import render_filepath_template
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_2)
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_15_1)
# one album, one keyword, two persons
photo = photosdb.photos(uuid=[UUID_DICT["0_2_0"]])[0]
@@ -232,7 +240,7 @@ def test_subst_multi_0_2_0_single():
import osxphotos
from osxphotos.template import render_filepath_template
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_2)
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_15_1)
# one album, one keyword, two persons
photo = photosdb.photos(uuid=[UUID_DICT["0_2_0"]])[0]
@@ -248,7 +256,7 @@ def test_subst_multi_0_2_0_default_val():
import osxphotos
from osxphotos.template import render_filepath_template
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_2)
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_15_1)
# one album, one keyword, two persons
photo = photosdb.photos(uuid=[UUID_DICT["0_2_0"]])[0]
@@ -264,7 +272,7 @@ def test_subst_multi_0_2_0_default_val_unknown_val():
import osxphotos
from osxphotos.template import render_filepath_template
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_2)
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_15_1)
# one album, one keyword, two persons
photo = photosdb.photos(uuid=[UUID_DICT["0_2_0"]])[0]
@@ -286,7 +294,7 @@ def test_subst_multi_0_2_0_default_val_unknown_val_2():
import osxphotos
from osxphotos.template import render_filepath_template
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_2)
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_15_1)
# one album, one keyword, two persons
photo = photosdb.photos(uuid=[UUID_DICT["0_2_0"]])[0]
@@ -298,3 +306,52 @@ def test_subst_multi_0_2_0_default_val_unknown_val_2():
rendered, unknown = render_filepath_template(template, photo)
assert sorted(rendered) == sorted(expected)
assert unknown == ["foo"]
def test_subst_multi_folder_albums_1():
""" Test substitutions for folder_album are correct """
import osxphotos
from osxphotos.template import render_filepath_template
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_15_4)
# photo in an album in a folder
photo = photosdb.photos(uuid=[UUID_DICT["folder_album_1"]])[0]
template = "{folder_album}"
expected = ["Folder1/SubFolder2/AlbumInFolder"]
rendered, unknown = render_filepath_template(template, photo)
assert sorted(rendered) == sorted(expected)
assert unknown == []
def test_subst_multi_folder_albums_2():
""" Test substitutions for folder_album are correct """
import osxphotos
from osxphotos.template import render_filepath_template
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_15_4)
# photo in an album in a folder
photo = photosdb.photos(uuid=[UUID_DICT["folder_album_no_folder"]])[0]
template = "{folder_album}"
expected = ["Pumpkin Farm", "Test Album"]
rendered, unknown = render_filepath_template(template, photo)
assert sorted(rendered) == sorted(expected)
assert unknown == []
def test_subst_multi_folder_albums_3(caplog):
""" Test substitutions for folder_album on < Photos 5 (not implemented) """
import osxphotos
from osxphotos.template import render_filepath_template
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_14_6)
# photo in an album in a folder
photo = photosdb.photos(uuid=[UUID_DICT["mojave_no_folder"]])[0]
template = "{folder_album}"
expected = ["Pumpkin Farm", "Test Album (1)"]
rendered, unknown = render_filepath_template(template, photo)
assert sorted(rendered) == sorted(expected)
assert unknown == []
assert "not yet implemented" in caplog.text