Compare commits

...

14 Commits

Author SHA1 Message Date
Rhet Turnbull
5a8105f5a0 Fix for #575, database version 5001 2022-01-09 07:44:38 -08:00
allcontributors[bot]
df66adeef6 docs: add ahti123 as a contributor for code, bug (#578)
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2022-01-09 07:29:15 -08:00
Ahti Liin
4e2367c868 changing photos_5 version constant to satisfy version 5001 (#577)
Co-authored-by: Ahti Liin <ahti@mooncascade.com>
2022-01-09 07:28:22 -08:00
Rhet Turnbull
53c701cc0e Added sqlgrep 2022-01-08 17:41:06 -08:00
Rhet Turnbull
92fced75da Added test for #576 2022-01-08 17:39:49 -08:00
Rhet Turnbull
4dd838b8bc Added grep command to CLI 2022-01-08 17:14:36 -08:00
Rhet Turnbull
0a3c375943 Updated CHANGELOG.md [skip ci] 2022-01-08 15:23:41 -08:00
Rhet Turnbull
64a0760a47 Updated docs [skip ci] 2022-01-08 15:23:14 -08:00
Rhet Turnbull
2e7db47806 Fix for #576, error exporting edited live photos 2022-01-08 15:15:28 -08:00
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
22 changed files with 258 additions and 101 deletions

View File

@@ -293,7 +293,8 @@
"avatar_url": "https://avatars.githubusercontent.com/u/6291?v=4", "avatar_url": "https://avatars.githubusercontent.com/u/6291?v=4",
"profile": "https://hyfen.net", "profile": "https://hyfen.net",
"contributions": [ "contributions": [
"doc", "code" "doc",
"code"
] ]
}, },
{ {
@@ -304,6 +305,16 @@
"contributions": [ "contributions": [
"bug" "bug"
] ]
},
{
"login": "ahti123",
"name": "Ahti Liin",
"avatar_url": "https://avatars.githubusercontent.com/u/22232632?v=4",
"profile": "https://github.com/ahti123",
"contributions": [
"code",
"bug"
]
} }
], ],
"contributorsPerLine": 7, "contributorsPerLine": 7,

View File

