Added --deleted, --deleted-only to CLI, closes #179
This commit is contained in:
@@ -207,6 +207,10 @@ Options:
|
||||
Search by end item date, e.g.
|
||||
2000-01-12T12:00:00 or 2000-12-31 (ISO 8601
|
||||
w/o TZ).
|
||||
--deleted Include photos from the 'Recently Deleted'
|
||||
folder.
|
||||
--deleted-only Include only photos from the 'Recently
|
||||
Deleted' folder.
|
||||
--update Only export new or updated files. See notes
|
||||
below on export and --update.
|
||||
--dry-run Dry run (test) the export but don't actually
|
||||
@@ -520,6 +524,10 @@ Example: export photos to file structure based on 4-digit year and full name of
|
||||
|
||||
`osxphotos export ~/Desktop/export --directory "{created.year}/{created.month}"`
|
||||
|
||||
Example: export default library using 'country name/year' as output directory (but use "NoCountry/year" if country not specified), add persons, album names, and year as keywords, write exif metadata to files when exporting, update only changed files, print verbose ouput
|
||||
|
||||
`osxphotos export ~/Desktop/export --directory "{place.name.country,NoCountry}/{created.year}" --person-keyword --album-keyword --keyword-template "{created.year}" --exiftool --update --verbose`
|
||||
|
||||
|
||||
## Example uses of the package
|
||||
|
||||
|
||||
@@ -236,6 +236,25 @@ JSON_OPTION = click.option(
|
||||
)
|
||||
|
||||
|
||||
def deleted_options(f):
|
||||
o = click.option
|
||||
options = [
|
||||
o(
|
||||
"--deleted",
|
||||
is_flag=True,
|
||||
help="Include photos from the 'Recently Deleted' folder.",
|
||||
),
|
||||
o(
|
||||
"--deleted-only",
|
||||
is_flag=True,
|
||||
help="Include only photos from the 'Recently Deleted' folder.",
|
||||
),
|
||||
]
|
||||
for o in options[::-1]:
|
||||
f = o(f)
|
||||
return f
|
||||
|
||||
|
||||
def query_options(f):
|
||||
o = click.option
|
||||
options = [
|
||||
@@ -745,10 +764,11 @@ def places(ctx, cli_obj, db, json_, photos_library):
|
||||
@cli.command()
|
||||
@DB_OPTION
|
||||
@JSON_OPTION
|
||||
@deleted_options
|
||||
@DB_ARGUMENT
|
||||
@click.pass_obj
|
||||
@click.pass_context
|
||||
def dump(ctx, cli_obj, db, json_, photos_library):
|
||||
def dump(ctx, cli_obj, db, json_, deleted, deleted_only, photos_library):
|
||||
""" Print list of all photos & associated info from the Photos library. """
|
||||
|
||||
db = get_photos_db(*photos_library, db, cli_obj.db)
|
||||
@@ -758,8 +778,20 @@ def dump(ctx, cli_obj, db, json_, photos_library):
|
||||
_list_libraries()
|
||||
return
|
||||
|
||||
# check exclusive options
|
||||
if deleted and deleted_only:
|
||||
click.echo("Incompatible dump options", err=True)
|
||||
click.echo(cli.commands["dump"].get_help(ctx), err=True)
|
||||
return
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=db)
|
||||
photos = photosdb.photos(movies=True)
|
||||
if deleted or deleted_only:
|
||||
photos = photosdb.photos(movies=True, intrash=True)
|
||||
else:
|
||||
photos = []
|
||||
if not deleted_only:
|
||||
photos += photosdb.photos(movies=True)
|
||||
|
||||
print_photo_info(photos, json_ or cli_obj.json)
|
||||
|
||||
|
||||
@@ -815,6 +847,7 @@ def _list_libraries(json_=False, error=True):
|
||||
@DB_OPTION
|
||||
@JSON_OPTION
|
||||
@query_options
|
||||
@deleted_options
|
||||
@click.option("--missing", is_flag=True, help="Search for photos missing from disk.")
|
||||
@click.option(
|
||||
"--not-missing",
|
||||
@@ -901,6 +934,8 @@ def query(
|
||||
place,
|
||||
no_place,
|
||||
label,
|
||||
deleted,
|
||||
deleted_only,
|
||||
):
|
||||
""" Query the Photos database using 1 or more search options;
|
||||
if more than one option is provided, they are treated as "AND"
|
||||
@@ -942,9 +977,12 @@ def query(
|
||||
(selfie, not_selfie),
|
||||
(panorama, not_panorama),
|
||||
(any(place), no_place),
|
||||
(deleted, deleted_only),
|
||||
]
|
||||
# print help if no non-exclusive term or a double exclusive term is given
|
||||
if not any(nonexclusive + [b ^ n for b, n in exclusive]):
|
||||
if any(all(bb) for bb in exclusive) or not any(
|
||||
nonexclusive + [b ^ n for b, n in exclusive]
|
||||
):
|
||||
click.echo("Incompatible query options", err=True)
|
||||
click.echo(cli.commands["query"].get_help(ctx), err=True)
|
||||
return
|
||||
@@ -1018,6 +1056,8 @@ def query(
|
||||
place=place,
|
||||
no_place=no_place,
|
||||
label=label,
|
||||
deleted=deleted,
|
||||
deleted_only=deleted_only,
|
||||
)
|
||||
|
||||
# below needed for to make CliRunner work for testing
|
||||
@@ -1029,6 +1069,7 @@ def query(
|
||||
@DB_OPTION
|
||||
@click.option("--verbose", "-V", "verbose_", is_flag=True, help="Print verbose output.")
|
||||
@query_options
|
||||
@deleted_options
|
||||
@click.option(
|
||||
"--update",
|
||||
is_flag=True,
|
||||
@@ -1252,6 +1293,8 @@ def export(
|
||||
no_place,
|
||||
no_extended_attributes,
|
||||
label,
|
||||
deleted,
|
||||
deleted_only,
|
||||
):
|
||||
""" Export photos from the Photos database.
|
||||
Export path DEST is required.
|
||||
@@ -1290,6 +1333,7 @@ def export(
|
||||
(export_by_date, directory),
|
||||
(export_as_hardlink, exiftool),
|
||||
(any(place), no_place),
|
||||
(deleted, deleted_only),
|
||||
]
|
||||
if any(all(bb) for bb in exclusive):
|
||||
click.echo("Incompatible export options", err=True)
|
||||
@@ -1411,6 +1455,8 @@ def export(
|
||||
place=place,
|
||||
no_place=no_place,
|
||||
label=label,
|
||||
deleted=deleted,
|
||||
deleted_only=deleted_only,
|
||||
)
|
||||
|
||||
results_exported = []
|
||||
@@ -1597,6 +1643,7 @@ def print_photo_info(photos, json=False):
|
||||
"has_raw",
|
||||
"uti_raw",
|
||||
"path_raw",
|
||||
"intrash",
|
||||
]
|
||||
)
|
||||
for p in photos:
|
||||
@@ -1641,6 +1688,7 @@ def print_photo_info(photos, json=False):
|
||||
p.has_raw,
|
||||
p.uti_raw,
|
||||
p.path_raw,
|
||||
p.intrash,
|
||||
]
|
||||
)
|
||||
for row in dump:
|
||||
@@ -1700,6 +1748,8 @@ def _query(
|
||||
place=None,
|
||||
no_place=None,
|
||||
label=None,
|
||||
deleted=False,
|
||||
deleted_only=False,
|
||||
):
|
||||
""" run a query against PhotosDB to extract the photos based on user supply criteria
|
||||
used by query and export commands
|
||||
@@ -1707,9 +1757,25 @@ def _query(
|
||||
if either is modified, need to ensure all three functions are updated """
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=db)
|
||||
photos = photosdb.photos(
|
||||
uuid=uuid, images=isphoto, movies=ismovie, from_date=from_date, to_date=to_date
|
||||
)
|
||||
if deleted or deleted_only:
|
||||
photos = photosdb.photos(
|
||||
uuid=uuid,
|
||||
images=isphoto,
|
||||
movies=ismovie,
|
||||
from_date=from_date,
|
||||
to_date=to_date,
|
||||
intrash=True,
|
||||
)
|
||||
else:
|
||||
photos = []
|
||||
if not deleted_only:
|
||||
photos += photosdb.photos(
|
||||
uuid=uuid,
|
||||
images=isphoto,
|
||||
movies=ismovie,
|
||||
from_date=from_date,
|
||||
to_date=to_date,
|
||||
)
|
||||
|
||||
if album:
|
||||
photos = get_photos_by_attribute(photos, "albums", album, ignore_case)
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
""" version info """
|
||||
|
||||
__version__ = "0.30.1"
|
||||
__version__ = "0.30.2"
|
||||
|
||||
@@ -753,6 +753,7 @@ class PhotoInfo:
|
||||
"place": self.place,
|
||||
"exif": exif,
|
||||
"score": score,
|
||||
"intrash": self.intrash,
|
||||
}
|
||||
return yaml.dump(info, sort_keys=False)
|
||||
|
||||
@@ -812,6 +813,7 @@ class PhotoInfo:
|
||||
"place": place,
|
||||
"exif": exif,
|
||||
"score": score,
|
||||
"intrash": self.intrash,
|
||||
}
|
||||
return json.dumps(pic)
|
||||
|
||||
|
||||
@@ -197,6 +197,17 @@ CLI_EXPORT_RAW_EDITED = [
|
||||
]
|
||||
CLI_EXPORT_RAW_EDITED_ORIGINAL = ["IMG_0476_2.CR2", "IMG_0476_2_edited.jpeg"]
|
||||
|
||||
CLI_UUID_DICT_15_5 = {"intrash": "71E3E212-00EB-430D-8A63-5E294B268554"}
|
||||
CLI_UUID_DICT_14_6 = {"intrash": "3tljdX43R8+k6peNHVrJNQ"}
|
||||
|
||||
PHOTOS_NOT_IN_TRASH_LEN_14_6 = 7
|
||||
PHOTOS_IN_TRASH_LEN_14_6 = 1
|
||||
PHOTOS_MISSING_14_6 = 1
|
||||
|
||||
PHOTOS_NOT_IN_TRASH_LEN_15_5 = 13
|
||||
PHOTOS_IN_TRASH_LEN_15_5 = 1
|
||||
PHOTOS_MISSING_15_5 = 2
|
||||
|
||||
CLI_PLACES_JSON = """{"places": {"_UNKNOWN_": 1, "Maui, Wailea, Hawai'i, United States": 1, "Washington, District of Columbia, United States": 1}}"""
|
||||
|
||||
CLI_EXIFTOOL = {
|
||||
@@ -957,6 +968,103 @@ def test_query_label_4():
|
||||
assert len(json_got) == 6
|
||||
|
||||
|
||||
def test_query_deleted_deleted_only():
|
||||
"""Test query with --deleted and --deleted-only"""
|
||||
import json
|
||||
import osxphotos
|
||||
import os
|
||||
import os.path
|
||||
from osxphotos.__main__ import query
|
||||
|
||||
runner = CliRunner()
|
||||
cwd = os.getcwd()
|
||||
result = runner.invoke(
|
||||
query,
|
||||
[
|
||||
"--json",
|
||||
"--db",
|
||||
os.path.join(cwd, PHOTOS_DB_15_5),
|
||||
"--deleted",
|
||||
"--deleted-only",
|
||||
],
|
||||
)
|
||||
assert "Incompatible query options" in result.output
|
||||
|
||||
|
||||
def test_query_deleted_1():
|
||||
"""Test query with --deleted"""
|
||||
import json
|
||||
import osxphotos
|
||||
import os
|
||||
import os.path
|
||||
from osxphotos.__main__ import query
|
||||
|
||||
runner = CliRunner()
|
||||
cwd = os.getcwd()
|
||||
result = runner.invoke(
|
||||
query, ["--json", "--db", os.path.join(cwd, PHOTOS_DB_15_5), "--deleted"]
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
json_got = json.loads(result.output)
|
||||
assert len(json_got) == PHOTOS_NOT_IN_TRASH_LEN_15_5 + PHOTOS_IN_TRASH_LEN_15_5
|
||||
|
||||
|
||||
def test_query_deleted_2():
|
||||
"""Test query with --deleted"""
|
||||
import json
|
||||
import osxphotos
|
||||
import os
|
||||
import os.path
|
||||
from osxphotos.__main__ import query
|
||||
|
||||
runner = CliRunner()
|
||||
cwd = os.getcwd()
|
||||
result = runner.invoke(
|
||||
query, ["--json", "--db", os.path.join(cwd, PHOTOS_DB_14_6), "--deleted"]
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
json_got = json.loads(result.output)
|
||||
assert len(json_got) == PHOTOS_NOT_IN_TRASH_LEN_14_6 + PHOTOS_IN_TRASH_LEN_14_6
|
||||
|
||||
|
||||
def test_query_deleted_3():
|
||||
"""Test query with --deleted-only"""
|
||||
import json
|
||||
import osxphotos
|
||||
import os
|
||||
import os.path
|
||||
from osxphotos.__main__ import query
|
||||
|
||||
runner = CliRunner()
|
||||
cwd = os.getcwd()
|
||||
result = runner.invoke(
|
||||
query, ["--json", "--db", os.path.join(cwd, PHOTOS_DB_15_5), "--deleted-only"]
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
json_got = json.loads(result.output)
|
||||
assert len(json_got) == PHOTOS_IN_TRASH_LEN_15_5
|
||||
assert json_got[0]["intrash"]
|
||||
|
||||
|
||||
def test_query_deleted_4():
|
||||
"""Test query with --deleted-only"""
|
||||
import json
|
||||
import osxphotos
|
||||
import os
|
||||
import os.path
|
||||
from osxphotos.__main__ import query
|
||||
|
||||
runner = CliRunner()
|
||||
cwd = os.getcwd()
|
||||
result = runner.invoke(
|
||||
query, ["--json", "--db", os.path.join(cwd, PHOTOS_DB_14_6), "--deleted-only"]
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
json_got = json.loads(result.output)
|
||||
assert len(json_got) == PHOTOS_IN_TRASH_LEN_14_6
|
||||
assert json_got[0]["intrash"]
|
||||
|
||||
|
||||
def test_export_sidecar():
|
||||
import glob
|
||||
import os
|
||||
@@ -1434,6 +1542,138 @@ def test_export_album_deleted_twin():
|
||||
assert sorted(files) == sorted(CLI_EXPORT_FILENAMES_DELETED_TWIN)
|
||||
|
||||
|
||||
def test_export_deleted_1():
|
||||
"""Test export with --deleted """
|
||||
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():
|
||||
skip = ["--skip-edited", "--skip-bursts", "--skip-live", "--skip-raw"]
|
||||
result = runner.invoke(
|
||||
export, [os.path.join(cwd, PHOTOS_DB_15_5), ".", "--deleted", *skip]
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
files = glob.glob("*")
|
||||
assert (
|
||||
len(files)
|
||||
== PHOTOS_NOT_IN_TRASH_LEN_15_5
|
||||
+ PHOTOS_IN_TRASH_LEN_15_5
|
||||
- PHOTOS_MISSING_15_5
|
||||
)
|
||||
|
||||
|
||||
def test_export_deleted_2():
|
||||
"""Test export with --deleted """
|
||||
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():
|
||||
skip = ["--skip-edited", "--skip-bursts", "--skip-live", "--skip-raw"]
|
||||
result = runner.invoke(
|
||||
export, [os.path.join(cwd, PHOTOS_DB_14_6), ".", "--deleted", *skip]
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
files = glob.glob("*")
|
||||
assert (
|
||||
len(files)
|
||||
== PHOTOS_NOT_IN_TRASH_LEN_14_6
|
||||
+ PHOTOS_IN_TRASH_LEN_14_6
|
||||
- PHOTOS_MISSING_14_6
|
||||
)
|
||||
|
||||
|
||||
def test_export_not_deleted_1():
|
||||
"""Test export does not find intrash files without --deleted flag """
|
||||
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():
|
||||
skip = ["--skip-edited", "--skip-bursts", "--skip-live", "--skip-raw"]
|
||||
result = runner.invoke(export, [os.path.join(cwd, PHOTOS_DB_15_5), ".", *skip])
|
||||
assert result.exit_code == 0
|
||||
files = glob.glob("*")
|
||||
assert len(files) == PHOTOS_NOT_IN_TRASH_LEN_15_5 - PHOTOS_MISSING_15_5
|
||||
|
||||
|
||||
def test_export_not_deleted_2():
|
||||
"""Test export does not find intrash files without --deleted flag """
|
||||
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():
|
||||
skip = ["--skip-edited", "--skip-bursts", "--skip-live", "--skip-raw"]
|
||||
result = runner.invoke(export, [os.path.join(cwd, PHOTOS_DB_14_6), ".", *skip])
|
||||
assert result.exit_code == 0
|
||||
files = glob.glob("*")
|
||||
assert len(files) == PHOTOS_NOT_IN_TRASH_LEN_14_6 - PHOTOS_MISSING_14_6
|
||||
|
||||
|
||||
def test_export_deleted_only_1():
|
||||
"""Test export with --deleted-only """
|
||||
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():
|
||||
skip = ["--skip-edited", "--skip-bursts", "--skip-live", "--skip-raw"]
|
||||
result = runner.invoke(
|
||||
export, [os.path.join(cwd, PHOTOS_DB_15_5), ".", "--deleted-only", *skip]
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
files = glob.glob("*")
|
||||
assert len(files) == PHOTOS_IN_TRASH_LEN_15_5
|
||||
|
||||
|
||||
def test_export_deleted_only_2():
|
||||
"""Test export with --deleted-only """
|
||||
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():
|
||||
skip = ["--skip-edited", "--skip-bursts", "--skip-live", "--skip-raw"]
|
||||
result = runner.invoke(
|
||||
export, [os.path.join(cwd, PHOTOS_DB_14_6), ".", "--deleted-only", *skip]
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
files = glob.glob("*")
|
||||
assert len(files) == PHOTOS_IN_TRASH_LEN_14_6
|
||||
|
||||
|
||||
def test_places():
|
||||
import json
|
||||
import os
|
||||
|
||||
Reference in New Issue
Block a user