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).
#### [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)
> 31 December 2021

View File

@@ -1720,7 +1720,7 @@ Substitution Description
{lf} A line feed: '\n', alias for {newline}
{cr} A carriage return: '\r'
{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
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}|
|{cr}|A carriage return: '\r'|
|{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|
|{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|

View File

@@ -1,4 +1,4 @@
# 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.
config: abcd83bede460ffb3604a85d16e98db7
config: 12e2b2711a035185a2f8b8e500263a8d
tags: 645f666f9bcd5a90fca523b33c5a78b7

View File

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

View File

@@ -6,7 +6,7 @@
<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/" />
<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/alabaster.css" />
<script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>

View File

@@ -5,7 +5,7 @@
<head>
<meta charset="utf-8" />
<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/alabaster.css" />
<script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>

View File

@@ -6,7 +6,7 @@
<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/" />
<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/alabaster.css" />
<script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>

View File

@@ -6,7 +6,7 @@
<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/" />
<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/alabaster.css" />
<script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>

View File

@@ -6,7 +6,7 @@
<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/" />
<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/alabaster.css" />
<script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>

View File

@@ -5,7 +5,7 @@
<head>
<meta charset="utf-8" />
<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/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"
# 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_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

View File

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

View File

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

View File

@@ -292,10 +292,8 @@ class PhotoExporter:
results = self.export2(
dest,
original=not edited,
original_filename=filename,
filename=filename,
edited=edited,
edited_filename=filename,
live_photo=live_photo,
raw_photo=raw_photo,
export_as_hardlink=export_as_hardlink,
@@ -317,10 +315,8 @@ class PhotoExporter:
def export2(
self,
dest,
original=True,
original_filename=None,
filename=None,
edited=False,
edited_filename=None,
live_photo=False,
raw_photo=False,
export_as_hardlink=False,
@@ -368,8 +364,7 @@ class PhotoExporter:
in which case export will use the extension provided by Photos upon export.
e.g. to get the extension of the edited photo,
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 (only one of original or edited can be used)
edited: (boolean, default=False); if True will export the edited version of the photo otherwise exports the original version
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
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):
raise TypeError("verbose must be callable")
if verbose is None:
verbose = self._verbose
self._render_options = render_options or RenderOptions()
export_original = original
export_original = not 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:
raise ValueError(
"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):
raise FileNotFoundError("Invalid path passed to export")
original_filename = original_filename or self.photo.original_filename
dest_original = pathlib.Path(dest) / original_filename
edited_filename = edited_filename or self._get_edited_filename(
original_filename
)
dest_edited = pathlib.Path(dest) / edited_filename
if export_edited:
filename = filename or self._get_edited_filename(
self.photo.original_filename
)
else:
filename = filename or self.photo.original_filename
dest = pathlib.Path(dest) / filename
# Is there something to convert?
if convert_to_jpeg and self.photo.isphoto:
@@ -488,39 +480,25 @@ class PhotoExporter:
if export_original and self.photo.uti_original != "public.jpeg":
# not a jpeg but will convert to jpeg upon export so fix file extension
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":
# in Big Sur+, edited HEICs are HEIC
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
else:
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_original, increment_file_count = self._validate_dest_path(
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, _ = self._validate_dest_path(
dest, increment=increment, update=update, overwrite=overwrite
)
dest = pathlib.Path(dest)
self._render_options.filepath = str(dest)
all_results = ExportResults()
if use_photos_export:
self._export_photo_with_photos_export(
dest=dest_original if export_original else dest_edited,
dest=dest,
all_results=all_results,
fileutil=fileutil,
export_db=export_db,
@@ -540,17 +518,11 @@ class PhotoExporter:
# find the source file on disk and export
# 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?
export_src_dest = []
if edited and self.photo.path_edited is not None:
export_src_dest.append((self.photo.path_edited, dest_edited))
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")
src = self.photo.path_edited if edited else self.photo.path
if src and 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
if update and dest.exists():
# destination exists, check to see if destination is the right UUID
@@ -595,11 +567,6 @@ class PhotoExporter:
# increment the destination file
dest = pathlib.Path(increment_filename(dest))
if export_original:
dest_original = dest
else:
dest_edited = dest
# export the dest file
results = self._export_photo(
src,
@@ -618,8 +585,6 @@ class PhotoExporter:
)
all_results += results
dest = dest_original if export_original else dest_edited
# copy live photo associated .mov if requested
if (
export_original
@@ -730,7 +695,6 @@ class PhotoExporter:
sidecar_xmp_files_skipped = []
sidecar_xmp_files_written = []
dest = dest_original if export_original else dest_edited
dest_suffix = "" if sidecar_drop_ext else dest.suffix
if sidecar & SIDECAR_JSON:
sidecar_filename = dest.parent / pathlib.Path(

View File

@@ -725,8 +725,10 @@ class PhotoInfo:
self._uti_original = self.uti
elif self._db._photos_ver >= 7:
# Monterey+
self._uti_original = get_uti_for_extension(
pathlib.Path(self.original_filename).suffix
# there are some cases with UTI_original is None (photo imported with no extension) so fallback to UTI and hope it's right
self._uti_original = (
get_uti_for_extension(pathlib.Path(self.original_filename).suffix)
or self.uti
)
else:
self._uti_original = self._info["UTI_original"]
@@ -1025,7 +1027,7 @@ class PhotoInfo:
@property
def israw(self):
"""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
def raw_original(self):

View File

@@ -520,7 +520,8 @@ class PhotoAsset:
== Photos.PHAssetResourceTypeAlternatePhoto
):
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
else:
raise PhotoKitExportError(

View File

@@ -27,11 +27,11 @@ from .._constants import (
_PHOTO_TYPE,
_PHOTOS_3_VERSION,
_PHOTOS_4_ALBUM_KIND,
_PHOTOS_4_ROOT_FOLDER,
_PHOTOS_4_TOP_LEVEL_ALBUMS,
_PHOTOS_4_ALBUM_TYPE_ALBUM,
_PHOTOS_4_ALBUM_TYPE_PROJECT,
_PHOTOS_4_ALBUM_TYPE_SLIDESHOW,
_PHOTOS_4_ROOT_FOLDER,
_PHOTOS_4_TOP_LEVEL_ALBUMS,
_PHOTOS_4_VERSION,
_PHOTOS_5_ALBUM_KIND,
_PHOTOS_5_FOLDER_KIND,
@@ -42,6 +42,7 @@ from .._constants import (
_TESTED_OS_VERSIONS,
_UNKNOWN_PERSON,
BURST_KEY,
BURST_PICK_TYPE_NONE,
BURST_SELECTED,
TIME_DELTA,
)
@@ -3062,6 +3063,7 @@ class PhotosDB:
if self._dbphotos[p]["burst"] and not (
self._dbphotos[p]["burstPickType"] & BURST_SELECTED
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
continue

View File

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