diff --git a/README.md b/README.md index 0736a8e9..e681382d 100644 --- a/README.md +++ b/README.md @@ -96,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 Specify Photos database path. Path to Photos @@ -108,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 @@ -172,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 @@ -184,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 @@ -193,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 @@ -225,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, @@ -1301,8 +1310,8 @@ Testing against "real world" Photos libraries would be especially helpful. If y 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) -- 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 if `--export-bursts` option is used. See [Issue #75](https://github.com/RhetTbull/osxphotos/issues/75) +- 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 diff --git a/osxphotos/__main__.py b/osxphotos/__main__.py index ef6f7a78..d8f4b039 100644 --- a/osxphotos/__main__.py +++ b/osxphotos/__main__.py @@ -891,12 +891,14 @@ def query( is_flag=True, 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.", + "(e.g. the RAW file was imported to Photos without a jpeg preview).", ) @click.option( - "--original-name", + "--current-name", is_flag=True, - help="Use photo's original filename instead of current filename for export.", + 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", @@ -919,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( @@ -981,7 +983,7 @@ def export( skip_bursts, skip_live, skip_raw, - original_name, + current_name, sidecar, only_photos, only_movies, @@ -1056,6 +1058,11 @@ def export( 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: diff --git a/osxphotos/_version.py b/osxphotos/_version.py index 191e7ce5..f394d84c 100644 --- a/osxphotos/_version.py +++ b/osxphotos/_version.py @@ -1,3 +1,3 @@ """ version info """ -__version__ = "0.28.0" +__version__ = "0.28.1" diff --git a/tests/test_cli.py b/tests/test_cli.py index f3ba7c1c..f0828d02 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -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,14 +222,31 @@ 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", "-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 @@ -230,14 +259,7 @@ def test_export_skip_edited(): # pylint: disable=not-context-manager with runner.isolated_filesystem(): result = runner.invoke( - export, - [ - os.path.join(cwd, CLI_PHOTOS_DB), - ".", - "--skip-edited", - "--original-name", - "-V", - ], + export, [os.path.join(cwd, CLI_PHOTOS_DB), ".", "--skip-edited", "-V"] ) assert result.exit_code == 0 files = glob.glob("*") @@ -291,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}", @@ -314,8 +335,7 @@ 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", "-V"], + export, [os.path.join(cwd, LIVE_PHOTOS_DB), ".", "--live", "-V"] ) files = glob.glob("*") assert sorted(files) == sorted(CLI_EXPORT_LIVE_ORIGINAL) @@ -333,14 +353,7 @@ def test_export_skip_live(): # pylint: disable=not-context-manager with runner.isolated_filesystem(): result = runner.invoke( - export, - [ - os.path.join(cwd, LIVE_PHOTOS_DB), - ".", - "--skip-live", - "--original-name", - "-V", - ], + 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] @@ -358,7 +371,14 @@ def test_export_raw(): # pylint: disable=not-context-manager with runner.isolated_filesystem(): result = runner.invoke( - export, [os.path.join(cwd, RAW_PHOTOS_DB), ".", "--skip-edited", "-V"] + export, + [ + os.path.join(cwd, RAW_PHOTOS_DB), + ".", + "--current-name", + "--skip-edited", + "-V", + ], ) files = glob.glob("*") assert sorted(files) == sorted(CLI_EXPORT_RAW) @@ -396,14 +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), - ".", - "--skip-edited", - "--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) @@ -420,7 +433,9 @@ def test_export_raw_edited(): 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", "-V"] + ) files = glob.glob("*") assert sorted(files) == sorted(CLI_EXPORT_RAW_EDITED) @@ -436,9 +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), ".", "--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) @@ -460,7 +473,6 @@ def test_export_directory_template_1(): [ os.path.join(cwd, CLI_PHOTOS_DB), ".", - "--original-name", "-V", "--directory", "{created.year}/{created.month}", @@ -489,7 +501,6 @@ def test_export_directory_template_2(): [ os.path.join(cwd, CLI_PHOTOS_DB), ".", - "--original-name", "-V", "--directory", "{place.name}", @@ -518,7 +529,6 @@ def test_export_directory_template_3(): [ os.path.join(cwd, CLI_PHOTOS_DB), ".", - "--original-name", "-V", "--directory", "{created.year}/{foo}", @@ -542,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() @@ -575,7 +578,6 @@ def test_export_directory_template_album_2(): [ os.path.join(cwd, CLI_PHOTOS_DB), ".", - "--original-name", "-V", "--directory", "{album,NOALBUM}",