Added {album}, {keyword}, and {person} to template system
This commit is contained in:
26
README.md
26
README.md
@@ -88,7 +88,6 @@ Example: `osxphotos help export`
|
|||||||
|
|
||||||
```
|
```
|
||||||
Usage: osxphotos export [OPTIONS] [PHOTOS_LIBRARY]... DEST
|
Usage: osxphotos export [OPTIONS] [PHOTOS_LIBRARY]... DEST
|
||||||
Usage: __main__.py export [OPTIONS] [PHOTOS_LIBRARY]... DEST
|
|
||||||
|
|
||||||
Export photos from the Photos database. Export path DEST is required.
|
Export photos from the Photos database. Export path DEST is required.
|
||||||
Optionally, query the Photos database using 1 or more search options; if
|
Optionally, query the Photos database using 1 or more search options; if
|
||||||
@@ -245,13 +244,11 @@ be '/Users/maria/Pictures/export/2020/March' if the photo was created in March
|
|||||||
|
|
||||||
In the template, valid template substitutions will be replaced by the
|
In the template, valid template substitutions will be replaced by the
|
||||||
corresponding value from the table below. Invalid substitutions will result
|
corresponding value from the table below. Invalid substitutions will result
|
||||||
in a warning but will be left unchanged. e.g. if you put '{foo}' in your
|
in a an error and the script will abort.
|
||||||
template, e.g. '{created.year}/{foo}', the resulting output directory would
|
|
||||||
look like '/Users/maria/Pictures/export/2020/{foo}'
|
|
||||||
|
|
||||||
If you want the actual text of the template substition to appear in the
|
If you want the actual text of the template substition to appear in the
|
||||||
rendered name, escape the curly braces with \, for example, using
|
rendered name, use double braces, e.g. '{{' or '}}', thus using
|
||||||
'{created.year}/\{name\}' for --directory would result in output of
|
'{created.year}/{{name}}' for --directory would result in output of
|
||||||
2020/{name}/photoname.jpg
|
2020/{name}/photoname.jpg
|
||||||
|
|
||||||
You may specify an optional default value to use if the substitution does not
|
You may specify an optional default value to use if the substitution does not
|
||||||
@@ -327,6 +324,18 @@ Substitution Description
|
|||||||
'United States'
|
'United States'
|
||||||
{place.address.country_code} ISO country code of the postal address, e.g.
|
{place.address.country_code} ISO country code of the postal address, e.g.
|
||||||
'US'
|
'US'
|
||||||
|
|
||||||
|
The following substitutions may result in multiple values. Thus if specified
|
||||||
|
for --directory these could result in multiple copies of a photo being being
|
||||||
|
exported, one to each directory. For example: --directory
|
||||||
|
'{created.year}/{album}' could result in the same photo being exported to each
|
||||||
|
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
|
||||||
```
|
```
|
||||||
|
|
||||||
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, including edited versions and live photo movies, group in folders by date created
|
||||||
@@ -1005,7 +1014,6 @@ If you want to include "{" or "}" in the output, use "{{" or "}}"
|
|||||||
|
|
||||||
e.g. `render_filepath_template("{created.year}/{{foo}}", photo)` would return `("2020/{foo}",[])`
|
e.g. `render_filepath_template("{created.year}/{{foo}}", photo)` would return `("2020/{foo}",[])`
|
||||||
|
|
||||||
|
|
||||||
| Substitution | Description |
|
| Substitution | Description |
|
||||||
|--------------|-------------|
|
|--------------|-------------|
|
||||||
|{name}|Filename of the photo|
|
|{name}|Filename of the photo|
|
||||||
@@ -1039,6 +1047,10 @@ e.g. `render_filepath_template("{created.year}/{{foo}}", photo)` would return `(
|
|||||||
|{place.address.postal_code}|Postal code part of the postal address, e.g. '20009'|
|
|{place.address.postal_code}|Postal code part of the postal address, e.g. '20009'|
|
||||||
|{place.address.country}|Country name of the postal address, e.g. 'United States'|
|
|{place.address.country}|Country name of the postal address, e.g. 'United States'|
|
||||||
|{place.address.country_code}|ISO country code of the postal address, e.g. 'US'|
|
|{place.address.country_code}|ISO country code of the postal address, e.g. 'US'|
|
||||||
|
|{album}|Album(s) photo is contained in|
|
||||||
|
|{keyword}|Keyword(s) assigned to photo|
|
||||||
|
|{person}|Person(s) / face(s) in a photo|
|
||||||
|
|
||||||
|
|
||||||
#### `DateTimeFormatter(dt)`
|
#### `DateTimeFormatter(dt)`
|
||||||
Class that provides easy access to formatted datetime values.
|
Class that provides easy access to formatted datetime values.
|
||||||
|
|||||||
@@ -21,7 +21,11 @@ import osxphotos
|
|||||||
from ._constants import _EXIF_TOOL_URL, _PHOTOS_5_VERSION, _UNKNOWN_PLACE
|
from ._constants import _EXIF_TOOL_URL, _PHOTOS_5_VERSION, _UNKNOWN_PLACE
|
||||||
from ._version import __version__
|
from ._version import __version__
|
||||||
from .exiftool import get_exiftool_path
|
from .exiftool import get_exiftool_path
|
||||||
from .template import render_filepath_template, TEMPLATE_SUBSTITUTIONS
|
from .template import (
|
||||||
|
render_filepath_template,
|
||||||
|
TEMPLATE_SUBSTITUTIONS,
|
||||||
|
TEMPLATE_SUBSTITUTIONS_MULTI_VALUED,
|
||||||
|
)
|
||||||
from .utils import _copy_file, create_path_by_date
|
from .utils import _copy_file, create_path_by_date
|
||||||
|
|
||||||
|
|
||||||
@@ -91,15 +95,13 @@ class ExportCommand(click.Command):
|
|||||||
formatter.write_text(
|
formatter.write_text(
|
||||||
"In the template, valid template substitutions will be replaced by "
|
"In the template, valid template substitutions will be replaced by "
|
||||||
+ "the corresponding value from the table below. Invalid substitutions will result in a "
|
+ "the corresponding value from the table below. Invalid substitutions will result in a "
|
||||||
+ "warning but will be left unchanged. e.g. if you put '{foo}' in your template, "
|
+ "an error and the script will abort."
|
||||||
+ "e.g. '{created.year}/{foo}', the resulting output directory would look like "
|
|
||||||
+ "'/Users/maria/Pictures/export/2020/{foo}' "
|
|
||||||
)
|
)
|
||||||
formatter.write("\n")
|
formatter.write("\n")
|
||||||
formatter.write_text(
|
formatter.write_text(
|
||||||
"If you want the actual text of the template substition to appear "
|
"If you want the actual text of the template substition to appear "
|
||||||
+ "in the rendered name, escape the curly braces with \\, for example, "
|
+ "in the rendered name, use double braces, e.g. '{{' or '}}', thus "
|
||||||
+ "using '{created.year}/\\{name\\}' for --directory "
|
+ "using '{created.year}/{{name}}' for --directory "
|
||||||
+ "would result in output of 2020/{name}/photoname.jpg"
|
+ "would result in output of 2020/{name}/photoname.jpg"
|
||||||
)
|
)
|
||||||
formatter.write("\n")
|
formatter.write("\n")
|
||||||
@@ -126,6 +128,23 @@ class ExportCommand(click.Command):
|
|||||||
formatter.write("\n")
|
formatter.write("\n")
|
||||||
templ_tuples = [("Substitution", "Description")]
|
templ_tuples = [("Substitution", "Description")]
|
||||||
templ_tuples.extend((k, v) for k, v in TEMPLATE_SUBSTITUTIONS.items())
|
templ_tuples.extend((k, v) for k, v in TEMPLATE_SUBSTITUTIONS.items())
|
||||||
|
formatter.write_dl(templ_tuples)
|
||||||
|
|
||||||
|
formatter.write("\n")
|
||||||
|
formatter.write_text(
|
||||||
|
"The following substitutions may result in multiple values. Thus "
|
||||||
|
+ "if specified for --directory these could result in multiple copies of a photo being "
|
||||||
|
+ "being exported, one to each directory. For example: "
|
||||||
|
+ "--directory '{created.year}/{album}' could result in the same photo being exported "
|
||||||
|
+ "to each of the following directories if the photos were created in 2019 "
|
||||||
|
+ "and were in albums 'Vacation' and 'Family': "
|
||||||
|
+ "2019/Vacation, 2019/Family"
|
||||||
|
)
|
||||||
|
formatter.write("\n")
|
||||||
|
templ_tuples = [("Substitution", "Description")]
|
||||||
|
templ_tuples.extend(
|
||||||
|
(k, v) for k, v in TEMPLATE_SUBSTITUTIONS_MULTI_VALUED.items()
|
||||||
|
)
|
||||||
|
|
||||||
formatter.write_dl(templ_tuples)
|
formatter.write_dl(templ_tuples)
|
||||||
help_text += formatter.getvalue()
|
help_text += formatter.getvalue()
|
||||||
@@ -1101,7 +1120,7 @@ def export(
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
for p in photos:
|
for p in photos:
|
||||||
export_path = export_photo(
|
export_paths = export_photo(
|
||||||
p,
|
p,
|
||||||
dest,
|
dest,
|
||||||
verbose,
|
verbose,
|
||||||
@@ -1115,8 +1134,8 @@ def export(
|
|||||||
exiftool,
|
exiftool,
|
||||||
directory,
|
directory,
|
||||||
)
|
)
|
||||||
if export_path:
|
if export_paths:
|
||||||
click.echo(f"Exported {p.filename} to {export_path}")
|
click.echo(f"Exported {p.filename} to {export_paths}")
|
||||||
else:
|
else:
|
||||||
click.echo(f"Did not export missing file {p.filename}")
|
click.echo(f"Did not export missing file {p.filename}")
|
||||||
else:
|
else:
|
||||||
@@ -1132,7 +1151,7 @@ def help(ctx, topic, **kw):
|
|||||||
click.echo(ctx.parent.get_help())
|
click.echo(ctx.parent.get_help())
|
||||||
else:
|
else:
|
||||||
ctx.info_name = topic
|
ctx.info_name = topic
|
||||||
click.echo(cli.commands[topic].get_help(ctx))
|
click.echo_via_pager(cli.commands[topic].get_help(ctx))
|
||||||
|
|
||||||
|
|
||||||
def print_photo_info(photos, json=False):
|
def print_photo_info(photos, json=False):
|
||||||
@@ -1483,9 +1502,13 @@ def export_photo(
|
|||||||
download_missing: attempt download of missing iCloud photos
|
download_missing: attempt download of missing iCloud photos
|
||||||
exiftool: use exiftool to write EXIF metadata directly to exported photo
|
exiftool: use exiftool to write EXIF metadata directly to exported photo
|
||||||
directory: template used to determine output directory
|
directory: template used to determine output directory
|
||||||
returns destination path of exported photo or None if photo was missing
|
returns list of path(s) of exported photo or None if photo was missing
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# Can export to multiple paths
|
||||||
|
# Start with single path [dest] but direcotry and export_by_date will modify dest_paths
|
||||||
|
dest_paths = [dest]
|
||||||
|
|
||||||
if not download_missing:
|
if not download_missing:
|
||||||
if photo.ismissing:
|
if photo.ismissing:
|
||||||
space = " " if not verbose else ""
|
space = " " if not verbose else ""
|
||||||
@@ -1515,20 +1538,25 @@ def export_photo(
|
|||||||
|
|
||||||
if export_by_date:
|
if export_by_date:
|
||||||
date_created = photo.date.timetuple()
|
date_created = photo.date.timetuple()
|
||||||
dest = create_path_by_date(dest, date_created)
|
dest_path = create_path_by_date(dest, date_created)
|
||||||
|
dest_paths = [dest_path]
|
||||||
elif directory:
|
elif directory:
|
||||||
dirname, unmatched = render_filepath_template(directory, photo)
|
# got a directory template, render it and check results are valid
|
||||||
dirname = dirname[0]
|
dirnames, unmatched = render_filepath_template(directory, photo)
|
||||||
if unmatched:
|
if unmatched:
|
||||||
click.echo(
|
raise click.BadOptionUsage(
|
||||||
f"Possible unmatched substitution in template: {unmatched}", err=True
|
"directory",
|
||||||
|
f"Invalid substitution in template '{directory}': {unmatched}",
|
||||||
)
|
)
|
||||||
|
dest_paths = []
|
||||||
|
for dirname in dirnames:
|
||||||
dirname = sanitize_filepath(dirname, platform="auto")
|
dirname = sanitize_filepath(dirname, platform="auto")
|
||||||
if not is_valid_filepath(dirname, platform="auto"):
|
if not is_valid_filepath(dirname, platform="auto"):
|
||||||
raise ValueError(f"Invalid file path: {dirname}")
|
raise ValueError(f"Invalid file path: {dirname}")
|
||||||
dest = os.path.join(dest, dirname)
|
dest_path = os.path.join(dest, dirname)
|
||||||
if not os.path.isdir(dest):
|
if not os.path.isdir(dest_path):
|
||||||
os.makedirs(dest)
|
os.makedirs(dest_path)
|
||||||
|
dest_paths.append(dest_path)
|
||||||
|
|
||||||
sidecar = [s.lower() for s in sidecar]
|
sidecar = [s.lower() for s in sidecar]
|
||||||
sidecar_json = sidecar_xmp = False
|
sidecar_json = sidecar_xmp = False
|
||||||
@@ -1542,8 +1570,12 @@ def export_photo(
|
|||||||
use_photos_export = download_missing and (
|
use_photos_export = download_missing and (
|
||||||
photo.ismissing or not os.path.exists(photo.path)
|
photo.ismissing or not os.path.exists(photo.path)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# export the photo to each path in dest_paths
|
||||||
|
photo_paths = []
|
||||||
|
for dest_path in dest_paths:
|
||||||
photo_path = photo.export(
|
photo_path = photo.export(
|
||||||
dest,
|
dest_path,
|
||||||
filename,
|
filename,
|
||||||
sidecar_json=sidecar_json,
|
sidecar_json=sidecar_json,
|
||||||
sidecar_xmp=sidecar_xmp,
|
sidecar_xmp=sidecar_xmp,
|
||||||
@@ -1552,6 +1584,7 @@ def export_photo(
|
|||||||
use_photos_export=use_photos_export,
|
use_photos_export=use_photos_export,
|
||||||
exiftool=exiftool,
|
exiftool=exiftool,
|
||||||
)[0]
|
)[0]
|
||||||
|
photo_paths.append(photo_path)
|
||||||
|
|
||||||
# if export-edited, also export the edited version
|
# if export-edited, also export the edited version
|
||||||
# verify the photo has adjustments and valid path to avoid raising an exception
|
# verify the photo has adjustments and valid path to avoid raising an exception
|
||||||
@@ -1572,9 +1605,11 @@ def export_photo(
|
|||||||
edited_suffix = pathlib.Path(photo.filename).suffix
|
edited_suffix = pathlib.Path(photo.filename).suffix
|
||||||
edited_name = f"{edited_name.stem}_edited{edited_suffix}"
|
edited_name = f"{edited_name.stem}_edited{edited_suffix}"
|
||||||
if verbose:
|
if verbose:
|
||||||
click.echo(f"Exporting edited version of {filename} as {edited_name}")
|
click.echo(
|
||||||
|
f"Exporting edited version of {filename} as {edited_name}"
|
||||||
|
)
|
||||||
photo.export(
|
photo.export(
|
||||||
dest,
|
dest_path,
|
||||||
edited_name,
|
edited_name,
|
||||||
sidecar_json=sidecar_json,
|
sidecar_json=sidecar_json,
|
||||||
sidecar_xmp=sidecar_xmp,
|
sidecar_xmp=sidecar_xmp,
|
||||||
@@ -1584,7 +1619,7 @@ def export_photo(
|
|||||||
exiftool=exiftool,
|
exiftool=exiftool,
|
||||||
)
|
)
|
||||||
|
|
||||||
return photo_path
|
return photo_paths
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
""" version info """
|
""" version info """
|
||||||
|
|
||||||
__version__ = "0.24.5"
|
__version__ = "0.25.0"
|
||||||
|
|||||||
@@ -54,9 +54,9 @@ TEMPLATE_SUBSTITUTIONS = {
|
|||||||
|
|
||||||
# Permitted multi-value substitutions (each of these returns None or 1 or more values)
|
# Permitted multi-value substitutions (each of these returns None or 1 or more values)
|
||||||
TEMPLATE_SUBSTITUTIONS_MULTI_VALUED = {
|
TEMPLATE_SUBSTITUTIONS_MULTI_VALUED = {
|
||||||
"{album}": "Album photo is contained in",
|
"{album}": "Album(s) photo is contained in",
|
||||||
"{keyword}": "Keywords assigned to photo",
|
"{keyword}": "Keyword(s) assigned to photo",
|
||||||
"{person}": "Person / face in a photo",
|
"{person}": "Person(s) / face(s) in a photo",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Just the multi-valued substitution names without the braces
|
# Just the multi-valued substitution names without the braces
|
||||||
@@ -311,7 +311,7 @@ def render_filepath_template(template, photo, none_str="_"):
|
|||||||
# '2011/Album2/keyword1/person1',
|
# '2011/Album2/keyword1/person1',
|
||||||
# '2011/Album2/keyword2/person1',]
|
# '2011/Album2/keyword2/person1',]
|
||||||
|
|
||||||
rendered_strings = [rendered]
|
rendered_strings = set([rendered])
|
||||||
for field in MULTI_VALUE_SUBSTITUTIONS:
|
for field in MULTI_VALUE_SUBSTITUTIONS:
|
||||||
if field == "album":
|
if field == "album":
|
||||||
values = photo.albums
|
values = photo.albums
|
||||||
@@ -320,11 +320,7 @@ def render_filepath_template(template, photo, none_str="_"):
|
|||||||
elif field == "person":
|
elif field == "person":
|
||||||
values = photo.persons
|
values = photo.persons
|
||||||
# remove any _UNKNOWN_PERSON values
|
# remove any _UNKNOWN_PERSON values
|
||||||
try:
|
values = [val for val in values if val != _UNKNOWN_PERSON]
|
||||||
values.remove(_UNKNOWN_PERSON)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"Unhandleded template value: {field}")
|
raise ValueError(f"Unhandleded template value: {field}")
|
||||||
|
|
||||||
@@ -334,7 +330,10 @@ def render_filepath_template(template, photo, none_str="_"):
|
|||||||
# Build a regex that matches only the field being processed
|
# Build a regex that matches only the field being processed
|
||||||
re_str = r"(?<!\\)\{(" + field + r")(,{0,1}(([\w\-. ]+))?)\}"
|
re_str = r"(?<!\\)\{(" + field + r")(,{0,1}(([\w\-. ]+))?)\}"
|
||||||
regex_multi = re.compile(re_str)
|
regex_multi = re.compile(re_str)
|
||||||
new_strings = [] # holds each of the new rendered_strings
|
|
||||||
|
# holds each of the new rendered_strings, set() to avoid duplicates
|
||||||
|
new_strings = set()
|
||||||
|
|
||||||
for str_template in rendered_strings:
|
for str_template in rendered_strings:
|
||||||
for val in values:
|
for val in values:
|
||||||
|
|
||||||
@@ -351,7 +350,7 @@ def render_filepath_template(template, photo, none_str="_"):
|
|||||||
photo, none_str, get_func=get_template_value_multi
|
photo, none_str, get_func=get_template_value_multi
|
||||||
)
|
)
|
||||||
new_string = regex_multi.sub(subst, str_template)
|
new_string = regex_multi.sub(subst, str_template)
|
||||||
new_strings.append(new_string)
|
new_strings.add(new_string)
|
||||||
|
|
||||||
# update rendered_strings for the next field to process
|
# update rendered_strings for the next field to process
|
||||||
rendered_strings = new_strings
|
rendered_strings = new_strings
|
||||||
|
|||||||
@@ -50,6 +50,26 @@ CLI_EXPORTED_DIRECTORY_TEMPLATE_FILENAMES1 = [
|
|||||||
"2018/September/Pumkins1.jpg",
|
"2018/September/Pumkins1.jpg",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
CLI_EXPORTED_DIRECTORY_TEMPLATE_FILENAMES_ALBUM1 = [
|
||||||
|
"_/wedding.jpg",
|
||||||
|
"_/Tulips.jpg",
|
||||||
|
"_/St James Park.jpg",
|
||||||
|
"Pumpkin Farm/Pumpkins3.jpg",
|
||||||
|
"Pumpkin Farm/Pumkins2.jpg",
|
||||||
|
"Pumpkin Farm/Pumkins1.jpg",
|
||||||
|
"Test Album/Pumkins1.jpg",
|
||||||
|
]
|
||||||
|
|
||||||
|
CLI_EXPORTED_DIRECTORY_TEMPLATE_FILENAMES_ALBUM2 = [
|
||||||
|
"NOALBUM/wedding.jpg",
|
||||||
|
"NOALBUM/Tulips.jpg",
|
||||||
|
"NOALBUM/St James Park.jpg",
|
||||||
|
"Pumpkin Farm/Pumpkins3.jpg",
|
||||||
|
"Pumpkin Farm/Pumkins2.jpg",
|
||||||
|
"Pumpkin Farm/Pumkins1.jpg",
|
||||||
|
"Test Album/Pumkins1.jpg",
|
||||||
|
]
|
||||||
|
|
||||||
CLI_EXPORTED_DIRECTORY_TEMPLATE_FILENAMES2 = [
|
CLI_EXPORTED_DIRECTORY_TEMPLATE_FILENAMES2 = [
|
||||||
"St James's Park, Great Britain, Westminster, England, United Kingdom/St James Park.jpg",
|
"St James's Park, Great Britain, Westminster, England, United Kingdom/St James Park.jpg",
|
||||||
"_/Pumpkins3.jpg",
|
"_/Pumpkins3.jpg",
|
||||||
@@ -407,10 +427,66 @@ def test_export_directory_template_3():
|
|||||||
"{created.year}/{foo}",
|
"{created.year}/{foo}",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
assert result.exit_code == 2
|
||||||
|
assert "Error: Invalid substitution in template" in result.output
|
||||||
|
|
||||||
|
|
||||||
|
def test_export_directory_template_album_1():
|
||||||
|
# test export using directory template with multiple albums
|
||||||
|
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),
|
||||||
|
".",
|
||||||
|
"--original-name",
|
||||||
|
"-V",
|
||||||
|
"--directory",
|
||||||
|
"{album}",
|
||||||
|
],
|
||||||
|
)
|
||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
assert "Possible unmatched substitution in template: ['foo']" in result.output
|
|
||||||
workdir = os.getcwd()
|
workdir = os.getcwd()
|
||||||
for filepath in CLI_EXPORTED_DIRECTORY_TEMPLATE_FILENAMES3:
|
for filepath in CLI_EXPORTED_DIRECTORY_TEMPLATE_FILENAMES_ALBUM1:
|
||||||
|
assert os.path.isfile(os.path.join(workdir, filepath))
|
||||||
|
|
||||||
|
|
||||||
|
def test_export_directory_template_album_2():
|
||||||
|
# test export using directory template with multiple albums
|
||||||
|
# specify default value
|
||||||
|
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),
|
||||||
|
".",
|
||||||
|
"--original-name",
|
||||||
|
"-V",
|
||||||
|
"--directory",
|
||||||
|
"{album,NOALBUM}",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
assert result.exit_code == 0
|
||||||
|
workdir = os.getcwd()
|
||||||
|
for filepath in CLI_EXPORTED_DIRECTORY_TEMPLATE_FILENAMES_ALBUM2:
|
||||||
assert os.path.isfile(os.path.join(workdir, filepath))
|
assert os.path.isfile(os.path.join(workdir, filepath))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -190,6 +190,22 @@ def test_subst_multi_2_1_1():
|
|||||||
assert sorted(rendered) == sorted(expected)
|
assert sorted(rendered) == sorted(expected)
|
||||||
|
|
||||||
|
|
||||||
|
def test_subst_multi_2_1_1_single():
|
||||||
|
""" Test that substitutions are correct """
|
||||||
|
# 2 albums, 1 keyword, 1 person but only do keywords
|
||||||
|
import osxphotos
|
||||||
|
from osxphotos.template import render_filepath_template
|
||||||
|
|
||||||
|
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_2)
|
||||||
|
# one album, one keyword, two persons
|
||||||
|
photo = photosdb.photos(uuid=[UUID_DICT["2_1_1"]])[0]
|
||||||
|
|
||||||
|
template = "{keyword}"
|
||||||
|
expected = ["Kids"]
|
||||||
|
rendered, _ = render_filepath_template(template, photo)
|
||||||
|
assert sorted(rendered) == sorted(expected)
|
||||||
|
|
||||||
|
|
||||||
def test_subst_multi_0_2_0():
|
def test_subst_multi_0_2_0():
|
||||||
""" Test that substitutions are correct """
|
""" Test that substitutions are correct """
|
||||||
# 0 albums, 2 keywords, 0 persons
|
# 0 albums, 2 keywords, 0 persons
|
||||||
@@ -206,6 +222,22 @@ def test_subst_multi_0_2_0():
|
|||||||
assert sorted(rendered) == sorted(expected)
|
assert sorted(rendered) == sorted(expected)
|
||||||
|
|
||||||
|
|
||||||
|
def test_subst_multi_0_2_0_single():
|
||||||
|
""" Test that substitutions are correct """
|
||||||
|
# 0 albums, 2 keywords, 0 persons, but only do albums
|
||||||
|
import osxphotos
|
||||||
|
from osxphotos.template import render_filepath_template
|
||||||
|
|
||||||
|
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_2)
|
||||||
|
# one album, one keyword, two persons
|
||||||
|
photo = photosdb.photos(uuid=[UUID_DICT["0_2_0"]])[0]
|
||||||
|
|
||||||
|
template = "{created.year}/{album}"
|
||||||
|
expected = ["2019/_"]
|
||||||
|
rendered, _ = render_filepath_template(template, photo)
|
||||||
|
assert sorted(rendered) == sorted(expected)
|
||||||
|
|
||||||
|
|
||||||
def test_subst_multi_0_2_0_default_val():
|
def test_subst_multi_0_2_0_default_val():
|
||||||
""" Test that substitutions are correct """
|
""" Test that substitutions are correct """
|
||||||
# 0 albums, 2 keywords, 0 persons, default vals provided
|
# 0 albums, 2 keywords, 0 persons, default vals provided
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
""" Builds the template table in markdown format for README.md """
|
""" Builds the template table in markdown format for README.md """
|
||||||
|
|
||||||
from osxphotos.template import TEMPLATE_SUBSTITUTIONS
|
from osxphotos.template import (
|
||||||
|
TEMPLATE_SUBSTITUTIONS,
|
||||||
|
TEMPLATE_SUBSTITUTIONS_MULTI_VALUED,
|
||||||
|
)
|
||||||
|
|
||||||
print("| Substitution | Description |")
|
print("| Substitution | Description |")
|
||||||
print("|--------------|-------------|")
|
print("|--------------|-------------|")
|
||||||
for subst, descr in TEMPLATE_SUBSTITUTIONS.items():
|
for subst, descr in [
|
||||||
|
*TEMPLATE_SUBSTITUTIONS.items(),
|
||||||
|
*TEMPLATE_SUBSTITUTIONS_MULTI_VALUED.items(),
|
||||||
|
]:
|
||||||
print(f"|{subst}|{descr}|")
|
print(f"|{subst}|{descr}|")
|
||||||
|
|||||||
Reference in New Issue
Block a user