@@ -4,6 +4,34 @@ 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.7](https://github.com/RhetTbull/osxphotos/compare/v0.44.6...v0.44.7)
> 8 January 2022
- Fix for #576, error exporting edited live photos [`2e7db47`](https://github.com/RhetTbull/osxphotos/commit/2e7db47806683fdd0db4d1d75e42471d2f127d4d)
#### [v0.44.6](https://github.com/RhetTbull/osxphotos/compare/v0.44.5...v0.44.6)
> 6 January 2022
- Fix for burst images with pick type = 0, partial fix for #571 [`d2d56a7`](https://github.com/RhetTbull/osxphotos/commit/d2d56a7f7118aeffa7ac81cc474fdd4fb4843065)
#### [v0.44.5](https://github.com/RhetTbull/osxphotos/compare/v0.44.4...v0.44.5)
> 6 January 2022
- More refactoring of export code, #462 [`0c9bd87`](https://github.com/RhetTbull/osxphotos/commit/0c9bd8760261770e11b0fa59153f49f2d65e2c2f)
- Fix for #570 [`661a573`](https://github.com/RhetTbull/osxphotos/commit/661a573bf50353fb2393c604080ffe0790ade59c)
- version bump [skip ci] [`b4897ff`](https://github.com/RhetTbull/osxphotos/commit/b4897ff1b5d2bc00f34158345b2b5fe85f1490ac)
#### [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

@@ -5,7 +5,7 @@
![PyPI - Python Version](https://img.shields.io/pypi/pyversions/osxphotos) ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/osxphotos)
[![Downloads](https://static.pepy.tech/personalized-badge/osxphotos?period=month&units=international_system&left_color=black&right_color=brightgreen&left_text=downloads/month)](https://pepy.tech/project/osxphotos) [![Downloads](https://static.pepy.tech/personalized-badge/osxphotos?period=month&units=international_system&left_color=black&right_color=brightgreen&left_text=downloads/month)](https://pepy.tech/project/osxphotos)
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section --> <!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
[![All Contributors](https://img.shields.io/badge/all_contributors-32-orange.svg?style=flat)](#contributors) [![All Contributors](https://img.shields.io/badge/all_contributors-33-orange.svg?style=flat)](#contributors)
<!-- ALL-CONTRIBUTORS-BADGE:END --> <!-- ALL-CONTRIBUTORS-BADGE:END -->
OSXPhotos provides the ability to interact with and query Apple's Photos.app library on macOS. You can query the Photos library database — for example, file name, file path, and metadata such as keywords/tags, persons/faces, albums, etc. You can also easily export both the original and edited photos. OSXPhotos provides the ability to interact with and query Apple's Photos.app library on macOS. You can query the Photos library database — for example, file name, file path, and metadata such as keywords/tags, persons/faces, albums, etc. You can also easily export both the original and edited photos.
@@ -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.8'
{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.8'|
|{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|
@@ -3850,6 +3850,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<td align="center"><a href="https://alandefreitas.github.io/alandefreitas/"><img src="https://avatars.githubusercontent.com/u/5369819?v=4?s=75" width="75px;" alt=""/><br /><sub><b>Alan de Freitas</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3Aalandefreitas" title="Bug reports">🐛</a></td> <td align="center"><a href="https://alandefreitas.github.io/alandefreitas/"><img src="https://avatars.githubusercontent.com/u/5369819?v=4?s=75" width="75px;" alt=""/><br /><sub><b>Alan de Freitas</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3Aalandefreitas" title="Bug reports">🐛</a></td>
<td align="center"><a href="https://hyfen.net"><img src="https://avatars.githubusercontent.com/u/6291?v=4?s=75" width="75px;" alt=""/><br /><sub><b>Andrew Louis</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=hyfen" title="Documentation">📖</a> <a href="https://github.com/RhetTbull/osxphotos/commits?author=hyfen" title="Code">💻</a></td> <td align="center"><a href="https://hyfen.net"><img src="https://avatars.githubusercontent.com/u/6291?v=4?s=75" width="75px;" alt=""/><br /><sub><b>Andrew Louis</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=hyfen" title="Documentation">📖</a> <a href="https://github.com/RhetTbull/osxphotos/commits?author=hyfen" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/neebah"><img src="https://avatars.githubusercontent.com/u/71442026?v=4?s=75" width="75px;" alt=""/><br /><sub><b>neebah</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3Aneebah" title="Bug reports">🐛</a></td> <td align="center"><a href="https://github.com/neebah"><img src="https://avatars.githubusercontent.com/u/71442026?v=4?s=75" width="75px;" alt=""/><br /><sub><b>neebah</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3Aneebah" title="Bug reports">🐛</a></td>
<td align="center"><a href="https://github.com/ahti123"><img src="https://avatars.githubusercontent.com/u/22232632?v=4?s=75" width="75px;" alt=""/><br /><sub><b>Ahti Liin</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=ahti123" title="Code">💻</a> <a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3Aahti123" title="Bug reports">🐛</a></td>
</tr> </tr>
</table> </table>

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: fff79f4920939baa44eddc90423972ec
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.8',
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.8 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.8 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.8 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.8 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.8 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.8 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

@@ -20,8 +20,8 @@ UNICODE_FORMAT = "NFC"
# Photos 3.0 (10.13.6) == 3301 # Photos 3.0 (10.13.6) == 3301
# Photos 4.0 (10.14.5) == 4016 # Photos 4.0 (10.14.5) == 4016
# Photos 4.0 (10.14.6) == 4025 # Photos 4.0 (10.14.6) == 4025
# Photos 5.0 (10.15.0) == 6000 # Photos 5.0 (10.15.0) == 6000 or 5001
_TESTED_DB_VERSIONS = ["6000", "4025", "4016", "3301", "2622"] _TESTED_DB_VERSIONS = ["6000", "5001", "4025", "4016", "3301", "2622"]
# database model versions (applies to Photos 5, Photos 6) # database model versions (applies to Photos 5, Photos 6)
# these come from PLModelVersion key in binary plist in Z_METADATA.Z_PLIST # these come from PLModelVersion key in binary plist in Z_METADATA.Z_PLIST
@@ -37,7 +37,7 @@ _PHOTOS_3_VERSION = "3301"
# versions 5.0 and later have a different database structure # versions 5.0 and later have a different database structure
_PHOTOS_4_VERSION = "4025" # latest Mojove version on 10.14.6 _PHOTOS_4_VERSION = "4025" # latest Mojove version on 10.14.6
_PHOTOS_5_VERSION = "6000" # seems to be current on 10.15.1 through 10.15.7 (also Big Sur and Monterey which switch to model version) _PHOTOS_5_VERSION = "5000" # I've seen both 5001 and 6000. 6000 is most common on Catalina and up but there are some version 5001 database in the wild
# Ranges for model version by Photos version # Ranges for model version by Photos version
_PHOTOS_5_MODEL_VERSION = [13000, 13999] _PHOTOS_5_MODEL_VERSION = [13000, 13999]
@@ -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.8"

View File

@@ -20,7 +20,7 @@ import osxmetadata
import photoscript import photoscript
import rich.traceback import rich.traceback
import yaml import yaml
from rich import pretty from rich import pretty, print
import osxphotos import osxphotos
@@ -60,9 +60,11 @@ from .photoexporter import ExportResults, PhotoExporter
from .photoinfo import PhotoInfo from .photoinfo import PhotoInfo
from .photokit import check_photokit_authorization, request_photokit_authorization from .photokit import check_photokit_authorization, request_photokit_authorization
from .photosalbum import PhotosAlbum from .photosalbum import PhotosAlbum
from .photosdb.photosdb_utils import get_photos_library_version
from .phototemplate import PhotoTemplate, RenderOptions from .phototemplate import PhotoTemplate, RenderOptions
from .pyrepl import embed_repl from .pyrepl import embed_repl
from .queryoptions import QueryOptions from .queryoptions import QueryOptions
from .sqlgrep import sqlgrep
from .uti import get_preferred_uti_extension from .uti import get_preferred_uti_extension
from .utils import expand_and_validate_filepath, load_function, normalize_fs_path from .utils import expand_and_validate_filepath, load_function, normalize_fs_path
@@ -2954,11 +2956,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,
@@ -4285,3 +4285,49 @@ def repl(ctx, cli_obj, db, emacs):
quit_words=["q", "quit", "exit"], quit_words=["q", "quit", "exit"],
vi_mode=not emacs, vi_mode=not emacs,
) )
@cli.command(hidden=True)
@DB_OPTION
@click.pass_obj
@click.pass_context
@click.option(
"--ignore-case",
"-i",
required=False,
is_flag=True,
default=False,
help="Ignore case when searching (default is case-sensitive)",
)
@click.option(
"--print-filename",
"-p",
required=False,
is_flag=True,
default=False,
help="Print name of database file when printing results",
)
@click.argument("pattern", metavar="PATTERN", required=True)
def grep(ctx, cli_obj, db, ignore_case, print_filename, pattern):
"""Search for PATTERN in the Photos sqlite database file"""
db = db or get_photos_db()
db = pathlib.Path(db)
if db.is_file():
# if passed the actual database, really want the parent of the database directory
db = db.parent.parent
photos_ver = get_photos_library_version(str(db))
if photos_ver < 5:
db_file = db / "database" / "photos.db"
else:
db_file = db / "database" / "Photos.sqlite"
if not db_file.is_file():
click.secho(f"Could not find database file {db_file}", fg="red")
ctx.exit(2)
db_file = str(db_file)
for table, column, row_id, value in sqlgrep(
db_file, pattern, ignore_case, print_filename, rich_markup=True
):
print(", ".join([table, column, row_id, value]))

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 = 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
@@ -650,7 +615,7 @@ class PhotoExporter:
and self.photo.live_photo and self.photo.live_photo
and self.photo.path_edited_live_photo and self.photo.path_edited_live_photo
): ):
live_name = dest.parent / f"{dest_edited.stem}.mov" live_name = dest.parent / f"{dest.stem}.mov"
src_live = self.photo.path_edited_live_photo src_live = self.photo.path_edited_live_photo
results = self._export_photo( results = self._export_photo(
src_live, src_live,
@@ -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

@@ -1,6 +1,7 @@
""" utility functions used by PhotosDB """ """ utility functions used by PhotosDB """
import logging import logging
import pathlib
import plistlib import plistlib
from .._constants import ( from .._constants import (
@@ -17,7 +18,7 @@ from ..utils import _open_sql_file
def get_db_version(db_file): def get_db_version(db_file):
""" Gets the Photos DB version from LiGlobals table """Gets the Photos DB version from LiGlobals table
Args: Args:
db_file: path to photos.db database file containing LiGlobals table db_file: path to photos.db database file containing LiGlobals table
@@ -44,11 +45,11 @@ def get_db_version(db_file):
def get_model_version(db_file): def get_model_version(db_file):
""" Returns the database model version from Z_METADATA """Returns the database model version from Z_METADATA
Args: Args:
db_file: path to Photos.sqlite database file containing Z_METADATA table db_file: path to Photos.sqlite database file containing Z_METADATA table
Returns: model version as str Returns: model version as str
""" """
@@ -67,11 +68,11 @@ def get_model_version(db_file):
def get_db_model_version(db_file): def get_db_model_version(db_file):
""" Returns Photos version based on model version found in db_file """Returns Photos version based on model version found in db_file
Args: Args:
db_file: path to Photos.sqlite file db_file: path to Photos.sqlite file
Returns: int of major Photos version number (e.g. 5 or 6). Returns: int of major Photos version number (e.g. 5 or 6).
If unknown model version found, logs warning and returns most current Photos version. If unknown model version found, logs warning and returns most current Photos version.
""" """
@@ -94,7 +95,7 @@ class UnknownLibraryVersion(Exception):
def get_photos_library_version(library_path): def get_photos_library_version(library_path):
"""Return int indicating which Photos version a library was created with """ """Return int indicating which Photos version a library was created with"""
library_path = pathlib.Path(library_path) library_path = pathlib.Path(library_path)
db_ver = get_db_version(str(library_path / "database" / "photos.db")) db_ver = get_db_version(str(library_path / "database" / "photos.db"))
db_ver = int(db_ver) db_ver = int(db_ver)

55
osxphotos/sqlgrep.py Normal file
View File

@@ -0,0 +1,55 @@
"""Search through a sqlite database file for a given string"""
import re
import sqlite3
from typing import Generator, List
def sqlgrep(
filename: str,
pattern: str,
ignore_case: bool = False,
print_filename: bool = True,
rich_markup: bool = False,
) -> Generator[List[str], None, None]:
"""grep through a sqlite database file for a given string
Args:
filename (str): The filename of the sqlite database file
pattern (str): The pattern to search for
ignore_case (bool, optional): Ignore case when searching. Defaults to False.
print_filename (bool, optional): include the filename of the file with table name. Defaults to True.
rich_markup (bool, optional): Add rich markup to mark found text in bold. Defaults to False.
Returns:
Generator which yields list of [table, column, row_id, value]
"""
flags = re.IGNORECASE if ignore_case else 0
try:
with sqlite3.connect(f"file:{filename}?mode=ro", uri=True) as conn:
regex = re.compile(r"(" + pattern + r")", flags=flags)
filename_header = f"{filename}: " if print_filename else ""
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
cursor.execute("SELECT name FROM sqlite_master WHERE type='table'")
for tablerow in cursor.fetchall():
table = tablerow[0]
cursor.execute("SELECT * FROM {t}".format(t=table))
for row_num, row in enumerate(cursor):
for field in row.keys():
field_value = row[field]
if not field_value or type(field_value) == bytes:
# don't search binary blobs
next
field_value = str(field_value)
if re.search(pattern, field_value, flags=flags):
if rich_markup:
field_value = regex.sub(r"[bold]\1[/bold]", field_value)
yield [
f"{filename_header}{table}",
field,
str(row_num),
field_value,
]
except sqlite3.DatabaseError as e:
raise sqlite3.DatabaseError(f"{filename}: {e}")

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:]

View File

@@ -768,7 +768,10 @@ CLI_EXPORT_UUID_FROM_FILE_FILENAMES = [
"wedding_edited.jpeg", "wedding_edited.jpeg",
] ]
CLI_EXPORT_SKIP_UUID = ["E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51", "6191423D-8DB8-4D4C-92BE-9BBBA308AAC4"] CLI_EXPORT_SKIP_UUID = [
"E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51",
"6191423D-8DB8-4D4C-92BE-9BBBA308AAC4",
]
CLI_EXPORT_SKIP_UUID_FILENAMES = [ CLI_EXPORT_SKIP_UUID_FILENAMES = [
"Tulips.jpg", "Tulips.jpg",
"Tulips_edited.jpeg", "Tulips_edited.jpeg",
@@ -903,6 +906,14 @@ QUERY_EXIF_DATA_CASE_INSENSITIVE = [
] ]
EXPORT_EXIF_DATA = [("EXIF:Make", "FUJIFILM", ["Tulips.jpg", "Tulips_edited.jpeg"])] EXPORT_EXIF_DATA = [("EXIF:Make", "FUJIFILM", ["Tulips.jpg", "Tulips_edited.jpeg"])]
UUID_LIVE_EDITED = "136A78FA-1B90-46CC-88A7-CCA3331F0353" # IMG_4813.HEIC
CLI_EXPORT_LIVE_EDITED = [
"IMG_4813.HEIC",
"IMG_4813.mov",
"IMG_4813_edited.jpeg",
"IMG_4813_edited.mov",
]
def modify_file(filename): def modify_file(filename):
"""appends data to a file to modify it""" """appends data to a file to modify it"""
@@ -1268,7 +1279,8 @@ def test_query_duplicate():
runner = CliRunner() runner = CliRunner()
cwd = os.getcwd() cwd = os.getcwd()
result = runner.invoke( result = runner.invoke(
query, ["--json", "--db", os.path.join(cwd, CLI_PHOTOS_DB), "--duplicate"], query,
["--json", "--db", os.path.join(cwd, CLI_PHOTOS_DB), "--duplicate"],
) )
assert result.exit_code == 0 assert result.exit_code == 0
@@ -1289,7 +1301,8 @@ def test_query_location():
runner = CliRunner() runner = CliRunner()
cwd = os.getcwd() cwd = os.getcwd()
result = runner.invoke( result = runner.invoke(
query, ["--json", "--db", os.path.join(cwd, CLI_PHOTOS_DB), "--location"], query,
["--json", "--db", os.path.join(cwd, CLI_PHOTOS_DB), "--location"],
) )
assert result.exit_code == 0 assert result.exit_code == 0
@@ -1311,7 +1324,8 @@ def test_query_no_location():
runner = CliRunner() runner = CliRunner()
cwd = os.getcwd() cwd = os.getcwd()
result = runner.invoke( result = runner.invoke(
query, ["--json", "--db", os.path.join(cwd, CLI_PHOTOS_DB), "--no-location"], query,
["--json", "--db", os.path.join(cwd, CLI_PHOTOS_DB), "--no-location"],
) )
assert result.exit_code == 0 assert result.exit_code == 0
@@ -1432,6 +1446,7 @@ def test_export_uuid_from_file():
files = glob.glob("*") files = glob.glob("*")
assert sorted(files) == sorted(CLI_EXPORT_UUID_FROM_FILE_FILENAMES) assert sorted(files) == sorted(CLI_EXPORT_UUID_FROM_FILE_FILENAMES)
def test_export_skip_uuid_from_file(): def test_export_skip_uuid_from_file():
"""Test export with --skip-uuid-from-file""" """Test export with --skip-uuid-from-file"""
import glob import glob
@@ -1458,7 +1473,8 @@ def test_export_skip_uuid_from_file():
assert result.exit_code == 0 assert result.exit_code == 0
files = glob.glob("*") files = glob.glob("*")
for skipped_file in CLI_EXPORT_SKIP_UUID_FILENAMES: for skipped_file in CLI_EXPORT_SKIP_UUID_FILENAMES:
assert skipped_file not in files assert skipped_file not in files
def test_export_skip_uuid(): def test_export_skip_uuid():
"""Test export with --skip-uuid""" """Test export with --skip-uuid"""
@@ -4304,7 +4320,7 @@ def test_export_error(monkeypatch):
@pytest.mark.skipif(exiftool is None, reason="exiftool not installed") @pytest.mark.skipif(exiftool is None, reason="exiftool not installed")
@pytest.mark.parametrize("exiftag,exifvalue,files_expected", EXPORT_EXIF_DATA) @pytest.mark.parametrize("exiftag,exifvalue,files_expected", EXPORT_EXIF_DATA)
def test_export_exif(exiftag, exifvalue, files_expected): def test_export_exif(exiftag, exifvalue, files_expected):
"""Test export --exif query """ """Test export --exif query"""
import glob import glob
import os import os
import os.path import os.path
@@ -4665,6 +4681,32 @@ def test_export_update_basic():
) )
@pytest.mark.skipif(
"OSXPHOTOS_TEST_EXPORT" not in os.environ,
reason="Skip if not running on author's personal library.",
)
def test_export_live_edited():
"""test export of edited live image #576"""
import glob
import os
import os.path
from osxphotos.cli import OSXPHOTOS_EXPORT_DB, export
runner = CliRunner()
cwd = os.getcwd()
# pylint: disable=not-context-manager
with runner.isolated_filesystem():
# basic export
result = runner.invoke(
export,
[os.path.join(cwd, PHOTOS_DB_RHET), ".", "-V", "--uuid", UUID_LIVE_EDITED],
)
assert result.exit_code == 0
files = glob.glob("*")
assert sorted(files) == sorted(CLI_EXPORT_LIVE_EDITED)
def test_export_update_child_folder(): def test_export_update_child_folder():
"""test export then update into a child folder of previous export""" """test export then update into a child folder of previous export"""
import glob import glob
@@ -7642,6 +7684,7 @@ def test_export_query_function():
def test_export_album_seq(): def test_export_album_seq():
"""Test {album_seq} template""" """Test {album_seq} template"""
import glob import glob
from osxphotos.cli import cli from osxphotos.cli import cli
runner = CliRunner() runner = CliRunner()
@@ -7719,7 +7762,6 @@ def test_export_description_template_conditional():
import osxphotos import osxphotos
from osxphotos.cli import cli from osxphotos.cli import cli
from osxphotos.exiftool import ExifTool from osxphotos.exiftool import ExifTool
import json
runner = CliRunner() runner = CliRunner()
cwd = os.getcwd() cwd = os.getcwd()