Compare commits

...

5 Commits

Author SHA1 Message Date
Rhet Turnbull
d2d56a7f71 Fix for burst images with pick type = 0, partial fix for #571 2022-01-06 22:46:16 -08:00
Rhet Turnbull
b4897ff1b5 version bump [skip ci] 2022-01-06 22:16:12 -08:00
Rhet Turnbull
661a573bf5 Fix for #570 2022-01-06 22:13:25 -08:00
Rhet Turnbull
0c9bd87602 More refactoring of export code, #462 2022-01-06 05:40:47 -08:00
Rhet Turnbull
896d888710 Updated CHANGELOG.md [skip ci] 2022-01-04 06:35:23 -08:00
18 changed files with 58 additions and 79 deletions

View File

@@ -4,6 +4,14 @@ 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.44.4](https://github.com/RhetTbull/osxphotos/compare/v0.44.3...v0.44.4)
> 4 January 2022
- Refactored photoinfo, photoexporter; #462 [`a73dc72`](https://github.com/RhetTbull/osxphotos/commit/a73dc72558b77152f4c90f143b6a60924b8905c8)
- More refactoring of export code, #462 [`147b30f`](https://github.com/RhetTbull/osxphotos/commit/147b30f97308db65868dc7a8d177d77ad0d0ad40)
- Export DB can now reside outside export directory, #568 [`76aee7f`](https://github.com/RhetTbull/osxphotos/commit/76aee7f189b4b32e2e263a4e798711713ed17a14)
#### [v0.44.3](https://github.com/RhetTbull/osxphotos/compare/v0.44.2...v0.44.3) #### [v0.44.3](https://github.com/RhetTbull/osxphotos/compare/v0.44.2...v0.44.3)
> 31 December 2021 > 31 December 2021

View File

@@ -1720,7 +1720,7 @@ Substitution Description
{lf} A line feed: '\n', alias for {newline} {lf} A line feed: '\n', alias for {newline}
{cr} A carriage return: '\r' {cr} A carriage return: '\r'
{crlf} a carriage return + line feed: '\r\n' {crlf} a carriage return + line feed: '\r\n'
{osxphotos_version} The osxphotos version, e.g. '0.44.4' {osxphotos_version} The osxphotos version, e.g. '0.44.6'
{osxphotos_cmd_line} The full command line used to run osxphotos {osxphotos_cmd_line} The full command line used to run osxphotos
The following substitutions may result in multiple values. Thus if specified for The following substitutions may result in multiple values. Thus if specified for
@@ -3622,7 +3622,7 @@ The following template field substitutions are availabe for use the templating s
|{lf}|A line feed: '\n', alias for {newline}| |{lf}|A line feed: '\n', alias for {newline}|
|{cr}|A carriage return: '\r'| |{cr}|A carriage return: '\r'|
|{crlf}|a carriage return + line feed: '\r\n'| |{crlf}|a carriage return + line feed: '\r\n'|
|{osxphotos_version}|The osxphotos version, e.g. '0.44.4'| |{osxphotos_version}|The osxphotos version, e.g. '0.44.6'|
|{osxphotos_cmd_line}|The full command line used to run osxphotos| |{osxphotos_cmd_line}|The full command line used to run osxphotos|
|{album}|Album(s) photo is contained in| |{album}|Album(s) photo is contained in|
|{folder_album}|Folder path + album photo is contained in. e.g. 'Folder/Subfolder/Album' or just 'Album' if no enclosing folder| |{folder_album}|Folder path + album photo is contained in. e.g. 'Folder/Subfolder/Album' or just 'Album' if no enclosing folder|

View File

@@ -1,4 +1,4 @@
# Sphinx build info version 1 # Sphinx build info version 1
# This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done. # This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done.
config: abcd83bede460ffb3604a85d16e98db7 config: 12e2b2711a035185a2f8b8e500263a8d
tags: 645f666f9bcd5a90fca523b33c5a78b7 tags: 645f666f9bcd5a90fca523b33c5a78b7

View File

@@ -1,6 +1,6 @@
var DOCUMENTATION_OPTIONS = { var DOCUMENTATION_OPTIONS = {
URL_ROOT: document.getElementById("documentation_options").getAttribute('data-url_root'), URL_ROOT: document.getElementById("documentation_options").getAttribute('data-url_root'),
VERSION: '0.44.4', VERSION: '0.44.6',
LANGUAGE: 'None', LANGUAGE: 'None',
COLLAPSE_INDEX: false, COLLAPSE_INDEX: false,
BUILDER: 'html', BUILDER: 'html',

View File

@@ -6,7 +6,7 @@
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /><meta name="generator" content="Docutils 0.17.1: http://docutils.sourceforge.net/" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /><meta name="generator" content="Docutils 0.17.1: http://docutils.sourceforge.net/" />
<title>osxphotos command line interface (CLI) &#8212; osxphotos 0.44.4 documentation</title> <title>osxphotos command line interface (CLI) &#8212; osxphotos 0.44.6 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css" /> <link rel="stylesheet" type="text/css" href="_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="_static/alabaster.css" /> <link rel="stylesheet" type="text/css" href="_static/alabaster.css" />
<script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script> <script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>

View File

@@ -5,7 +5,7 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Index &#8212; osxphotos 0.44.4 documentation</title> <title>Index &#8212; osxphotos 0.44.6 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css" /> <link rel="stylesheet" type="text/css" href="_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="_static/alabaster.css" /> <link rel="stylesheet" type="text/css" href="_static/alabaster.css" />
<script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script> <script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>

View File

@@ -6,7 +6,7 @@
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /><meta name="generator" content="Docutils 0.17.1: http://docutils.sourceforge.net/" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /><meta name="generator" content="Docutils 0.17.1: http://docutils.sourceforge.net/" />
<title>Welcome to osxphotoss documentation! &#8212; osxphotos 0.44.4 documentation</title> <title>Welcome to osxphotoss documentation! &#8212; osxphotos 0.44.6 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css" /> <link rel="stylesheet" type="text/css" href="_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="_static/alabaster.css" /> <link rel="stylesheet" type="text/css" href="_static/alabaster.css" />
<script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script> <script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>

View File

@@ -6,7 +6,7 @@
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /><meta name="generator" content="Docutils 0.17.1: http://docutils.sourceforge.net/" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /><meta name="generator" content="Docutils 0.17.1: http://docutils.sourceforge.net/" />
<title>osxphotos &#8212; osxphotos 0.44.4 documentation</title> <title>osxphotos &#8212; osxphotos 0.44.6 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css" /> <link rel="stylesheet" type="text/css" href="_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="_static/alabaster.css" /> <link rel="stylesheet" type="text/css" href="_static/alabaster.css" />
<script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script> <script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>

View File

@@ -6,7 +6,7 @@
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /><meta name="generator" content="Docutils 0.17.1: http://docutils.sourceforge.net/" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /><meta name="generator" content="Docutils 0.17.1: http://docutils.sourceforge.net/" />
<title>osxphotos package &#8212; osxphotos 0.44.4 documentation</title> <title>osxphotos package &#8212; osxphotos 0.44.6 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css" /> <link rel="stylesheet" type="text/css" href="_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="_static/alabaster.css" /> <link rel="stylesheet" type="text/css" href="_static/alabaster.css" />
<script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script> <script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>

View File

@@ -5,7 +5,7 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Search &#8212; osxphotos 0.44.4 documentation</title> <title>Search &#8212; osxphotos 0.44.6 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css" /> <link rel="stylesheet" type="text/css" href="_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="_static/alabaster.css" /> <link rel="stylesheet" type="text/css" href="_static/alabaster.css" />

View File

@@ -258,6 +258,7 @@ EXTENDED_ATTRIBUTE_NAMES_QUOTED = [f"'{x}'" for x in EXTENDED_ATTRIBUTE_NAMES]
OSXPHOTOS_EXPORT_DB = ".osxphotos_export.db" OSXPHOTOS_EXPORT_DB = ".osxphotos_export.db"
# bit flags for burst images ("burstPickType") # bit flags for burst images ("burstPickType")
BURST_PICK_TYPE_NONE = 0b0 # 0: sometimes used for single images with a burst UUID
BURST_NOT_SELECTED = 0b10 # 2: burst image is not selected BURST_NOT_SELECTED = 0b10 # 2: burst image is not selected
BURST_DEFAULT_PICK = 0b100 # 4: burst image is the one Photos picked to be key image before any selections made BURST_DEFAULT_PICK = 0b100 # 4: burst image is the one Photos picked to be key image before any selections made
BURST_SELECTED = 0b1000 # 8: burst image is selected BURST_SELECTED = 0b1000 # 8: burst image is selected

View File

@@ -1,3 +1,3 @@
""" version info """ """ version info """
__version__ = "0.44.4" __version__ = "0.44.6"

View File

@@ -2954,11 +2954,9 @@ def export_photo_to_directory(
try: try:
exporter = PhotoExporter(photo) exporter = PhotoExporter(photo)
export_results = exporter.export2( export_results = exporter.export2(
dest_path, dest=dest_path,
original_filename=filename,
edited=edited, edited=edited,
original=export_original, filename=filename,
edited_filename=filename,
sidecar=sidecar_flags, sidecar=sidecar_flags,
sidecar_drop_ext=sidecar_drop_ext, sidecar_drop_ext=sidecar_drop_ext,
live_photo=export_live, live_photo=export_live,

View File

@@ -292,10 +292,8 @@ class PhotoExporter:
results = self.export2( results = self.export2(
dest, dest,
original=not edited, filename=filename,
original_filename=filename,
edited=edited, edited=edited,
edited_filename=filename,
live_photo=live_photo, live_photo=live_photo,
raw_photo=raw_photo, raw_photo=raw_photo,
export_as_hardlink=export_as_hardlink, export_as_hardlink=export_as_hardlink,
@@ -317,10 +315,8 @@ class PhotoExporter:
def export2( def export2(
self, self,
dest, dest,
original=True, filename=None,
original_filename=None,
edited=False, edited=False,
edited_filename=None,
live_photo=False, live_photo=False,
raw_photo=False, raw_photo=False,
export_as_hardlink=False, export_as_hardlink=False,
@@ -368,8 +364,7 @@ class PhotoExporter:
in which case export will use the extension provided by Photos upon export. in which case export will use the extension provided by Photos upon export.
e.g. to get the extension of the edited photo, e.g. to get the extension of the edited photo,
reference PhotoInfo.path_edited reference PhotoInfo.path_edited
original: (boolean, default=True); if True, will export the original version of the photo edited: (boolean, default=False); if True will export the edited version of the photo otherwise exports the original version
edited: (boolean, default=False); if True will export the edited version of the photo (only one of original or edited can be used)
live_photo: (boolean, default=False); if True, will also export the associated .mov for live photos live_photo: (boolean, default=False); if True, will also export the associated .mov for live photos
raw_photo: (boolean, default=False); if True, will also export the associated RAW photo raw_photo: (boolean, default=False); if True, will also export the associated RAW photo
export_as_hardlink: (boolean, default=False); if True, will hardlink files instead of copying them export_as_hardlink: (boolean, default=False); if True, will hardlink files instead of copying them
@@ -452,16 +447,13 @@ class PhotoExporter:
if verbose and not callable(verbose): if verbose and not callable(verbose):
raise TypeError("verbose must be callable") raise TypeError("verbose must be callable")
if verbose is None: if verbose is None:
verbose = self._verbose verbose = self._verbose
self._render_options = render_options or RenderOptions() self._render_options = render_options or RenderOptions()
export_original = original export_original = not edited
export_edited = edited export_edited = edited
if export_original and export_edited:
raise ValueError("Cannot export both original and edited photos")
if export_edited and not self.photo.hasadjustments: if export_edited and not self.photo.hasadjustments:
raise ValueError( raise ValueError(
"Photo does not have adjustments, cannot export edited version" "Photo does not have adjustments, cannot export edited version"
@@ -473,13 +465,13 @@ class PhotoExporter:
elif not dry_run and not os.path.isdir(dest): elif not dry_run and not os.path.isdir(dest):
raise FileNotFoundError("Invalid path passed to export") raise FileNotFoundError("Invalid path passed to export")
original_filename = original_filename or self.photo.original_filename if export_edited:
dest_original = pathlib.Path(dest) / original_filename filename = filename or self._get_edited_filename(
self.photo.original_filename
edited_filename = edited_filename or self._get_edited_filename( )
original_filename else:
) filename = filename or self.photo.original_filename
dest_edited = pathlib.Path(dest) / edited_filename dest = pathlib.Path(dest) / filename
# Is there something to convert? # Is there something to convert?
if convert_to_jpeg and self.photo.isphoto: if convert_to_jpeg and self.photo.isphoto:
@@ -488,39 +480,25 @@ class PhotoExporter:
if export_original and self.photo.uti_original != "public.jpeg": if export_original and self.photo.uti_original != "public.jpeg":
# not a jpeg but will convert to jpeg upon export so fix file extension # not a jpeg but will convert to jpeg upon export so fix file extension
something_to_convert = True something_to_convert = True
dest_original = dest_original.parent / f"{dest_original.stem}{ext}" dest = dest.parent / f"{dest.stem}{ext}"
if export_edited and self.photo.uti != "public.jpeg": if export_edited and self.photo.uti != "public.jpeg":
# in Big Sur+, edited HEICs are HEIC # in Big Sur+, edited HEICs are HEIC
something_to_convert = True something_to_convert = True
dest_edited = dest_edited.parent / f"{dest_edited.stem}{ext}" dest_edited = dest.parent / f"{dest.stem}{ext}"
convert_to_jpeg = something_to_convert convert_to_jpeg = something_to_convert
else: else:
convert_to_jpeg = False convert_to_jpeg = False
# TODO: need to look at this to see what happens if original not being exported but edited exists and already has an increment dest, _ = self._validate_dest_path(
dest_original, increment_file_count = self._validate_dest_path( dest, increment=increment, update=update, overwrite=overwrite
dest_original, increment=increment, update=update, overwrite=overwrite
)
dest_original = pathlib.Path(dest_original)
if export_edited:
dest_edited, increment_file_count = self._validate_dest_path(
dest_edited,
increment=increment,
update=update,
overwrite=overwrite,
count=increment_file_count,
)
dest_edited = pathlib.Path(dest_edited)
self._render_options.filepath = (
str(dest_original) if export_original else str(dest_edited)
) )
dest = pathlib.Path(dest)
self._render_options.filepath = str(dest)
all_results = ExportResults() all_results = ExportResults()
if use_photos_export: if use_photos_export:
self._export_photo_with_photos_export( self._export_photo_with_photos_export(
dest=dest_original if export_original else dest_edited, dest=dest,
all_results=all_results, all_results=all_results,
fileutil=fileutil, fileutil=fileutil,
export_db=export_db, export_db=export_db,
@@ -540,17 +518,11 @@ class PhotoExporter:
# find the source file on disk and export # find the source file on disk and export
# get path to source file and verify it's not None and is valid file # get path to source file and verify it's not None and is valid file
# TODO: how to handle ismissing or not hasadjustments and edited=True cases? # TODO: how to handle ismissing or not hasadjustments and edited=True cases?
export_src_dest = [] src = self.photo.path_edited if edited else self.photo.path
if edited and self.photo.path_edited is not None: if src and not pathlib.Path(src).is_file():
export_src_dest.append((self.photo.path_edited, dest_edited)) raise FileNotFoundError(f"{src} does not appear to exist")
elif not edited and self.photo.path is not None:
export_src_dest.append((self.photo.path, dest_original))
# TODO: this for loop not necessary
for src, dest in export_src_dest:
if not pathlib.Path(src).is_file():
raise FileNotFoundError(f"{src} does not appear to exist")
if src:
# found source now try to find right destination # found source now try to find right destination
if update and dest.exists(): if update and dest.exists():
# destination exists, check to see if destination is the right UUID # destination exists, check to see if destination is the right UUID
@@ -595,11 +567,6 @@ class PhotoExporter:
# increment the destination file # increment the destination file
dest = pathlib.Path(increment_filename(dest)) dest = pathlib.Path(increment_filename(dest))
if export_original:
dest_original = dest
else:
dest_edited = dest
# export the dest file # export the dest file
results = self._export_photo( results = self._export_photo(
src, src,
@@ -618,8 +585,6 @@ class PhotoExporter:
) )
all_results += results all_results += results
dest = dest_original if export_original else dest_edited
# copy live photo associated .mov if requested # copy live photo associated .mov if requested
if ( if (
export_original export_original
@@ -730,7 +695,6 @@ class PhotoExporter:
sidecar_xmp_files_skipped = [] sidecar_xmp_files_skipped = []
sidecar_xmp_files_written = [] sidecar_xmp_files_written = []
dest = dest_original if export_original else dest_edited
dest_suffix = "" if sidecar_drop_ext else dest.suffix dest_suffix = "" if sidecar_drop_ext else dest.suffix
if sidecar & SIDECAR_JSON: if sidecar & SIDECAR_JSON:
sidecar_filename = dest.parent / pathlib.Path( sidecar_filename = dest.parent / pathlib.Path(

View File

@@ -725,8 +725,10 @@ class PhotoInfo:
self._uti_original = self.uti self._uti_original = self.uti
elif self._db._photos_ver >= 7: elif self._db._photos_ver >= 7:
# Monterey+ # Monterey+
self._uti_original = get_uti_for_extension( # there are some cases with UTI_original is None (photo imported with no extension) so fallback to UTI and hope it's right
pathlib.Path(self.original_filename).suffix self._uti_original = (
get_uti_for_extension(pathlib.Path(self.original_filename).suffix)
or self.uti
) )
else: else:
self._uti_original = self._info["UTI_original"] self._uti_original = self._info["UTI_original"]
@@ -1025,7 +1027,7 @@ class PhotoInfo:
@property @property
def israw(self): def israw(self):
"""returns True if photo is a raw image. For images with an associated RAW+JPEG pair, see has_raw""" """returns True if photo is a raw image. For images with an associated RAW+JPEG pair, see has_raw"""
return "raw-image" in self.uti_original return "raw-image" in self.uti_original if self.uti_original else False
@property @property
def raw_original(self): def raw_original(self):

View File

@@ -520,7 +520,8 @@ class PhotoAsset:
== Photos.PHAssetResourceTypeAlternatePhoto == Photos.PHAssetResourceTypeAlternatePhoto
): ):
data = self._request_resource_data(resource) data = self._request_resource_data(resource)
ext = pathlib.Path(self.raw_filename).suffix[1:] suffix = pathlib.Path(self.raw_filename).suffix
ext = suffix[1:] if suffix else ""
break break
else: else:
raise PhotoKitExportError( raise PhotoKitExportError(

View File

@@ -27,11 +27,11 @@ from .._constants import (
_PHOTO_TYPE, _PHOTO_TYPE,
_PHOTOS_3_VERSION, _PHOTOS_3_VERSION,
_PHOTOS_4_ALBUM_KIND, _PHOTOS_4_ALBUM_KIND,
_PHOTOS_4_ROOT_FOLDER,
_PHOTOS_4_TOP_LEVEL_ALBUMS,
_PHOTOS_4_ALBUM_TYPE_ALBUM, _PHOTOS_4_ALBUM_TYPE_ALBUM,
_PHOTOS_4_ALBUM_TYPE_PROJECT, _PHOTOS_4_ALBUM_TYPE_PROJECT,
_PHOTOS_4_ALBUM_TYPE_SLIDESHOW, _PHOTOS_4_ALBUM_TYPE_SLIDESHOW,
_PHOTOS_4_ROOT_FOLDER,
_PHOTOS_4_TOP_LEVEL_ALBUMS,
_PHOTOS_4_VERSION, _PHOTOS_4_VERSION,
_PHOTOS_5_ALBUM_KIND, _PHOTOS_5_ALBUM_KIND,
_PHOTOS_5_FOLDER_KIND, _PHOTOS_5_FOLDER_KIND,
@@ -42,6 +42,7 @@ from .._constants import (
_TESTED_OS_VERSIONS, _TESTED_OS_VERSIONS,
_UNKNOWN_PERSON, _UNKNOWN_PERSON,
BURST_KEY, BURST_KEY,
BURST_PICK_TYPE_NONE,
BURST_SELECTED, BURST_SELECTED,
TIME_DELTA, TIME_DELTA,
) )
@@ -3062,6 +3063,7 @@ class PhotosDB:
if self._dbphotos[p]["burst"] and not ( if self._dbphotos[p]["burst"] and not (
self._dbphotos[p]["burstPickType"] & BURST_SELECTED self._dbphotos[p]["burstPickType"] & BURST_SELECTED
or self._dbphotos[p]["burstPickType"] & BURST_KEY or self._dbphotos[p]["burstPickType"] & BURST_KEY
or self._dbphotos[p]["burstPickType"] == BURST_PICK_TYPE_NONE
): ):
# not a key/selected burst photo, don't include in returned results # not a key/selected burst photo, don't include in returned results
continue continue

View File

@@ -591,6 +591,9 @@ def get_preferred_uti_extension(uti):
def get_uti_for_extension(extension): def get_uti_for_extension(extension):
"""get UTI for a given file extension""" """get UTI for a given file extension"""
if not extension:
return None
# accepts extension with or without leading 0 # accepts extension with or without leading 0
if extension[0] == ".": if extension[0] == ".":
extension = extension[1:] extension = extension[1:]