Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
116cb662fb | ||
|
|
db68defc44 | ||
|
|
7460bc88fc | ||
|
|
dbbbbf10a8 | ||
|
|
0633814ab2 | ||
|
|
df7d45659a | ||
|
|
cec266bba4 | ||
|
|
d0d2e80800 | ||
|
|
aafdbea564 | ||
|
|
c42050a10c | ||
|
|
c27cfb1223 | ||
|
|
ad144da8a0 | ||
|
|
5352aec3b9 | ||
|
|
e951e5361e | ||
|
|
f7bd1376e1 | ||
|
|
26f96d582c | ||
|
|
8cb15d1555 | ||
|
|
2d9429c8ee | ||
|
|
3b6dd08d2b | ||
|
|
3c85f26f90 | ||
|
|
52c054f81f | ||
|
|
8dc59cbc35 | ||
|
|
802e2f069a | ||
|
|
5d4d7d7db7 | ||
|
|
ea9b41bae4 |
@@ -100,6 +100,15 @@
|
|||||||
"contributions": [
|
"contributions": [
|
||||||
"code"
|
"code"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "jstrine",
|
||||||
|
"name": "Jonathan Strine",
|
||||||
|
"avatar_url": "https://avatars1.githubusercontent.com/u/33943447?v=4",
|
||||||
|
"profile": "https://github.com/jstrine",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"contributorsPerLine": 7
|
"contributorsPerLine": 7
|
||||||
|
|||||||
47
CHANGELOG.md
47
CHANGELOG.md
@@ -4,6 +4,53 @@ 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).
|
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
||||||
|
|
||||||
|
#### [v0.36.20](https://github.com/RhetTbull/osxphotos/compare/v0.36.19...v0.36.20)
|
||||||
|
|
||||||
|
> 23 November 2020
|
||||||
|
|
||||||
|
- Added photokit export as hidden --use-photokit option [`26f96d5`](https://github.com/RhetTbull/osxphotos/commit/26f96d582c01ce9816b1f54f0e74c8570f133f7c)
|
||||||
|
|
||||||
|
#### [v0.36.19](https://github.com/RhetTbull/osxphotos/compare/v0.36.18...v0.36.19)
|
||||||
|
|
||||||
|
> 19 November 2020
|
||||||
|
|
||||||
|
- Removed debug statement in _photoinfo_export [`8cb15d1`](https://github.com/RhetTbull/osxphotos/commit/8cb15d15551094dcaf1b0ef32d6ac0273be7fd37)
|
||||||
|
|
||||||
|
#### [v0.36.18](https://github.com/RhetTbull/osxphotos/compare/v0.36.17...v0.36.18)
|
||||||
|
|
||||||
|
> 14 November 2020
|
||||||
|
|
||||||
|
- Moved AppleScript to photoscript [`3c85f26`](https://github.com/RhetTbull/osxphotos/commit/3c85f26f901645ce297685ccd639792757fbc995)
|
||||||
|
- Fixed missing data file for photoscript [`2d9429c`](https://github.com/RhetTbull/osxphotos/commit/2d9429c8eefabe6233fc580f65511c48ee6c01e5)
|
||||||
|
- Version bump, updated requirements [`3b6dd08`](https://github.com/RhetTbull/osxphotos/commit/3b6dd08d2bb2b20a55064bf24fe7ce788e7268ef)
|
||||||
|
|
||||||
|
#### [v0.36.17](https://github.com/RhetTbull/osxphotos/compare/v0.36.15...v0.36.17)
|
||||||
|
|
||||||
|
> 12 November 2020
|
||||||
|
|
||||||
|
- Fixed path for photos actually missing off disk [`5d4d7d7`](https://github.com/RhetTbull/osxphotos/commit/5d4d7d7db7ca1109b6230803fe777d7a30882efe)
|
||||||
|
- Fixed erroneous attempt to export edited with --download-missing [`8dc59cb`](https://github.com/RhetTbull/osxphotos/commit/8dc59cbc35c33e71d0d912f4139e855180ac4fbd)
|
||||||
|
- version bump [`802e2f0`](https://github.com/RhetTbull/osxphotos/commit/802e2f069a5f8b37ddc6b3b8ba07519ce10f88a7)
|
||||||
|
|
||||||
|
#### [v0.36.15](https://github.com/RhetTbull/osxphotos/compare/v0.36.14...v0.36.15)
|
||||||
|
|
||||||
|
> 11 November 2020
|
||||||
|
|
||||||
|
- Avoid copying db files if not necessary [`ea9b41b`](https://github.com/RhetTbull/osxphotos/commit/ea9b41bae41a05aad53454f67871c5e6c9a49f79)
|
||||||
|
|
||||||
|
#### [v0.36.14](https://github.com/RhetTbull/osxphotos/compare/v0.36.13...v0.36.14)
|
||||||
|
|
||||||
|
> 9 November 2020
|
||||||
|
|
||||||
|
- Fix for issue #247 [`38397b5`](https://github.com/RhetTbull/osxphotos/commit/38397b507b456169cf3be2d2dc6743ec8653feb3)
|
||||||
|
|
||||||
|
#### [v0.36.13](https://github.com/RhetTbull/osxphotos/compare/v0.36.11...v0.36.13)
|
||||||
|
|
||||||
|
> 9 November 2020
|
||||||
|
|
||||||
|
- Refactored phototemplate.py to add PATH_SEP option [`3636fcb`](https://github.com/RhetTbull/osxphotos/commit/3636fcbc76100d9898a59f24ed6e9b1965cc6022)
|
||||||
|
- More work on phototemplate.py to add inline expansion [`a6231e2`](https://github.com/RhetTbull/osxphotos/commit/a6231e29ff28b2c7dc3239445f41afcb35926a7a)
|
||||||
|
|
||||||
#### [v0.36.11](https://github.com/RhetTbull/osxphotos/compare/v0.36.10...v0.36.11)
|
#### [v0.36.11](https://github.com/RhetTbull/osxphotos/compare/v0.36.10...v0.36.11)
|
||||||
|
|
||||||
> 8 November 2020
|
> 8 November 2020
|
||||||
|
|||||||
13
README.md
13
README.md
@@ -3,7 +3,7 @@
|
|||||||
[](https://opensource.org/licenses/MIT)
|
[](https://opensource.org/licenses/MIT)
|
||||||
[](https://github.com/RhetTbull/osxphotos/workflows/Python%20package/badge.svg)
|
[](https://github.com/RhetTbull/osxphotos/workflows/Python%20package/badge.svg)
|
||||||
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
|
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
|
||||||
[](#contributors-)
|
[](#contributors-)
|
||||||
<!-- ALL-CONTRIBUTORS-BADGE:END -->
|
<!-- ALL-CONTRIBUTORS-BADGE:END -->
|
||||||
|
|
||||||
- [OSXPhotos](#osxphotos)
|
- [OSXPhotos](#osxphotos)
|
||||||
@@ -356,6 +356,16 @@ Options:
|
|||||||
to a filesystem that doesn't support Mac OS
|
to a filesystem that doesn't support Mac OS
|
||||||
extended attributes. Only use this if you
|
extended attributes. Only use this if you
|
||||||
get an error while exporting.
|
get an error while exporting.
|
||||||
|
--use-photos-export Force the use of AppleScript or PhotoKit to
|
||||||
|
export even if not missing (see also '--
|
||||||
|
download-missing' and '--use-photokit').
|
||||||
|
--use-photokit Use with '--download-missing' or '--use-
|
||||||
|
photos-export' to use direct Photos
|
||||||
|
interface instead of AppleScript to export.
|
||||||
|
Highly experimental alpha feature; does not
|
||||||
|
work with iTerm2 (use with Terminal.app).
|
||||||
|
This is faster and more reliable than the
|
||||||
|
default AppleScript interface.
|
||||||
-h, --help Show this message and exit.
|
-h, --help Show this message and exit.
|
||||||
|
|
||||||
** Export **
|
** Export **
|
||||||
@@ -2149,6 +2159,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
|||||||
<td align="center"><a href="https://github.com/grundsch"><img src="https://avatars0.githubusercontent.com/u/3874928?v=4?s=100" width="100px;" alt=""/><br /><sub><b>grundsch</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=grundsch" title="Code">💻</a></td>
|
<td align="center"><a href="https://github.com/grundsch"><img src="https://avatars0.githubusercontent.com/u/3874928?v=4?s=100" width="100px;" alt=""/><br /><sub><b>grundsch</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=grundsch" title="Code">💻</a></td>
|
||||||
<td align="center"><a href="https://github.com/agprimatic"><img src="https://avatars1.githubusercontent.com/u/4685054?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Ag Primatic</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=agprimatic" title="Code">💻</a></td>
|
<td align="center"><a href="https://github.com/agprimatic"><img src="https://avatars1.githubusercontent.com/u/4685054?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Ag Primatic</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=agprimatic" title="Code">💻</a></td>
|
||||||
<td align="center"><a href="https://github.com/hhoeck"><img src="https://avatars1.githubusercontent.com/u/6313998?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Horst Höck</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=hhoeck" title="Code">💻</a></td>
|
<td align="center"><a href="https://github.com/hhoeck"><img src="https://avatars1.githubusercontent.com/u/6313998?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Horst Höck</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=hhoeck" title="Code">💻</a></td>
|
||||||
|
<td align="center"><a href="https://github.com/jstrine"><img src="https://avatars1.githubusercontent.com/u/33943447?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Jonathan Strine</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=jstrine" title="Code">💻</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
|||||||
@@ -5,4 +5,4 @@
|
|||||||
# If you need to install pyinstaller:
|
# If you need to install pyinstaller:
|
||||||
# python3 -m pip install --upgrade pyinstaller
|
# python3 -m pip install --upgrade pyinstaller
|
||||||
|
|
||||||
pyinstaller --onefile --hidden-import="pkg_resources.py2_warn" --name osxphotos --add-data osxphotos/templates/xmp_sidecar.mako:osxphotos/templates cli.py
|
pyinstaller osxphotos.spec
|
||||||
48
osxphotos.spec
Normal file
48
osxphotos.spec
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
# -*- mode: python ; coding: utf-8 -*-
|
||||||
|
# spec file for pyinstaller
|
||||||
|
# run `pyinstaller osxphotos.spec`
|
||||||
|
|
||||||
|
import os
|
||||||
|
import importlib
|
||||||
|
|
||||||
|
pathex = os.getcwd()
|
||||||
|
|
||||||
|
# include necessary data files
|
||||||
|
datas=[('osxphotos/templates/xmp_sidecar.mako', 'osxphotos/templates')]
|
||||||
|
package_imports = [['photoscript', ['photoscript.applescript']]]
|
||||||
|
for package, files in package_imports:
|
||||||
|
proot = os.path.dirname(importlib.import_module(package).__file__)
|
||||||
|
datas.extend((os.path.join(proot, f), package) for f in files)
|
||||||
|
|
||||||
|
block_cipher = None
|
||||||
|
|
||||||
|
a = Analysis(['cli.py'],
|
||||||
|
pathex=[pathex],
|
||||||
|
binaries=[],
|
||||||
|
datas=datas,
|
||||||
|
hiddenimports=['pkg_resources.py2_warn'],
|
||||||
|
hookspath=[],
|
||||||
|
runtime_hooks=[],
|
||||||
|
excludes=[],
|
||||||
|
win_no_prefer_redirects=False,
|
||||||
|
win_private_assemblies=False,
|
||||||
|
cipher=block_cipher,
|
||||||
|
noarchive=False)
|
||||||
|
|
||||||
|
pyz = PYZ(a.pure, a.zipped_data,
|
||||||
|
cipher=block_cipher)
|
||||||
|
|
||||||
|
exe = EXE(pyz,
|
||||||
|
a.scripts,
|
||||||
|
a.binaries,
|
||||||
|
a.zipfiles,
|
||||||
|
a.datas,
|
||||||
|
[],
|
||||||
|
name='osxphotos',
|
||||||
|
debug=False,
|
||||||
|
bootloader_ignore_signals=False,
|
||||||
|
strip=False,
|
||||||
|
upx=True,
|
||||||
|
upx_exclude=[],
|
||||||
|
runtime_tmpdir=None,
|
||||||
|
console=True )
|
||||||
@@ -28,6 +28,7 @@ from .export_db import ExportDB, ExportDBInMemory
|
|||||||
from .fileutil import FileUtil, FileUtilNoOp
|
from .fileutil import FileUtil, FileUtilNoOp
|
||||||
from .path_utils import is_valid_filepath, sanitize_filename, sanitize_filepath
|
from .path_utils import is_valid_filepath, sanitize_filename, sanitize_filepath
|
||||||
from .photoinfo import ExportResults
|
from .photoinfo import ExportResults
|
||||||
|
from .photokit import check_photokit_authorization, request_photokit_authorization
|
||||||
from .phototemplate import TEMPLATE_SUBSTITUTIONS, TEMPLATE_SUBSTITUTIONS_MULTI_VALUED
|
from .phototemplate import TEMPLATE_SUBSTITUTIONS, TEMPLATE_SUBSTITUTIONS_MULTI_VALUED
|
||||||
|
|
||||||
# global variable to control verbose output
|
# global variable to control verbose output
|
||||||
@@ -1394,8 +1395,15 @@ def query(
|
|||||||
"--use-photos-export",
|
"--use-photos-export",
|
||||||
is_flag=True,
|
is_flag=True,
|
||||||
default=False,
|
default=False,
|
||||||
hidden=True,
|
help="Force the use of AppleScript or PhotoKit to export even if not missing (see also '--download-missing' and '--use-photokit').",
|
||||||
help="Force the use of AppleScript to export even if not missing (see also --download-missing).",
|
)
|
||||||
|
@click.option(
|
||||||
|
"--use-photokit",
|
||||||
|
is_flag=True,
|
||||||
|
default=False,
|
||||||
|
help="Use with '--download-missing' or '--use-photos-export' to use direct Photos interface instead of AppleScript to export. "
|
||||||
|
"Highly experimental alpha feature; does not work with iTerm2 (use with Terminal.app). "
|
||||||
|
"This is faster and more reliable than the default AppleScript interface.",
|
||||||
)
|
)
|
||||||
@DB_ARGUMENT
|
@DB_ARGUMENT
|
||||||
@click.argument("dest", nargs=1, type=click.Path(exists=True))
|
@click.argument("dest", nargs=1, type=click.Path(exists=True))
|
||||||
@@ -1487,6 +1495,7 @@ def export(
|
|||||||
deleted,
|
deleted,
|
||||||
deleted_only,
|
deleted_only,
|
||||||
use_photos_export,
|
use_photos_export,
|
||||||
|
use_photokit,
|
||||||
):
|
):
|
||||||
""" Export photos from the Photos database.
|
""" Export photos from the Photos database.
|
||||||
Export path DEST is required.
|
Export path DEST is required.
|
||||||
@@ -1537,6 +1546,18 @@ def export(
|
|||||||
click.echo(cli.commands["export"].get_help(ctx), err=True)
|
click.echo(cli.commands["export"].get_help(ctx), err=True)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if use_photokit and not check_photokit_authorization():
|
||||||
|
click.echo(
|
||||||
|
"Requesting access to use your Photos library. Click 'OK' on the dialog box to grant access."
|
||||||
|
)
|
||||||
|
request_photokit_authorization()
|
||||||
|
click.confirm("Have you granted access?")
|
||||||
|
if not check_photokit_authorization():
|
||||||
|
click.echo(
|
||||||
|
"Failed to get access to the Photos library which is needed with `--use-photokit`."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
# initialize export flags
|
# initialize export flags
|
||||||
# by default, will export all versions of photos unless skip flag is set
|
# by default, will export all versions of photos unless skip flag is set
|
||||||
(export_edited, export_bursts, export_live, export_raw) = [
|
(export_edited, export_bursts, export_live, export_raw) = [
|
||||||
@@ -1733,6 +1754,7 @@ def export(
|
|||||||
convert_to_jpeg=convert_to_jpeg,
|
convert_to_jpeg=convert_to_jpeg,
|
||||||
jpeg_quality=jpeg_quality,
|
jpeg_quality=jpeg_quality,
|
||||||
ignore_date_modified=ignore_date_modified,
|
ignore_date_modified=ignore_date_modified,
|
||||||
|
use_photokit=use_photokit,
|
||||||
)
|
)
|
||||||
results_exported.extend(results.exported)
|
results_exported.extend(results.exported)
|
||||||
results_new.extend(results.new)
|
results_new.extend(results.new)
|
||||||
@@ -1783,6 +1805,7 @@ def export(
|
|||||||
convert_to_jpeg=convert_to_jpeg,
|
convert_to_jpeg=convert_to_jpeg,
|
||||||
jpeg_quality=jpeg_quality,
|
jpeg_quality=jpeg_quality,
|
||||||
ignore_date_modified=ignore_date_modified,
|
ignore_date_modified=ignore_date_modified,
|
||||||
|
use_photokit=use_photokit,
|
||||||
)
|
)
|
||||||
results_exported.extend(results.exported)
|
results_exported.extend(results.exported)
|
||||||
results_new.extend(results.new)
|
results_new.extend(results.new)
|
||||||
@@ -2290,6 +2313,7 @@ def export_photo(
|
|||||||
convert_to_jpeg=False,
|
convert_to_jpeg=False,
|
||||||
jpeg_quality=1.0,
|
jpeg_quality=1.0,
|
||||||
ignore_date_modified=False,
|
ignore_date_modified=False,
|
||||||
|
use_photokit=False,
|
||||||
):
|
):
|
||||||
""" Helper function for export that does the actual export
|
""" Helper function for export that does the actual export
|
||||||
|
|
||||||
@@ -2341,10 +2365,10 @@ def export_photo(
|
|||||||
space = " " if not verbose_ else ""
|
space = " " if not verbose_ else ""
|
||||||
verbose(f"{space}Skipping missing photo {photo.original_filename}")
|
verbose(f"{space}Skipping missing photo {photo.original_filename}")
|
||||||
return ExportResults([], [], [], [], [], [])
|
return ExportResults([], [], [], [], [], [])
|
||||||
elif not os.path.exists(photo.path):
|
elif photo.path is None:
|
||||||
space = " " if not verbose_ else ""
|
space = " " if not verbose_ else ""
|
||||||
verbose(
|
verbose(
|
||||||
f"{space}WARNING: file {photo.path} is missing but ismissing=False, "
|
f"{space}WARNING: photo {photo.original_filename} ({photo.uuid}) is missing but ismissing=False, "
|
||||||
f"skipping {photo.original_filename}"
|
f"skipping {photo.original_filename}"
|
||||||
)
|
)
|
||||||
return ExportResults([], [], [], [], [], [])
|
return ExportResults([], [], [], [], [], [])
|
||||||
@@ -2362,6 +2386,10 @@ def export_photo(
|
|||||||
results_touched = []
|
results_touched = []
|
||||||
|
|
||||||
export_original = not (skip_original_if_edited and photo.hasadjustments)
|
export_original = not (skip_original_if_edited and photo.hasadjustments)
|
||||||
|
|
||||||
|
# can't export edited if photo doesn't have edited versions
|
||||||
|
export_edited = export_edited if photo.hasadjustments else False
|
||||||
|
|
||||||
# slow_mo photos will always have hasadjustments=True even if not edited
|
# slow_mo photos will always have hasadjustments=True even if not edited
|
||||||
if photo.hasadjustments and photo.path_edited is None:
|
if photo.hasadjustments and photo.path_edited is None:
|
||||||
if photo.slow_mo:
|
if photo.slow_mo:
|
||||||
@@ -2396,7 +2424,7 @@ def export_photo(
|
|||||||
download_missing
|
download_missing
|
||||||
and (
|
and (
|
||||||
photo.ismissing
|
photo.ismissing
|
||||||
or not os.path.exists(photo.path)
|
or photo.path is None
|
||||||
or (export_edited and photo.path_edited is None)
|
or (export_edited and photo.path_edited is None)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -2430,6 +2458,7 @@ def export_photo(
|
|||||||
convert_to_jpeg=convert_to_jpeg,
|
convert_to_jpeg=convert_to_jpeg,
|
||||||
jpeg_quality=jpeg_quality,
|
jpeg_quality=jpeg_quality,
|
||||||
ignore_date_modified=ignore_date_modified,
|
ignore_date_modified=ignore_date_modified,
|
||||||
|
use_photokit=use_photokit,
|
||||||
)
|
)
|
||||||
|
|
||||||
results_exported.extend(export_results.exported)
|
results_exported.extend(export_results.exported)
|
||||||
@@ -2492,6 +2521,7 @@ def export_photo(
|
|||||||
convert_to_jpeg=convert_to_jpeg,
|
convert_to_jpeg=convert_to_jpeg,
|
||||||
jpeg_quality=jpeg_quality,
|
jpeg_quality=jpeg_quality,
|
||||||
ignore_date_modified=ignore_date_modified,
|
ignore_date_modified=ignore_date_modified,
|
||||||
|
use_photokit=use_photokit,
|
||||||
)
|
)
|
||||||
|
|
||||||
results_exported.extend(export_results_edited.exported)
|
results_exported.extend(export_results_edited.exported)
|
||||||
@@ -2549,7 +2579,7 @@ def get_filenames_from_template(photo, filename_template, original_name):
|
|||||||
)
|
)
|
||||||
filenames = [f"{file_}{photo_ext}" for file_ in filenames]
|
filenames = [f"{file_}{photo_ext}" for file_ in filenames]
|
||||||
else:
|
else:
|
||||||
filenames = [photo.original_filename] if original_name else [photo.filename]
|
filenames = [photo.original_filename] if (original_name and (photo.original_filename is not None)) else [photo.filename]
|
||||||
|
|
||||||
filenames = [sanitize_filename(filename) for filename in filenames]
|
filenames = [sanitize_filename(filename) for filename in filenames]
|
||||||
return filenames
|
return filenames
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
""" version info """
|
""" version info """
|
||||||
|
|
||||||
__version__ = "0.36.14"
|
__version__ = "0.36.22"
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
""" FileUtil class with methods for copy, hardlink, unlink, etc. """
|
""" FileUtil class with methods for copy, hardlink, unlink, etc. """
|
||||||
|
|
||||||
import logging
|
|
||||||
import os
|
import os
|
||||||
import pathlib
|
import pathlib
|
||||||
import stat
|
import stat
|
||||||
@@ -74,7 +73,6 @@ class FileUtilMacOS(FileUtilABC):
|
|||||||
try:
|
try:
|
||||||
os.link(src, dest)
|
os.link(src, dest)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.critical(f"os.link returned error: {e}")
|
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -92,7 +90,7 @@ class FileUtilMacOS(FileUtilABC):
|
|||||||
if src is None or dest is None:
|
if src is None or dest is None:
|
||||||
raise ValueError("src and dest must not be None", src, dest)
|
raise ValueError("src and dest must not be None", src, dest)
|
||||||
|
|
||||||
if not os.path.isfile(src):
|
if not os.path.exists(src):
|
||||||
raise FileNotFoundError("src file does not appear to exist", src)
|
raise FileNotFoundError("src file does not appear to exist", src)
|
||||||
|
|
||||||
if norsrc:
|
if norsrc:
|
||||||
@@ -104,9 +102,6 @@ class FileUtilMacOS(FileUtilABC):
|
|||||||
try:
|
try:
|
||||||
result = subprocess.run(command, check=True, stderr=subprocess.PIPE)
|
result = subprocess.run(command, check=True, stderr=subprocess.PIPE)
|
||||||
except subprocess.CalledProcessError as e:
|
except subprocess.CalledProcessError as e:
|
||||||
logging.critical(
|
|
||||||
f"ditto returned error: {e.returncode} {e.stderr.decode(sys.getfilesystemencoding()).rstrip()}"
|
|
||||||
)
|
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
return result.returncode
|
return result.returncode
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
""" utility functions for validating/sanitizing path components """
|
""" utility functions for validating/sanitizing path components """
|
||||||
|
|
||||||
from ._constants import MAX_DIRNAME_LEN, MAX_FILENAME_LEN
|
|
||||||
import pathvalidate
|
import pathvalidate
|
||||||
|
|
||||||
|
from ._constants import MAX_DIRNAME_LEN, MAX_FILENAME_LEN
|
||||||
|
|
||||||
|
|
||||||
def sanitize_filepath(filepath):
|
def sanitize_filepath(filepath):
|
||||||
""" sanitize a filepath """
|
""" sanitize a filepath """
|
||||||
|
|||||||
@@ -21,9 +21,10 @@ import re
|
|||||||
import tempfile
|
import tempfile
|
||||||
from collections import namedtuple # pylint: disable=syntax-error
|
from collections import namedtuple # pylint: disable=syntax-error
|
||||||
|
|
||||||
|
import photoscript
|
||||||
from mako.template import Template
|
from mako.template import Template
|
||||||
|
|
||||||
from .._applescript import AppleScript
|
# from .._applescript import AppleScript
|
||||||
from .._constants import (
|
from .._constants import (
|
||||||
_MAX_IPTC_KEYWORD_LEN,
|
_MAX_IPTC_KEYWORD_LEN,
|
||||||
_OSXPHOTOS_NONE_SENTINEL,
|
_OSXPHOTOS_NONE_SENTINEL,
|
||||||
@@ -31,9 +32,15 @@ from .._constants import (
|
|||||||
_UNKNOWN_PERSON,
|
_UNKNOWN_PERSON,
|
||||||
_XMP_TEMPLATE_NAME,
|
_XMP_TEMPLATE_NAME,
|
||||||
)
|
)
|
||||||
from ..export_db import ExportDBNoOp
|
|
||||||
from ..exiftool import ExifTool
|
from ..exiftool import ExifTool
|
||||||
|
from ..export_db import ExportDBNoOp
|
||||||
from ..fileutil import FileUtil
|
from ..fileutil import FileUtil
|
||||||
|
from ..photokit import (
|
||||||
|
PHOTOS_VERSION_CURRENT,
|
||||||
|
PHOTOS_VERSION_ORIGINAL,
|
||||||
|
PhotoLibrary,
|
||||||
|
PhotoKitFetchFailed,
|
||||||
|
)
|
||||||
from ..utils import dd_to_dms_str, findfiles
|
from ..utils import dd_to_dms_str, findfiles
|
||||||
|
|
||||||
ExportResults = namedtuple(
|
ExportResults = namedtuple(
|
||||||
@@ -78,33 +85,33 @@ def _export_photo_uuid_applescript(
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# setup the applescript to do the export
|
# setup the applescript to do the export
|
||||||
export_scpt = AppleScript(
|
# export_scpt = AppleScript(
|
||||||
"""
|
# """
|
||||||
on export_by_uuid(theUUID, thePath, original, edited, theTimeOut)
|
# on export_by_uuid(theUUID, thePath, original, edited, theTimeOut)
|
||||||
tell application "Photos"
|
# tell application "Photos"
|
||||||
set thePath to thePath
|
# set thePath to thePath
|
||||||
set theItem to media item id theUUID
|
# set theItem to media item id theUUID
|
||||||
set theFilename to filename of theItem
|
# set theFilename to filename of theItem
|
||||||
set itemList to {theItem}
|
# set itemList to {theItem}
|
||||||
|
|
||||||
if original then
|
|
||||||
with timeout of theTimeOut seconds
|
|
||||||
export itemList to POSIX file thePath with using originals
|
|
||||||
end timeout
|
|
||||||
end if
|
|
||||||
|
|
||||||
if edited then
|
|
||||||
with timeout of theTimeOut seconds
|
|
||||||
export itemList to POSIX file thePath
|
|
||||||
end timeout
|
|
||||||
end if
|
|
||||||
|
|
||||||
return theFilename
|
|
||||||
end tell
|
|
||||||
|
|
||||||
end export_by_uuid
|
# if original then
|
||||||
"""
|
# with timeout of theTimeOut seconds
|
||||||
)
|
# export itemList to POSIX file thePath with using originals
|
||||||
|
# end timeout
|
||||||
|
# end if
|
||||||
|
|
||||||
|
# if edited then
|
||||||
|
# with timeout of theTimeOut seconds
|
||||||
|
# export itemList to POSIX file thePath
|
||||||
|
# end timeout
|
||||||
|
# end if
|
||||||
|
|
||||||
|
# return theFilename
|
||||||
|
# end tell
|
||||||
|
|
||||||
|
# end export_by_uuid
|
||||||
|
# """
|
||||||
|
# )
|
||||||
|
|
||||||
dest = pathlib.Path(dest)
|
dest = pathlib.Path(dest)
|
||||||
if not dest.is_dir():
|
if not dest.is_dir():
|
||||||
@@ -115,30 +122,36 @@ def _export_photo_uuid_applescript(
|
|||||||
|
|
||||||
tmpdir = tempfile.TemporaryDirectory(prefix="osxphotos_")
|
tmpdir = tempfile.TemporaryDirectory(prefix="osxphotos_")
|
||||||
|
|
||||||
# export original
|
exported_files = []
|
||||||
filename = None
|
filename = None
|
||||||
try:
|
try:
|
||||||
filename = export_scpt.call(
|
photo = photoscript.Photo(uuid)
|
||||||
"export_by_uuid", uuid, tmpdir.name, original, edited, timeout
|
filename = photo.filename
|
||||||
)
|
exported_files = photo.export(tmpdir.name, original=original, timeout=timeout)
|
||||||
|
# filename = export_scpt.call(
|
||||||
|
# "export_by_uuid", uuid, tmpdir.name, original, edited, timeout
|
||||||
|
# )
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.warning(f"Error exporting uuid {uuid}: {e}")
|
logging.warning(f"Error exporting uuid {uuid}: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if filename is not None:
|
if exported_files and filename:
|
||||||
# need to find actual filename as sometimes Photos renames JPG to jpeg on export
|
# need to find actual filename as sometimes Photos renames JPG to jpeg on export
|
||||||
# may be more than one file exported (e.g. if Live Photo, Photos exports both .jpeg and .mov)
|
# may be more than one file exported (e.g. if Live Photo, Photos exports both .jpeg and .mov)
|
||||||
# TemporaryDirectory will cleanup on return
|
# TemporaryDirectory will cleanup on return
|
||||||
filename_stem = pathlib.Path(filename).stem
|
filename_stem = pathlib.Path(filename).stem
|
||||||
files = glob.glob(os.path.join(tmpdir.name, "*"))
|
|
||||||
exported_paths = []
|
exported_paths = []
|
||||||
for fname in files:
|
for fname in exported_files:
|
||||||
path = pathlib.Path(fname)
|
path = pathlib.Path(tmpdir.name) / fname
|
||||||
if len(files) > 1 and not live_photo and path.suffix.lower() == ".mov":
|
if (
|
||||||
|
len(exported_files) > 1
|
||||||
|
and not live_photo
|
||||||
|
and path.suffix.lower() == ".mov"
|
||||||
|
):
|
||||||
# it's the .mov part of live photo but not requested, so don't export
|
# it's the .mov part of live photo but not requested, so don't export
|
||||||
logging.debug(f"Skipping live photo file {path}")
|
logging.debug(f"Skipping live photo file {path}")
|
||||||
continue
|
continue
|
||||||
if len(files) > 1 and burst and path.stem != filename_stem:
|
if len(exported_files) > 1 and burst and path.stem != filename_stem:
|
||||||
# skip any burst photo that's not the one we asked for
|
# skip any burst photo that's not the one we asked for
|
||||||
logging.debug(f"Skipping burst photo file {path}")
|
logging.debug(f"Skipping burst photo file {path}")
|
||||||
continue
|
continue
|
||||||
@@ -310,6 +323,7 @@ def export2(
|
|||||||
convert_to_jpeg=False,
|
convert_to_jpeg=False,
|
||||||
jpeg_quality=1.0,
|
jpeg_quality=1.0,
|
||||||
ignore_date_modified=False,
|
ignore_date_modified=False,
|
||||||
|
use_photokit=False,
|
||||||
):
|
):
|
||||||
""" export photo, like export but with update and dry_run options
|
""" export photo, like export but with update and dry_run options
|
||||||
dest: must be valid destination path or exception raised
|
dest: must be valid destination path or exception raised
|
||||||
@@ -651,32 +665,73 @@ def export2(
|
|||||||
# didn't get passed a filename, add _edited
|
# didn't get passed a filename, add _edited
|
||||||
filestem = f"{dest.stem}{edited_identifier}"
|
filestem = f"{dest.stem}{edited_identifier}"
|
||||||
dest = dest.parent / f"{filestem}.jpeg"
|
dest = dest.parent / f"{filestem}.jpeg"
|
||||||
|
if use_photokit:
|
||||||
exported = _export_photo_uuid_applescript(
|
photolib = PhotoLibrary()
|
||||||
self.uuid,
|
photo = None
|
||||||
dest.parent,
|
try:
|
||||||
filestem=filestem,
|
photo = photolib.fetch_uuid(self.uuid)
|
||||||
original=False,
|
except PhotoKitFetchFailed:
|
||||||
edited=True,
|
# if failed to find UUID, might be a burst photo
|
||||||
live_photo=live_photo,
|
if self.burst and self._info["burstUUID"]:
|
||||||
timeout=timeout,
|
bursts = photolib.fetch_burst_uuid(
|
||||||
burst=self.burst,
|
self._info["burstUUID"], all=True
|
||||||
dry_run=dry_run,
|
)
|
||||||
)
|
# PhotoKit UUIDs may contain "/L0/001" so only look at beginning
|
||||||
|
photo = [p for p in bursts if p.uuid.startswith(self.uuid)]
|
||||||
|
photo = photo[0] if photo else None
|
||||||
|
if photo:
|
||||||
|
exported = photo.export(
|
||||||
|
dest.parent, dest.name, version=PHOTOS_VERSION_CURRENT
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
exported = []
|
||||||
|
else:
|
||||||
|
exported = _export_photo_uuid_applescript(
|
||||||
|
self.uuid,
|
||||||
|
dest.parent,
|
||||||
|
filestem=filestem,
|
||||||
|
original=False,
|
||||||
|
edited=True,
|
||||||
|
live_photo=live_photo,
|
||||||
|
timeout=timeout,
|
||||||
|
burst=self.burst,
|
||||||
|
dry_run=dry_run,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
# export original version and not edited
|
# export original version and not edited
|
||||||
filestem = dest.stem
|
filestem = dest.stem
|
||||||
exported = _export_photo_uuid_applescript(
|
if use_photokit:
|
||||||
self.uuid,
|
photolib = PhotoLibrary()
|
||||||
dest.parent,
|
photo = None
|
||||||
filestem=filestem,
|
try:
|
||||||
original=True,
|
photo = photolib.fetch_uuid(self.uuid)
|
||||||
edited=False,
|
except PhotoKitFetchFailed:
|
||||||
live_photo=live_photo,
|
# if failed to find UUID, might be a burst photo
|
||||||
timeout=timeout,
|
if self.burst and self._info["burstUUID"]:
|
||||||
burst=self.burst,
|
bursts = photolib.fetch_burst_uuid(
|
||||||
dry_run=dry_run,
|
self._info["burstUUID"], all=True
|
||||||
)
|
)
|
||||||
|
# PhotoKit UUIDs may contain "/L0/001" so only look at beginning
|
||||||
|
photo = [p for p in bursts if p.uuid.startswith(self.uuid)]
|
||||||
|
photo = photo[0] if photo else None
|
||||||
|
if photo:
|
||||||
|
exported = photo.export(
|
||||||
|
dest.parent, dest.name, version=PHOTOS_VERSION_ORIGINAL
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
exported = []
|
||||||
|
else:
|
||||||
|
exported = _export_photo_uuid_applescript(
|
||||||
|
self.uuid,
|
||||||
|
dest.parent,
|
||||||
|
filestem=filestem,
|
||||||
|
original=True,
|
||||||
|
edited=False,
|
||||||
|
live_photo=live_photo,
|
||||||
|
timeout=timeout,
|
||||||
|
burst=self.burst,
|
||||||
|
dry_run=dry_run,
|
||||||
|
)
|
||||||
if exported:
|
if exported:
|
||||||
if touch_file:
|
if touch_file:
|
||||||
for exported_file in exported:
|
for exported_file in exported:
|
||||||
@@ -1191,7 +1246,6 @@ def _exiftool_dict(
|
|||||||
|
|
||||||
timeoriginal = date.strftime(f"%H:%M:%S{offsettime}")
|
timeoriginal = date.strftime(f"%H:%M:%S{offsettime}")
|
||||||
exif["IPTC:TimeCreated"] = timeoriginal
|
exif["IPTC:TimeCreated"] = timeoriginal
|
||||||
print(f"time = {timeoriginal}")
|
|
||||||
|
|
||||||
if self.date_modified is not None and not ignore_date_modified:
|
if self.date_modified is not None and not ignore_date_modified:
|
||||||
exif["EXIF:ModifyDate"] = self.date_modified.strftime("%Y:%m:%d %H:%M:%S")
|
exif["EXIF:ModifyDate"] = self.date_modified.strftime("%Y:%m:%d %H:%M:%S")
|
||||||
|
|||||||
@@ -164,6 +164,8 @@ class PhotoInfo:
|
|||||||
photopath = os.path.join(
|
photopath = os.path.join(
|
||||||
self._db._masters_path, self._info["imagePath"]
|
self._db._masters_path, self._info["imagePath"]
|
||||||
)
|
)
|
||||||
|
if not os.path.isfile(photopath):
|
||||||
|
photopath = None
|
||||||
self._path = photopath
|
self._path = photopath
|
||||||
return photopath
|
return photopath
|
||||||
|
|
||||||
@@ -175,6 +177,8 @@ class PhotoInfo:
|
|||||||
self._info["directory"],
|
self._info["directory"],
|
||||||
self._info["filename"],
|
self._info["filename"],
|
||||||
)
|
)
|
||||||
|
if not os.path.isfile(photopath):
|
||||||
|
photopath = None
|
||||||
self._path = photopath
|
self._path = photopath
|
||||||
return photopath
|
return photopath
|
||||||
|
|
||||||
@@ -188,6 +192,8 @@ class PhotoInfo:
|
|||||||
self._info["directory"],
|
self._info["directory"],
|
||||||
self._info["filename"],
|
self._info["filename"],
|
||||||
)
|
)
|
||||||
|
if not os.path.isfile(photopath):
|
||||||
|
photopath = None
|
||||||
self._path = photopath
|
self._path = photopath
|
||||||
return photopath
|
return photopath
|
||||||
|
|
||||||
|
|||||||
1215
osxphotos/photokit.py
Normal file
1215
osxphotos/photokit.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -35,6 +35,7 @@ from .._constants import (
|
|||||||
from .._version import __version__
|
from .._version import __version__
|
||||||
from ..albuminfo import AlbumInfo, FolderInfo, ImportInfo
|
from ..albuminfo import AlbumInfo, FolderInfo, ImportInfo
|
||||||
from ..datetime_utils import datetime_has_tz, datetime_naive_to_local
|
from ..datetime_utils import datetime_has_tz, datetime_naive_to_local
|
||||||
|
from ..fileutil import FileUtil
|
||||||
from ..personinfo import PersonInfo
|
from ..personinfo import PersonInfo
|
||||||
from ..photoinfo import PhotoInfo
|
from ..photoinfo import PhotoInfo
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
@@ -546,6 +547,33 @@ class PhotosDB:
|
|||||||
|
|
||||||
return dest_path
|
return dest_path
|
||||||
|
|
||||||
|
# NOTE: This method seems to cause problems with applescript
|
||||||
|
# Bummer...would'be been nice to avoid copying the DB
|
||||||
|
# def _link_db_file(self, fname):
|
||||||
|
# """ links the sqlite database file to a temp file """
|
||||||
|
# """ returns the name of the temp file """
|
||||||
|
# """ If sqlite shared memory and write-ahead log files exist, those are copied too """
|
||||||
|
# # required because python's sqlite3 implementation can't read a locked file
|
||||||
|
# # _, suffix = os.path.splitext(fname)
|
||||||
|
# dest_name = dest_path = ""
|
||||||
|
# try:
|
||||||
|
# dest_name = pathlib.Path(fname).name
|
||||||
|
# dest_path = os.path.join(self._tempdir_name, dest_name)
|
||||||
|
# FileUtil.hardlink(fname, dest_path)
|
||||||
|
# # link write-ahead log and shared memory files (-wal and -shm) files if they exist
|
||||||
|
# if os.path.exists(f"{fname}-wal"):
|
||||||
|
# FileUtil.hardlink(f"{fname}-wal", f"{dest_path}-wal")
|
||||||
|
# if os.path.exists(f"{fname}-shm"):
|
||||||
|
# FileUtil.hardlink(f"{fname}-shm", f"{dest_path}-shm")
|
||||||
|
# except:
|
||||||
|
# print("Error linking " + fname + " to " + dest_path, file=sys.stderr)
|
||||||
|
# raise Exception
|
||||||
|
|
||||||
|
# if _debug():
|
||||||
|
# logging.debug(dest_path)
|
||||||
|
|
||||||
|
# return dest_path
|
||||||
|
|
||||||
def _process_database4(self):
|
def _process_database4(self):
|
||||||
""" process the Photos database to extract info
|
""" process the Photos database to extract info
|
||||||
works on Photos version <= 4.0 """
|
works on Photos version <= 4.0 """
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
% if desc is None:
|
% if desc is None:
|
||||||
<dc:description></dc:description>
|
<dc:description></dc:description>
|
||||||
% else:
|
% else:
|
||||||
<dc:description>${desc}</dc:description>
|
<dc:description>${desc | x}</dc:description>
|
||||||
% endif
|
% endif
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
% if title is None:
|
% if title is None:
|
||||||
<dc:title></dc:title>
|
<dc:title></dc:title>
|
||||||
% else:
|
% else:
|
||||||
<dc:title>${title}</dc:title>
|
<dc:title>${title | x}</dc:title>
|
||||||
% endif
|
% endif
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
<dc:subject>
|
<dc:subject>
|
||||||
<rdf:Seq>
|
<rdf:Seq>
|
||||||
% for subj in subject:
|
% for subj in subject:
|
||||||
<rdf:li>${subj}</rdf:li>
|
<rdf:li>${subj | x}</rdf:li>
|
||||||
% endfor
|
% endfor
|
||||||
</rdf:Seq>
|
</rdf:Seq>
|
||||||
</dc:subject>
|
</dc:subject>
|
||||||
@@ -48,7 +48,7 @@
|
|||||||
<Iptc4xmpExt:PersonInImage>
|
<Iptc4xmpExt:PersonInImage>
|
||||||
<rdf:Bag>
|
<rdf:Bag>
|
||||||
% for person in persons:
|
% for person in persons:
|
||||||
<rdf:li>${person}</rdf:li>
|
<rdf:li>${person | x}</rdf:li>
|
||||||
% endfor
|
% endfor
|
||||||
</rdf:Bag>
|
</rdf:Bag>
|
||||||
</Iptc4xmpExt:PersonInImage>
|
</Iptc4xmpExt:PersonInImage>
|
||||||
@@ -60,7 +60,7 @@
|
|||||||
<digiKam:TagsList>
|
<digiKam:TagsList>
|
||||||
<rdf:Seq>
|
<rdf:Seq>
|
||||||
% for keyword in keywords:
|
% for keyword in keywords:
|
||||||
<rdf:li>${keyword}</rdf:li>
|
<rdf:li>${keyword | x}</rdf:li>
|
||||||
% endfor
|
% endfor
|
||||||
</rdf:Seq>
|
</rdf:Seq>
|
||||||
</digiKam:TagsList>
|
</digiKam:TagsList>
|
||||||
@@ -81,10 +81,8 @@
|
|||||||
|
|
||||||
<%def name="gps_info(latitude, longitude)">
|
<%def name="gps_info(latitude, longitude)">
|
||||||
% if latitude is not None and longitude is not None:
|
% if latitude is not None and longitude is not None:
|
||||||
<exif:GPSLongitudeRef>${"E" if longitude >= 0 else "W"}</exif:GPSLongitudeRef>
|
<exif:GPSLongitude>${int(abs(longitude))},${(abs(longitude) % 1) * 60}${"E" if longitude >= 0 else "W"}</exif:GPSLongitude>
|
||||||
<exif:GPSLongitude>${abs(longitude)}</exif:GPSLongitude>
|
<exif:GPSLatitude>${int(abs(latitude))},${(abs(latitude) % 1) * 60}${"N" if latitude >= 0 else "S"}</exif:GPSLatitude>
|
||||||
<exif:GPSLatitude>${abs(latitude)}</exif:GPSLatitude>
|
|
||||||
<exif:GPSLatitudeRef>${"N" if latitude >= 0 else "S"}</exif:GPSLatitudeRef>
|
|
||||||
% endif
|
% endif
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ from plistlib import load as plistload
|
|||||||
import CoreFoundation
|
import CoreFoundation
|
||||||
import CoreServices
|
import CoreServices
|
||||||
import objc
|
import objc
|
||||||
from Foundation import *
|
|
||||||
|
|
||||||
from ._constants import UNICODE_FORMAT
|
from ._constants import UNICODE_FORMAT
|
||||||
from .fileutil import FileUtil
|
from .fileutil import FileUtil
|
||||||
@@ -57,10 +56,12 @@ def _debug():
|
|||||||
""" returns True if debugging turned on (via _set_debug), otherwise, false """
|
""" returns True if debugging turned on (via _set_debug), otherwise, false """
|
||||||
return _DEBUG
|
return _DEBUG
|
||||||
|
|
||||||
|
|
||||||
def noop(*args, **kwargs):
|
def noop(*args, **kwargs):
|
||||||
""" do nothing (no operation) """
|
""" do nothing (no operation) """
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def _get_os_version():
|
def _get_os_version():
|
||||||
# returns tuple containing OS version
|
# returns tuple containing OS version
|
||||||
# e.g. 10.13.6 = (10, 13, 6)
|
# e.g. 10.13.6 = (10, 13, 6)
|
||||||
@@ -200,7 +201,7 @@ def get_last_library_path():
|
|||||||
# pylint: disable=no-member
|
# pylint: disable=no-member
|
||||||
# pylint: disable=undefined-variable
|
# pylint: disable=undefined-variable
|
||||||
photosurl = CoreFoundation.CFURLCreateByResolvingBookmarkData(
|
photosurl = CoreFoundation.CFURLCreateByResolvingBookmarkData(
|
||||||
kCFAllocatorDefault, photosurlref, 0, None, None, None, None
|
CoreFoundation.kCFAllocatorDefault, photosurlref, 0, None, None, None, None
|
||||||
)
|
)
|
||||||
|
|
||||||
# the CFURLRef we got is a sruct that python treats as an array
|
# the CFURLRef we got is a sruct that python treats as an array
|
||||||
@@ -361,9 +362,35 @@ def _db_is_locked(dbname):
|
|||||||
|
|
||||||
def normalize_unicode(value):
|
def normalize_unicode(value):
|
||||||
""" normalize unicode data """
|
""" normalize unicode data """
|
||||||
if value is not None:
|
if value is None:
|
||||||
if not isinstance(value, str):
|
|
||||||
raise ValueError("value must be str")
|
|
||||||
return unicodedata.normalize(UNICODE_FORMAT, value)
|
|
||||||
else:
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
if not isinstance(value, str):
|
||||||
|
raise ValueError("value must be str")
|
||||||
|
return unicodedata.normalize(UNICODE_FORMAT, value)
|
||||||
|
|
||||||
|
|
||||||
|
def increment_filename(filepath):
|
||||||
|
""" Return filename (1).ext, etc if filename.ext exists
|
||||||
|
|
||||||
|
If file exists in filename's parent folder with same stem as filename,
|
||||||
|
add (1), (2), etc. until a non-existing filename is found.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
filepath: str; full path, including file name
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
new filepath (or same if not incremented)
|
||||||
|
|
||||||
|
Note: This obviously is subject to race condition so using with caution.
|
||||||
|
"""
|
||||||
|
dest = pathlib.Path(str(filepath))
|
||||||
|
count = 1
|
||||||
|
dest_files = findfiles(f"{dest.stem}*", str(dest.parent))
|
||||||
|
dest_files = [pathlib.Path(f).stem.lower() for f in dest_files]
|
||||||
|
dest_new = dest.stem
|
||||||
|
while dest_new.lower() in dest_files:
|
||||||
|
dest_new = f"{dest.stem} ({count})"
|
||||||
|
count += 1
|
||||||
|
dest = dest.parent / f"{dest_new}{dest.suffix}"
|
||||||
|
return str(dest)
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ parso==0.6.2
|
|||||||
pathspec==0.7.0
|
pathspec==0.7.0
|
||||||
pathvalidate==2.2.1
|
pathvalidate==2.2.1
|
||||||
pexpect==4.8.0
|
pexpect==4.8.0
|
||||||
|
photoscript==0.1.0
|
||||||
pickleshare==0.7.5
|
pickleshare==0.7.5
|
||||||
Pillow==7.2.0
|
Pillow==7.2.0
|
||||||
pkginfo==1.5.0.1
|
pkginfo==1.5.0.1
|
||||||
|
|||||||
1
setup.py
1
setup.py
@@ -79,6 +79,7 @@ setup(
|
|||||||
"pathvalidate==2.2.1",
|
"pathvalidate==2.2.1",
|
||||||
"dataclasses==0.7;python_version<'3.7'",
|
"dataclasses==0.7;python_version<'3.7'",
|
||||||
"wurlitzer>=2.0.1",
|
"wurlitzer>=2.0.1",
|
||||||
|
"photoscript>=0.1.0",
|
||||||
],
|
],
|
||||||
entry_points={"console_scripts": ["osxphotos=osxphotos.__main__:cli"]},
|
entry_points={"console_scripts": ["osxphotos=osxphotos.__main__:cli"]},
|
||||||
include_package_data=True,
|
include_package_data=True,
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
114
tests/tempdiskimage.py
Normal file
114
tests/tempdiskimage.py
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
""" Create a temporary disk image on MacOS """
|
||||||
|
|
||||||
|
import pathlib
|
||||||
|
import platform
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
import time
|
||||||
|
|
||||||
|
|
||||||
|
class TempDiskImage:
|
||||||
|
""" Create and mount a temporary disk image """
|
||||||
|
|
||||||
|
def __init__(self, size=100, prefix=None):
|
||||||
|
""" Create and mount a temporary disk image.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
size: int; size in MB of disk image, default = 100
|
||||||
|
prefix: str; optional prefix to prepend to name of the temporary disk image
|
||||||
|
name: str; name of the mounted volume, default = "TemporaryDiskImage"
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError if size is not int
|
||||||
|
RunTimeError if not on MacOS
|
||||||
|
"""
|
||||||
|
if type(size) != int:
|
||||||
|
raise TypeError("size must be int")
|
||||||
|
|
||||||
|
system = platform.system()
|
||||||
|
if system != "Darwin":
|
||||||
|
raise RuntimeError("TempDiskImage only runs on MacOS")
|
||||||
|
|
||||||
|
self._tempdir = tempfile.TemporaryDirectory()
|
||||||
|
# hacky mktemp: this could create a race condition but unlikely given it's created in a TemporaryDirectory
|
||||||
|
prefix = "TemporaryDiskImage" if prefix is None else prefix
|
||||||
|
volume_name = f"{prefix}_{str(time.time()).replace('.','_')}_{str(time.perf_counter()).replace('.','_')}"
|
||||||
|
image_name = f"{volume_name}.dmg"
|
||||||
|
image_path = pathlib.Path(self._tempdir.name) / image_name
|
||||||
|
hdiutil = subprocess.run(
|
||||||
|
[
|
||||||
|
"/usr/bin/hdiutil",
|
||||||
|
"create",
|
||||||
|
"-size",
|
||||||
|
f"{size}m",
|
||||||
|
"-fs",
|
||||||
|
"HFS+",
|
||||||
|
"-volname",
|
||||||
|
volume_name,
|
||||||
|
image_path,
|
||||||
|
],
|
||||||
|
check=True,
|
||||||
|
text=True,
|
||||||
|
capture_output=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
if "created" not in hdiutil.stdout:
|
||||||
|
raise OSError(f"Could not create DMG {image_path}")
|
||||||
|
|
||||||
|
self.path = image_path
|
||||||
|
self._mount_point, self.name = self._mount_image(self.path)
|
||||||
|
|
||||||
|
def _mount_image(self, image_path):
|
||||||
|
""" mount a DMG file and return path, returns (mount_point, path) """
|
||||||
|
hdiutil = subprocess.run(
|
||||||
|
["/usr/bin/hdiutil", "attach", image_path],
|
||||||
|
check=True,
|
||||||
|
text=True,
|
||||||
|
capture_output=True,
|
||||||
|
)
|
||||||
|
mount_point, path = None, None
|
||||||
|
for line in hdiutil.stdout.split("\n"):
|
||||||
|
line = line.strip()
|
||||||
|
if "Apple_HFS" not in line:
|
||||||
|
continue
|
||||||
|
output = line.split()
|
||||||
|
if len(output) < 3:
|
||||||
|
raise ValueError(f"Error mounting disk image {image_path}")
|
||||||
|
mount_point = output[0]
|
||||||
|
path = output[2]
|
||||||
|
break
|
||||||
|
return (mount_point, path)
|
||||||
|
|
||||||
|
def unmount(self):
|
||||||
|
try:
|
||||||
|
if self._mount_point:
|
||||||
|
hdiutil = subprocess.run(
|
||||||
|
["/usr/bin/hdiutil", "detach", self._mount_point],
|
||||||
|
check=True,
|
||||||
|
text=True,
|
||||||
|
capture_output=True,
|
||||||
|
)
|
||||||
|
self._mount_point = None
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc_value, traceback):
|
||||||
|
self.unmount()
|
||||||
|
if exc_type:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Create a temporary disk image, 50mb in size
|
||||||
|
img = TempDiskImage(size=50, prefix="MyDiskImage")
|
||||||
|
# Be sure to unmount it, image will be cleaned up automatically
|
||||||
|
img.unmount()
|
||||||
|
|
||||||
|
# Or use it as a context handler
|
||||||
|
# Default values are 100mb and prefix = "TemporaryDiskImage"
|
||||||
|
with TempDiskImage() as img:
|
||||||
|
print(f"image: {img.path}")
|
||||||
|
print(f"mounted at: {img.name}")
|
||||||
@@ -863,6 +863,21 @@ def test_export_14(photosdb, caplog):
|
|||||||
|
|
||||||
assert "Invalid destination suffix" not in caplog.text
|
assert "Invalid destination suffix" not in caplog.text
|
||||||
|
|
||||||
|
def test_export_no_original_filename(photosdb):
|
||||||
|
# test export OK if original filename is null
|
||||||
|
# issue #267
|
||||||
|
|
||||||
|
tempdir = tempfile.TemporaryDirectory(prefix="osxphotos_")
|
||||||
|
dest = tempdir.name
|
||||||
|
photos = photosdb.photos(uuid=[UUID_DICT["export"]])
|
||||||
|
photos[0]._info["original_filename"] = None
|
||||||
|
filename = f"{photos[0].uuid}.jpeg"
|
||||||
|
expected_dest = os.path.join(dest, filename)
|
||||||
|
got_dest = photos[0].export(dest)[0]
|
||||||
|
|
||||||
|
assert got_dest == expected_dest
|
||||||
|
assert os.path.isfile(got_dest)
|
||||||
|
|
||||||
|
|
||||||
def test_eq():
|
def test_eq():
|
||||||
""" Test equality of two PhotoInfo objects """
|
""" Test equality of two PhotoInfo objects """
|
||||||
|
|||||||
@@ -11,9 +11,9 @@ pytestmark = pytest.mark.skipif(
|
|||||||
PHOTOS_DB = "/Users/rhet/Pictures/Photos Library.photoslibrary"
|
PHOTOS_DB = "/Users/rhet/Pictures/Photos Library.photoslibrary"
|
||||||
|
|
||||||
UUID_DICT = {
|
UUID_DICT = {
|
||||||
"has_adjustments": "A8111956-E900-4DEC-9191-A04A87C07BC5",
|
"has_adjustments": "2B2D5434-6D31-49E2-BF47-B973D34A317B",
|
||||||
"no_adjustments": "EA7BB55F-92F1-4818-94E3-E8DEDC6B2E31",
|
"no_adjustments": "A8D646C3-89A9-4D74-8001-4EB46BA55B94",
|
||||||
"live": "9032C168-9319-40C0-8210-5ADC42F4C603",
|
"live": "BFF29EBD-22DF-4FCF-9817-317E7104EA50",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1029,7 +1029,7 @@ def test_xmp_sidecar_gps():
|
|||||||
xmlns:photoshop="http://ns.adobe.com/photoshop/1.0/">
|
xmlns:photoshop="http://ns.adobe.com/photoshop/1.0/">
|
||||||
<photoshop:SidecarForExtension>jpg</photoshop:SidecarForExtension>
|
<photoshop:SidecarForExtension>jpg</photoshop:SidecarForExtension>
|
||||||
<dc:description></dc:description>
|
<dc:description></dc:description>
|
||||||
<dc:title>St. James's Park</dc:title>
|
<dc:title>St. James's Park</dc:title>
|
||||||
<!-- keywords and persons listed in <dc:subject> as Photos does -->
|
<!-- keywords and persons listed in <dc:subject> as Photos does -->
|
||||||
<dc:subject>
|
<dc:subject>
|
||||||
<rdf:Seq>
|
<rdf:Seq>
|
||||||
@@ -1038,7 +1038,7 @@ def test_xmp_sidecar_gps():
|
|||||||
<rdf:li>London</rdf:li>
|
<rdf:li>London</rdf:li>
|
||||||
<rdf:li>United Kingdom</rdf:li>
|
<rdf:li>United Kingdom</rdf:li>
|
||||||
<rdf:li>London 2018</rdf:li>
|
<rdf:li>London 2018</rdf:li>
|
||||||
<rdf:li>St. James's Park</rdf:li>
|
<rdf:li>St. James's Park</rdf:li>
|
||||||
</rdf:Seq>
|
</rdf:Seq>
|
||||||
</dc:subject>
|
</dc:subject>
|
||||||
<photoshop:DateCreated>2018-10-13T09:18:12.501000-04:00</photoshop:DateCreated>
|
<photoshop:DateCreated>2018-10-13T09:18:12.501000-04:00</photoshop:DateCreated>
|
||||||
@@ -1055,7 +1055,7 @@ def test_xmp_sidecar_gps():
|
|||||||
<rdf:li>London</rdf:li>
|
<rdf:li>London</rdf:li>
|
||||||
<rdf:li>United Kingdom</rdf:li>
|
<rdf:li>United Kingdom</rdf:li>
|
||||||
<rdf:li>London 2018</rdf:li>
|
<rdf:li>London 2018</rdf:li>
|
||||||
<rdf:li>St. James's Park</rdf:li>
|
<rdf:li>St. James's Park</rdf:li>
|
||||||
</rdf:Seq>
|
</rdf:Seq>
|
||||||
</digiKam:TagsList>
|
</digiKam:TagsList>
|
||||||
</rdf:Description>
|
</rdf:Description>
|
||||||
@@ -1066,10 +1066,8 @@ def test_xmp_sidecar_gps():
|
|||||||
</rdf:Description>
|
</rdf:Description>
|
||||||
<rdf:Description rdf:about=""
|
<rdf:Description rdf:about=""
|
||||||
xmlns:exif='http://ns.adobe.com/exif/1.0/'>
|
xmlns:exif='http://ns.adobe.com/exif/1.0/'>
|
||||||
<exif:GPSLongitudeRef>W</exif:GPSLongitudeRef>
|
<exif:GPSLongitude>0,7.908329999999999W</exif:GPSLongitude>
|
||||||
<exif:GPSLongitude>0.1318055</exif:GPSLongitude>
|
<exif:GPSLatitude>51,30.21430019999997N</exif:GPSLatitude>
|
||||||
<exif:GPSLatitude>51.50357167</exif:GPSLatitude>
|
|
||||||
<exif:GPSLatitudeRef>N</exif:GPSLatitudeRef>
|
|
||||||
</rdf:Description>
|
</rdf:Description>
|
||||||
</rdf:RDF>
|
</rdf:RDF>
|
||||||
</x:xmpmeta>"""
|
</x:xmpmeta>"""
|
||||||
|
|||||||
@@ -20,10 +20,10 @@ NAMES_DICT = {
|
|||||||
"heic": "7783E8E6-9CAC-40F3-BE22-81FB7051C266.jpeg",
|
"heic": "7783E8E6-9CAC-40F3-BE22-81FB7051C266.jpeg",
|
||||||
}
|
}
|
||||||
|
|
||||||
UUID_LIVE_HEIC = "1337F3F6-5C9F-4FC7-80CC-BD9A5B928F72"
|
UUID_LIVE_HEIC = "612CE30B-3D8F-417A-9B14-EC42CBA10ACC"
|
||||||
NAMES_LIVE_HEIC = [
|
NAMES_LIVE_HEIC = [
|
||||||
"1337F3F6-5C9F-4FC7-80CC-BD9A5B928F72.jpeg",
|
"612CE30B-3D8F-417A-9B14-EC42CBA10ACC.jpeg",
|
||||||
"1337F3F6-5C9F-4FC7-80CC-BD9A5B928F72.mov",
|
"612CE30B-3D8F-417A-9B14-EC42CBA10ACC.mov",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
387
tests/test_photokit.py
Normal file
387
tests/test_photokit.py
Normal file
@@ -0,0 +1,387 @@
|
|||||||
|
""" test photokit.py methods """
|
||||||
|
|
||||||
|
import os
|
||||||
|
import pathlib
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from osxphotos.photokit import (
|
||||||
|
LivePhotoAsset,
|
||||||
|
PhotoAsset,
|
||||||
|
PhotoLibrary,
|
||||||
|
VideoAsset,
|
||||||
|
PHOTOS_VERSION_CURRENT,
|
||||||
|
PHOTOS_VERSION_ORIGINAL,
|
||||||
|
PHOTOS_VERSION_UNADJUSTED,
|
||||||
|
)
|
||||||
|
|
||||||
|
skip_test = "OSXPHOTOS_TEST_EXPORT" not in os.environ
|
||||||
|
pytestmark = pytest.mark.skipif(
|
||||||
|
skip_test, reason="Skip if not running with author's personal library."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
UUID_DICT = {
|
||||||
|
"plain_photo": {
|
||||||
|
"uuid": "A8D646C3-89A9-4D74-8001-4EB46BA55B94",
|
||||||
|
"filename": "IMG_8844.JPG",
|
||||||
|
},
|
||||||
|
"hdr": {"uuid": "DA87C6FF-60E8-4DCB-A21D-9C57595667F1", "filename": "IMG_6162.JPG"},
|
||||||
|
"selfie": {
|
||||||
|
"uuid": "316AEBE0-971D-4A33-833C-6BDBFF83469B",
|
||||||
|
"filename": "IMG_1929.JPG",
|
||||||
|
},
|
||||||
|
"video": {
|
||||||
|
"uuid": "5814D9DE-FAB6-473A-9C9A-5A73C6DD1AF5",
|
||||||
|
"filename": "IMG_9411.TRIM.MOV",
|
||||||
|
},
|
||||||
|
"hasadjustments": {
|
||||||
|
"uuid": "2B2D5434-6D31-49E2-BF47-B973D34A317B",
|
||||||
|
"filename": "IMG_2860.JPG",
|
||||||
|
"adjusted_size": 3012634,
|
||||||
|
"unadjusted_size": 2580058,
|
||||||
|
},
|
||||||
|
"slow_mo": {
|
||||||
|
"uuid": "DAABC6D9-1FBA-4485-AA39-0A2B100300B1",
|
||||||
|
"filename": "IMG_4055.MOV",
|
||||||
|
},
|
||||||
|
"live_photo": {
|
||||||
|
"uuid": "612CE30B-3D8F-417A-9B14-EC42CBA10ACC",
|
||||||
|
"filename": "IMG_3259.HEIC",
|
||||||
|
"filename_video": "IMG_3259.mov",
|
||||||
|
},
|
||||||
|
"burst": {
|
||||||
|
"uuid": "CD97EC84-71F0-40C6-BAC1-2BABEE305CAC",
|
||||||
|
"filename": "IMG_8196.JPG",
|
||||||
|
"burst_selected": 3,
|
||||||
|
"burst_all": 5,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_fetch_uuid():
|
||||||
|
""" test fetch_uuid """
|
||||||
|
uuid = UUID_DICT["plain_photo"]["uuid"]
|
||||||
|
filename = UUID_DICT["plain_photo"]["filename"]
|
||||||
|
|
||||||
|
lib = PhotoLibrary()
|
||||||
|
photo = lib.fetch_uuid(uuid)
|
||||||
|
assert isinstance(photo, PhotoAsset)
|
||||||
|
|
||||||
|
|
||||||
|
def test_plain_photo():
|
||||||
|
""" test plain_photo """
|
||||||
|
uuid = UUID_DICT["plain_photo"]["uuid"]
|
||||||
|
filename = UUID_DICT["plain_photo"]["filename"]
|
||||||
|
|
||||||
|
lib = PhotoLibrary()
|
||||||
|
photo = lib.fetch_uuid(uuid)
|
||||||
|
assert photo.original_filename == filename
|
||||||
|
assert photo.isphoto
|
||||||
|
assert not photo.ismovie
|
||||||
|
|
||||||
|
|
||||||
|
def test_hdr():
|
||||||
|
""" test hdr """
|
||||||
|
uuid = UUID_DICT["hdr"]["uuid"]
|
||||||
|
filename = UUID_DICT["hdr"]["filename"]
|
||||||
|
|
||||||
|
lib = PhotoLibrary()
|
||||||
|
photo = lib.fetch_uuid(uuid)
|
||||||
|
assert photo.original_filename == filename
|
||||||
|
assert photo.hdr
|
||||||
|
|
||||||
|
|
||||||
|
def test_burst():
|
||||||
|
""" test burst and burstid """
|
||||||
|
test_dict = UUID_DICT["burst"]
|
||||||
|
uuid = test_dict["uuid"]
|
||||||
|
filename = test_dict["filename"]
|
||||||
|
|
||||||
|
lib = PhotoLibrary()
|
||||||
|
photo = lib.fetch_uuid(uuid)
|
||||||
|
assert photo.original_filename == filename
|
||||||
|
assert photo.burst
|
||||||
|
assert photo.burstid
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# def test_selfie():
|
||||||
|
# """ test selfie """
|
||||||
|
# uuid = UUID_DICT["selfie"]["uuid"]
|
||||||
|
# filename = UUID_DICT["selfie"]["filename"]
|
||||||
|
|
||||||
|
# lib = PhotoLibrary()
|
||||||
|
# photo = lib.fetch_uuid(uuid)
|
||||||
|
# assert photo.original_filename == filename
|
||||||
|
# assert photo.selfie
|
||||||
|
|
||||||
|
|
||||||
|
def test_video():
|
||||||
|
""" test ismovie """
|
||||||
|
uuid = UUID_DICT["video"]["uuid"]
|
||||||
|
filename = UUID_DICT["video"]["filename"]
|
||||||
|
|
||||||
|
lib = PhotoLibrary()
|
||||||
|
photo = lib.fetch_uuid(uuid)
|
||||||
|
assert isinstance(photo, VideoAsset)
|
||||||
|
assert photo.original_filename == filename
|
||||||
|
assert photo.ismovie
|
||||||
|
assert not photo.isphoto
|
||||||
|
|
||||||
|
|
||||||
|
def test_slow_mo():
|
||||||
|
""" test slow_mo """
|
||||||
|
test_dict = UUID_DICT["slow_mo"]
|
||||||
|
uuid = test_dict["uuid"]
|
||||||
|
filename = test_dict["filename"]
|
||||||
|
|
||||||
|
lib = PhotoLibrary()
|
||||||
|
photo = lib.fetch_uuid(uuid)
|
||||||
|
assert isinstance(photo, VideoAsset)
|
||||||
|
assert photo.original_filename == filename
|
||||||
|
assert photo.ismovie
|
||||||
|
assert photo.slow_mo
|
||||||
|
assert not photo.isphoto
|
||||||
|
|
||||||
|
|
||||||
|
### PhotoAsset
|
||||||
|
|
||||||
|
|
||||||
|
def test_export_photo_original():
|
||||||
|
""" test PhotoAsset.export """
|
||||||
|
test_dict = UUID_DICT["hasadjustments"]
|
||||||
|
uuid = test_dict["uuid"]
|
||||||
|
lib = PhotoLibrary()
|
||||||
|
photo = lib.fetch_uuid(uuid)
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory(prefix="photokit_test") as tempdir:
|
||||||
|
export_path = photo.export(tempdir, version=PHOTOS_VERSION_ORIGINAL)
|
||||||
|
export_path = pathlib.Path(export_path[0])
|
||||||
|
assert export_path.is_file()
|
||||||
|
filename = test_dict["filename"]
|
||||||
|
assert export_path.stem == pathlib.Path(filename).stem
|
||||||
|
assert export_path.stat().st_size == test_dict["unadjusted_size"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_export_photo_unadjusted():
|
||||||
|
""" test PhotoAsset.export """
|
||||||
|
test_dict = UUID_DICT["hasadjustments"]
|
||||||
|
uuid = test_dict["uuid"]
|
||||||
|
lib = PhotoLibrary()
|
||||||
|
photo = lib.fetch_uuid(uuid)
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory(prefix="photokit_test") as tempdir:
|
||||||
|
export_path = photo.export(tempdir, version=PHOTOS_VERSION_UNADJUSTED)
|
||||||
|
export_path = pathlib.Path(export_path[0])
|
||||||
|
assert export_path.is_file()
|
||||||
|
filename = test_dict["filename"]
|
||||||
|
assert export_path.stem == pathlib.Path(filename).stem
|
||||||
|
assert export_path.stat().st_size == test_dict["unadjusted_size"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_export_photo_current():
|
||||||
|
""" test PhotoAsset.export """
|
||||||
|
test_dict = UUID_DICT["hasadjustments"]
|
||||||
|
uuid = test_dict["uuid"]
|
||||||
|
lib = PhotoLibrary()
|
||||||
|
photo = lib.fetch_uuid(uuid)
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory(prefix="photokit_test") as tempdir:
|
||||||
|
export_path = photo.export(tempdir)
|
||||||
|
export_path = pathlib.Path(export_path[0])
|
||||||
|
assert export_path.is_file()
|
||||||
|
filename = test_dict["filename"]
|
||||||
|
assert export_path.stem == pathlib.Path(filename).stem
|
||||||
|
assert export_path.stat().st_size == test_dict["adjusted_size"]
|
||||||
|
|
||||||
|
|
||||||
|
### VideoAsset
|
||||||
|
|
||||||
|
|
||||||
|
def test_export_video_original():
|
||||||
|
""" test VideoAsset.export """
|
||||||
|
test_dict = UUID_DICT["video"]
|
||||||
|
uuid = test_dict["uuid"]
|
||||||
|
lib = PhotoLibrary()
|
||||||
|
photo = lib.fetch_uuid(uuid)
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory(prefix="photokit_test") as tempdir:
|
||||||
|
export_path = photo.export(tempdir, version=PHOTOS_VERSION_ORIGINAL)
|
||||||
|
export_path = pathlib.Path(export_path[0])
|
||||||
|
assert export_path.is_file()
|
||||||
|
filename = test_dict["filename"]
|
||||||
|
assert export_path.stem == pathlib.Path(filename).stem
|
||||||
|
|
||||||
|
|
||||||
|
def test_export_video_unadjusted():
|
||||||
|
""" test VideoAsset.export """
|
||||||
|
test_dict = UUID_DICT["video"]
|
||||||
|
uuid = test_dict["uuid"]
|
||||||
|
lib = PhotoLibrary()
|
||||||
|
photo = lib.fetch_uuid(uuid)
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory(prefix="photokit_test") as tempdir:
|
||||||
|
export_path = photo.export(tempdir, version=PHOTOS_VERSION_UNADJUSTED)
|
||||||
|
export_path = pathlib.Path(export_path[0])
|
||||||
|
assert export_path.is_file()
|
||||||
|
filename = test_dict["filename"]
|
||||||
|
assert export_path.stem == pathlib.Path(filename).stem
|
||||||
|
|
||||||
|
|
||||||
|
def test_export_video_current():
|
||||||
|
""" test VideoAsset.export """
|
||||||
|
test_dict = UUID_DICT["video"]
|
||||||
|
uuid = test_dict["uuid"]
|
||||||
|
lib = PhotoLibrary()
|
||||||
|
photo = lib.fetch_uuid(uuid)
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory(prefix="photokit_test") as tempdir:
|
||||||
|
export_path = photo.export(tempdir, version=PHOTOS_VERSION_CURRENT)
|
||||||
|
export_path = pathlib.Path(export_path[0])
|
||||||
|
assert export_path.is_file()
|
||||||
|
filename = test_dict["filename"]
|
||||||
|
assert export_path.stem == pathlib.Path(filename).stem
|
||||||
|
|
||||||
|
|
||||||
|
### Slow-Mo VideoAsset
|
||||||
|
|
||||||
|
|
||||||
|
def test_export_slow_mo_original():
|
||||||
|
""" test VideoAsset.export for slow mo video"""
|
||||||
|
test_dict = UUID_DICT["slow_mo"]
|
||||||
|
uuid = test_dict["uuid"]
|
||||||
|
lib = PhotoLibrary()
|
||||||
|
photo = lib.fetch_uuid(uuid)
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory(prefix="photokit_test") as tempdir:
|
||||||
|
export_path = photo.export(tempdir, version=PHOTOS_VERSION_ORIGINAL)
|
||||||
|
export_path = pathlib.Path(export_path[0])
|
||||||
|
assert export_path.is_file()
|
||||||
|
filename = test_dict["filename"]
|
||||||
|
assert export_path.stem == pathlib.Path(filename).stem
|
||||||
|
|
||||||
|
|
||||||
|
def test_export_slow_mo_unadjusted():
|
||||||
|
""" test VideoAsset.export for slow mo video"""
|
||||||
|
test_dict = UUID_DICT["slow_mo"]
|
||||||
|
uuid = test_dict["uuid"]
|
||||||
|
lib = PhotoLibrary()
|
||||||
|
photo = lib.fetch_uuid(uuid)
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory(prefix="photokit_test") as tempdir:
|
||||||
|
export_path = photo.export(tempdir, version=PHOTOS_VERSION_UNADJUSTED)
|
||||||
|
export_path = pathlib.Path(export_path[0])
|
||||||
|
assert export_path.is_file()
|
||||||
|
filename = test_dict["filename"]
|
||||||
|
assert export_path.stem == pathlib.Path(filename).stem
|
||||||
|
|
||||||
|
|
||||||
|
def test_export_slow_mo_current():
|
||||||
|
""" test VideoAsset.export for slow mo video"""
|
||||||
|
test_dict = UUID_DICT["slow_mo"]
|
||||||
|
uuid = test_dict["uuid"]
|
||||||
|
lib = PhotoLibrary()
|
||||||
|
photo = lib.fetch_uuid(uuid)
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory(prefix="photokit_test") as tempdir:
|
||||||
|
export_path = photo.export(tempdir, version=PHOTOS_VERSION_CURRENT)
|
||||||
|
export_path = pathlib.Path(export_path[0])
|
||||||
|
assert export_path.is_file()
|
||||||
|
filename = test_dict["filename"]
|
||||||
|
assert export_path.stem == pathlib.Path(filename).stem
|
||||||
|
|
||||||
|
|
||||||
|
### LivePhotoAsset
|
||||||
|
|
||||||
|
|
||||||
|
def test_export_live_original():
|
||||||
|
""" test LivePhotoAsset.export """
|
||||||
|
test_dict = UUID_DICT["live_photo"]
|
||||||
|
uuid = test_dict["uuid"]
|
||||||
|
lib = PhotoLibrary()
|
||||||
|
photo = lib.fetch_uuid(uuid)
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory(prefix="photokit_test") as tempdir:
|
||||||
|
export_path = photo.export(tempdir, version=PHOTOS_VERSION_ORIGINAL)
|
||||||
|
for f in export_path:
|
||||||
|
filepath = pathlib.Path(f)
|
||||||
|
assert filepath.is_file()
|
||||||
|
filename = test_dict["filename"]
|
||||||
|
assert filepath.stem == pathlib.Path(filename).stem
|
||||||
|
|
||||||
|
|
||||||
|
def test_export_live_unadjusted():
|
||||||
|
""" test LivePhotoAsset.export """
|
||||||
|
test_dict = UUID_DICT["live_photo"]
|
||||||
|
uuid = test_dict["uuid"]
|
||||||
|
lib = PhotoLibrary()
|
||||||
|
photo = lib.fetch_uuid(uuid)
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory(prefix="photokit_test") as tempdir:
|
||||||
|
export_path = photo.export(tempdir, version=PHOTOS_VERSION_UNADJUSTED)
|
||||||
|
for file in export_path:
|
||||||
|
filepath = pathlib.Path(file)
|
||||||
|
assert filepath.is_file()
|
||||||
|
filename = test_dict["filename"]
|
||||||
|
assert filepath.stem == pathlib.Path(filename).stem
|
||||||
|
|
||||||
|
|
||||||
|
def test_export_live_current():
|
||||||
|
""" test LivePhotAsset.export """
|
||||||
|
test_dict = UUID_DICT["live_photo"]
|
||||||
|
uuid = test_dict["uuid"]
|
||||||
|
lib = PhotoLibrary()
|
||||||
|
photo = lib.fetch_uuid(uuid)
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory(prefix="photokit_test") as tempdir:
|
||||||
|
export_path = photo.export(tempdir, version=PHOTOS_VERSION_CURRENT)
|
||||||
|
for file in export_path:
|
||||||
|
filepath = pathlib.Path(file)
|
||||||
|
assert filepath.is_file()
|
||||||
|
filename = test_dict["filename"]
|
||||||
|
assert filepath.stem == pathlib.Path(filename).stem
|
||||||
|
|
||||||
|
|
||||||
|
def test_export_live_current_just_photo():
|
||||||
|
""" test LivePhotAsset.export """
|
||||||
|
test_dict = UUID_DICT["live_photo"]
|
||||||
|
uuid = test_dict["uuid"]
|
||||||
|
lib = PhotoLibrary()
|
||||||
|
photo = lib.fetch_uuid(uuid)
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory(prefix="photokit_test") as tempdir:
|
||||||
|
export_path = photo.export(tempdir, photo=True, video=False)
|
||||||
|
assert len(export_path) == 1
|
||||||
|
assert export_path[0].lower().endswith(".heic")
|
||||||
|
|
||||||
|
|
||||||
|
def test_export_live_current_just_video():
|
||||||
|
""" test LivePhotAsset.export """
|
||||||
|
test_dict = UUID_DICT["live_photo"]
|
||||||
|
uuid = test_dict["uuid"]
|
||||||
|
lib = PhotoLibrary()
|
||||||
|
photo = lib.fetch_uuid(uuid)
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory(prefix="photokit_test") as tempdir:
|
||||||
|
export_path = photo.export(tempdir, photo=False, video=True)
|
||||||
|
assert len(export_path) == 1
|
||||||
|
assert export_path[0].lower().endswith(".mov")
|
||||||
|
|
||||||
|
|
||||||
|
def test_fetch_burst_uuid():
|
||||||
|
""" test fetch_burst_uuid """
|
||||||
|
test_dict = UUID_DICT["burst"]
|
||||||
|
uuid = test_dict["uuid"]
|
||||||
|
filename = test_dict["filename"]
|
||||||
|
|
||||||
|
lib = PhotoLibrary()
|
||||||
|
photo = lib.fetch_uuid(uuid)
|
||||||
|
bursts_selected = lib.fetch_burst_uuid(photo.burstid)
|
||||||
|
assert len(bursts_selected) == test_dict["burst_selected"]
|
||||||
|
assert isinstance(bursts_selected[0], PhotoAsset)
|
||||||
|
|
||||||
|
bursts_all = lib.fetch_burst_uuid(photo.burstid, all=True)
|
||||||
|
assert len(bursts_all) == test_dict["burst_all"]
|
||||||
|
assert isinstance(bursts_all[0], PhotoAsset)
|
||||||
Reference in New Issue
Block a user