Compare commits

...

8 Commits

Author SHA1 Message Date
Rhet Turnbull
1d6bc4e09e Additional fix for #615 2022-02-05 23:57:50 -08:00
Rhet Turnbull
3e14b718ef Updated docs [skip ci] 2022-02-05 23:12:42 -08:00
Rhet Turnbull
1ae6270561 Fixed exiftool to ignore unsupported file types, #615 2022-02-05 22:54:50 -08:00
Rhet Turnbull
55a601c07e Updated tests 2022-02-05 14:30:20 -08:00
Rhet Turnbull
7d67b81879 Updated CHANGELOG.md [skip ci] 2022-02-05 14:08:43 -08:00
Rhet Turnbull
cd02144ac3 Fix for --name searching only original_filename on Photos 5+, #594 2022-02-05 12:55:56 -08:00
Rhet Turnbull
9b247acd1c Fix for unicode in query strings, #618 2022-02-05 12:36:25 -08:00
Rhet Turnbull
942126ea3d Updated CHANGELOG.md [skip ci] 2022-02-05 10:56:18 -08:00
19 changed files with 5290 additions and 231 deletions

View File

@@ -4,6 +4,21 @@ 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.45.6](https://github.com/RhetTbull/osxphotos/compare/v0.45.5...v0.45.6)
> 5 February 2022
- Fix for unicode in query strings, #618 [`9b247ac`](https://github.com/RhetTbull/osxphotos/commit/9b247acd1cc4b2def59fdd18a6fb3c8eb9914f11)
- Fix for --name searching only original_filename on Photos 5+, #594 [`cd02144`](https://github.com/RhetTbull/osxphotos/commit/cd02144ac33cc1c13a20358133971c84d35b8a57)
#### [v0.45.5](https://github.com/RhetTbull/osxphotos/compare/v0.45.4...v0.45.5)
> 5 February 2022
- Fix for #561, no really, I mean it this time [`b3d3e14`](https://github.com/RhetTbull/osxphotos/commit/b3d3e14ffe41fbb22edb614b24f3985f379766a2)
- Updated docs [skip ci] [`2b9ea11`](https://github.com/RhetTbull/osxphotos/commit/2b9ea11701799af9a661a8e2af70fca97235f487)
- Updated tests for #561 [skip ci] [`77a49a0`](https://github.com/RhetTbull/osxphotos/commit/77a49a09a1bee74113a7114c543fbc25fa410ffc)
#### [v0.45.4](https://github.com/RhetTbull/osxphotos/compare/v0.45.3...v0.45.4) #### [v0.45.4](https://github.com/RhetTbull/osxphotos/compare/v0.45.3...v0.45.4)
> 3 February 2022 > 3 February 2022

View File

@@ -1,7 +1,7 @@
include README.md include osxphotos/*.json
include README.rst include osxphotos/*.md
include osxphotos/templates/*
include osxphotos/phototemplate.tx include osxphotos/phototemplate.tx
include osxphotos/phototemplate.md include osxphotos/queries/*
include osxphotos/tutorial.md include osxphotos/templates/*
include osxphotos/queries/* include README.md
include README.rst

View File

@@ -1725,7 +1725,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.45.5' {osxphotos_version} The osxphotos version, e.g. '0.45.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
@@ -3629,7 +3629,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.45.5'| |{osxphotos_version}|The osxphotos version, e.g. '0.45.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|

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: 99a74e6a82cae702311959821c897fdd config: bf43bf49b725c31ce72a8823e4f8012b
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.45.5', VERSION: '0.45.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.45.5 documentation</title> <title>osxphotos command line interface (CLI) &#8212; osxphotos 0.45.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.45.5 documentation</title> <title>Index &#8212; osxphotos 0.45.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.45.5 documentation</title> <title>Welcome to osxphotoss documentation! &#8212; osxphotos 0.45.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.45.5 documentation</title> <title>osxphotos &#8212; osxphotos 0.45.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.45.5 documentation</title> <title>osxphotos package &#8212; osxphotos 0.45.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.45.5 documentation</title> <title>Search &#8212; osxphotos 0.45.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

@@ -14,6 +14,7 @@ datas = [
("osxphotos/phototemplate.tx", "osxphotos"), ("osxphotos/phototemplate.tx", "osxphotos"),
("osxphotos/phototemplate.md", "osxphotos"), ("osxphotos/phototemplate.md", "osxphotos"),
("osxphotos/tutorial.md", "osxphotos"), ("osxphotos/tutorial.md", "osxphotos"),
("osxphotos/exiftool_filetypes.json", "osxphotos"),
] ]
package_imports = [["photoscript", ["photoscript.applescript"]]] package_imports = [["photoscript", ["photoscript.applescript"]]]
for package, files in package_imports: for package, files in package_imports:

View File

@@ -1,3 +1,3 @@
""" version info """ """ version info """
__version__ = "0.45.5" __version__ = "0.45.8"

View File

@@ -11,6 +11,7 @@ import html
import json import json
import logging import logging
import os import os
import pathlib
import re import re
import shutil import shutil
import subprocess import subprocess
@@ -19,11 +20,12 @@ from functools import lru_cache # pylint: disable=syntax-error
__all__ = [ __all__ = [
"escape_str", "escape_str",
"unescape_str", "exiftool_can_write",
"terminate_exiftool",
"get_exiftool_path",
"ExifTool", "ExifTool",
"ExifToolCaching", "ExifToolCaching",
"get_exiftool_path",
"terminate_exiftool",
"unescape_str",
] ]
# exiftool -stay_open commands outputs this EOF marker after command is run # exiftool -stay_open commands outputs this EOF marker after command is run
@@ -33,6 +35,24 @@ EXIFTOOL_STAYOPEN_EOF_LEN = len(EXIFTOOL_STAYOPEN_EOF)
# list of exiftool processes to cleanup when exiting or when terminate is called # list of exiftool processes to cleanup when exiting or when terminate is called
EXIFTOOL_PROCESSES = [] EXIFTOOL_PROCESSES = []
# exiftool supported file types, created by utils/exiftool_supported_types.py
EXIFTOOL_FILETYPES_JSON = "exiftool_filetypes.json"
with (pathlib.Path(__file__).parent / EXIFTOOL_FILETYPES_JSON).open("r") as f:
EXIFTOOL_SUPPORTED_FILETYPES = json.load(f)
def exiftool_can_write(suffix: str) -> bool:
"""Return True if exiftool supports writing to a file with the given suffix, otherwise False"""
if not suffix:
return False
suffix = suffix.lower()
if suffix[0] == ".":
suffix = suffix[1:]
return (
suffix in EXIFTOOL_SUPPORTED_FILETYPES
and EXIFTOOL_SUPPORTED_FILETYPES[suffix]["write"]
)
def escape_str(s): def escape_str(s):
"""escape string for use with exiftool -E""" """escape string for use with exiftool -E"""

File diff suppressed because it is too large Load Diff

View File

@@ -3,7 +3,6 @@
import dataclasses import dataclasses
import glob
import hashlib import hashlib
import json import json
import logging import logging
@@ -33,7 +32,7 @@ from ._constants import (
) )
from ._version import __version__ from ._version import __version__
from .datetime_utils import datetime_tz_to_utc from .datetime_utils import datetime_tz_to_utc
from .exiftool import ExifTool from .exiftool import ExifTool, exiftool_can_write
from .export_db import ExportDB_ABC, ExportDBNoOp from .export_db import ExportDB_ABC, ExportDBNoOp
from .fileutil import FileUtil from .fileutil import FileUtil
from .photokit import ( from .photokit import (
@@ -1260,11 +1259,27 @@ class PhotoExporter:
exiftool_results = ExportResults() exiftool_results = ExportResults()
# don't try to write if unsupported file type for exiftool
if not exiftool_can_write(os.path.splitext(src)[-1]):
exiftool_results.exiftool_warning.append(
(
dest,
f"Unsupported file type for exiftool, skipping exiftool for {dest}",
)
)
# set file signature so the file doesn't get re-exported with --update
export_db.set_data(
dest,
uuid=self.photo.uuid,
exif_stat=fileutil.file_sig(src),
exif_json=self._exiftool_json_sidecar(options=options),
)
return exiftool_results
# determine if we need to write the exif metadata # determine if we need to write the exif metadata
# if we are not updating, we always write # if we are not updating, we always write
# else, need to check the database to determine if we need to write # else, need to check the database to determine if we need to write
run_exiftool = not options.update run_exiftool = not options.update
current_data = "foo"
if options.update: if options.update:
files_are_different = False files_are_different = False
old_data = export_db.get_exifdata_for_file(dest) old_data = export_db.get_exifdata_for_file(dest)

View File

@@ -39,6 +39,7 @@ from .._constants import (
_PHOTOS_5_PROJECT_ALBUM_KIND, _PHOTOS_5_PROJECT_ALBUM_KIND,
_PHOTOS_5_ROOT_FOLDER_KIND, _PHOTOS_5_ROOT_FOLDER_KIND,
_PHOTOS_5_SHARED_ALBUM_KIND, _PHOTOS_5_SHARED_ALBUM_KIND,
_PHOTOS_5_VERSION,
_TESTED_OS_VERSIONS, _TESTED_OS_VERSIONS,
_UNKNOWN_PERSON, _UNKNOWN_PERSON,
BURST_KEY, BURST_KEY,
@@ -659,14 +660,18 @@ class PhotosDB:
for person in c: for person in c:
pk = person[0] pk = person[0]
fullname = person[2] if person[2] is not None else _UNKNOWN_PERSON fullname = (
normalize_unicode(person[2])
if person[2] is not None
else _UNKNOWN_PERSON
)
self._dbpersons_pk[pk] = { self._dbpersons_pk[pk] = {
"pk": pk, "pk": pk,
"uuid": person[1], "uuid": person[1],
"fullname": fullname, "fullname": fullname,
"facecount": person[3], "facecount": person[3],
"keyface": person[5], "keyface": person[5],
"displayname": person[4], "displayname": normalize_unicode(person[4]),
"photo_uuid": None, "photo_uuid": None,
"keyface_uuid": None, "keyface_uuid": None,
} }
@@ -733,13 +738,6 @@ class PhotosDB:
except KeyError: except KeyError:
self._dbfaces_pk[pk] = [uuid] self._dbfaces_pk[pk] = [uuid]
if _debug():
logging.debug(f"Finished walking through persons")
logging.debug(pformat(self._dbpersons_pk))
logging.debug(pformat(self._dbpersons_fullname))
logging.debug(pformat(self._dbfaces_pk))
logging.debug(pformat(self._dbfaces_uuid))
# Get info on albums # Get info on albums
verbose("Processing albums.") verbose("Processing albums.")
c.execute( c.execute(
@@ -876,14 +874,6 @@ class PhotosDB:
else: else:
self._dbalbum_folders[album] = {} self._dbalbum_folders[album] = {}
if _debug():
logging.debug(f"Finished walking through albums")
logging.debug(pformat(self._dbalbums_album))
logging.debug(pformat(self._dbalbums_uuid))
logging.debug(pformat(self._dbalbum_details))
logging.debug(pformat(self._dbalbum_folders))
logging.debug(pformat(self._dbfolder_details))
# Get info on keywords # Get info on keywords
verbose("Processing keywords.") verbose("Processing keywords.")
c.execute( c.execute(
@@ -899,13 +889,16 @@ class PhotosDB:
RKMaster.uuid = RKVersion.masterUuid RKMaster.uuid = RKVersion.masterUuid
""" """
) )
for keyword in c: for keyword_title, keyword_uuid, _ in c:
if not keyword[1] in self._dbkeywords_uuid: keyword_title = normalize_unicode(keyword_title)
self._dbkeywords_uuid[keyword[1]] = [] try:
if not keyword[0] in self._dbkeywords_keyword: self._dbkeywords_uuid[keyword_uuid].append(keyword_title)
self._dbkeywords_keyword[keyword[0]] = [] except KeyError:
self._dbkeywords_uuid[keyword[1]].append(keyword[0]) self._dbkeywords_uuid[keyword_uuid] = [keyword_title]
self._dbkeywords_keyword[keyword[0]].append(keyword[1]) try:
self._dbkeywords_keyword[keyword_title].append(keyword_uuid)
except KeyError:
self._dbkeywords_keyword[keyword_title] = [keyword_uuid]
# Get info on disk volumes # Get info on disk volumes
c.execute("select RKVolume.modelId, RKVolume.name from RKVolume") c.execute("select RKVolume.modelId, RKVolume.name from RKVolume")
@@ -1027,13 +1020,11 @@ class PhotosDB:
for row in c: for row in c:
uuid = row[0] uuid = row[0]
if _debug():
logging.debug(f"uuid = '{uuid}, master = '{row[2]}")
self._dbphotos[uuid] = {} self._dbphotos[uuid] = {}
self._dbphotos[uuid]["_uuid"] = uuid # stored here for easier debugging self._dbphotos[uuid]["_uuid"] = uuid # stored here for easier debugging
self._dbphotos[uuid]["modelID"] = row[1] self._dbphotos[uuid]["modelID"] = row[1]
self._dbphotos[uuid]["masterUuid"] = row[2] self._dbphotos[uuid]["masterUuid"] = row[2]
self._dbphotos[uuid]["filename"] = row[3] self._dbphotos[uuid]["filename"] = normalize_unicode(row[3])
# There are sometimes negative values for lastmodifieddate in the database # There are sometimes negative values for lastmodifieddate in the database
# I don't know what these mean but they will raise exception in datetime if # I don't know what these mean but they will raise exception in datetime if
@@ -1272,13 +1263,13 @@ class PhotosDB:
info["volumeId"] = row[1] info["volumeId"] = row[1]
info["imagePath"] = row[2] info["imagePath"] = row[2]
info["isMissing"] = row[3] info["isMissing"] = row[3]
info["originalFilename"] = row[4] info["originalFilename"] = normalize_unicode(row[4])
info["UTI"] = row[5] info["UTI"] = row[5]
info["modelID"] = row[6] info["modelID"] = row[6]
info["fileSize"] = row[7] info["fileSize"] = row[7]
info["isTrulyRAW"] = row[8] info["isTrulyRAW"] = row[8]
info["alternateMasterUuid"] = row[9] info["alternateMasterUuid"] = row[9]
info["filename"] = row[10] info["filename"] = normalize_unicode(row[10])
self._dbphotos_master[uuid] = info self._dbphotos_master[uuid] = info
# get details needed to find path of the edited photos # get details needed to find path of the edited photos
@@ -1550,39 +1541,6 @@ class PhotosDB:
# done processing, dump debug data if requested # done processing, dump debug data if requested
verbose("Done processing details from Photos library.") verbose("Done processing details from Photos library.")
if _debug():
logging.debug("Faces (_dbfaces_uuid):")
logging.debug(pformat(self._dbfaces_uuid))
logging.debug("Persons (_dbpersons_pk):")
logging.debug(pformat(self._dbpersons_pk))
logging.debug("Keywords by uuid (_dbkeywords_uuid):")
logging.debug(pformat(self._dbkeywords_uuid))
logging.debug("Keywords by keyword (_dbkeywords_keywords):")
logging.debug(pformat(self._dbkeywords_keyword))
logging.debug("Albums by uuid (_dbalbums_uuid):")
logging.debug(pformat(self._dbalbums_uuid))
logging.debug("Albums by album (_dbalbums_albums):")
logging.debug(pformat(self._dbalbums_album))
logging.debug("Album details (_dbalbum_details):")
logging.debug(pformat(self._dbalbum_details))
logging.debug("Album titles (_dbalbum_titles):")
logging.debug(pformat(self._dbalbum_titles))
logging.debug("Volumes (_dbvolumes):")
logging.debug(pformat(self._dbvolumes))
logging.debug("Photos (_dbphotos):")
logging.debug(pformat(self._dbphotos))
logging.debug("Burst Photos (dbphotos_burst:")
logging.debug(pformat(self._dbphotos_burst))
def _build_album_folder_hierarchy_4(self, uuid, folders=None): def _build_album_folder_hierarchy_4(self, uuid, folders=None):
"""recursively build folder/album hierarchy """recursively build folder/album hierarchy
@@ -1673,7 +1631,7 @@ class PhotosDB:
for person in c: for person in c:
pk = person[0] pk = person[0]
fullname = ( fullname = (
person[2] normalize_unicode(person[2])
if (person[2] != "" and person[2] is not None) if (person[2] != "" and person[2] is not None)
else _UNKNOWN_PERSON else _UNKNOWN_PERSON
) )
@@ -1683,7 +1641,7 @@ class PhotosDB:
"fullname": fullname, "fullname": fullname,
"facecount": person[3], "facecount": person[3],
"keyface": person[4], "keyface": person[4],
"displayname": person[5], "displayname": normalize_unicode(person[5]),
"photo_uuid": None, "photo_uuid": None,
"keyface_uuid": None, "keyface_uuid": None,
} }
@@ -1747,13 +1705,6 @@ class PhotosDB:
except KeyError: except KeyError:
self._dbfaces_pk[pk] = [uuid] self._dbfaces_pk[pk] = [uuid]
if _debug():
logging.debug(f"Finished walking through persons")
logging.debug(pformat(self._dbpersons_pk))
logging.debug(pformat(self._dbpersons_fullname))
logging.debug(pformat(self._dbfaces_pk))
logging.debug(pformat(self._dbfaces_uuid))
# get details about albums # get details about albums
verbose("Processing albums.") verbose("Processing albums.")
c.execute( c.execute(
@@ -1870,13 +1821,6 @@ class PhotosDB:
# shared albums can't be in folders # shared albums can't be in folders
self._dbalbum_folders[album] = [] self._dbalbum_folders[album] = []
if _debug():
logging.debug(f"Finished walking through albums")
logging.debug(pformat(self._dbalbums_album))
logging.debug(pformat(self._dbalbums_uuid))
logging.debug(pformat(self._dbalbum_details))
logging.debug(pformat(self._dbalbum_folders))
# get details on keywords # get details on keywords
verbose("Processing keywords.") verbose("Processing keywords.")
c.execute( c.execute(
@@ -1886,29 +1830,22 @@ class PhotosDB:
JOIN Z_1KEYWORDS ON Z_1KEYWORDS.Z_1ASSETATTRIBUTES = ZADDITIONALASSETATTRIBUTES.Z_PK JOIN Z_1KEYWORDS ON Z_1KEYWORDS.Z_1ASSETATTRIBUTES = ZADDITIONALASSETATTRIBUTES.Z_PK
JOIN ZKEYWORD ON ZKEYWORD.Z_PK = {keyword_join} """ JOIN ZKEYWORD ON ZKEYWORD.Z_PK = {keyword_join} """
) )
for keyword in c: for keyword_title, keyword_uuid in c:
keyword_title = normalize_unicode(keyword[0]) keyword_title = normalize_unicode(keyword_title)
if not keyword[1] in self._dbkeywords_uuid: try:
self._dbkeywords_uuid[keyword[1]] = [] self._dbkeywords_uuid[keyword_uuid].append(keyword_title)
if not keyword_title in self._dbkeywords_keyword: except KeyError:
self._dbkeywords_keyword[keyword_title] = [] self._dbkeywords_uuid[keyword_uuid] = [keyword_title]
self._dbkeywords_uuid[keyword[1]].append(keyword[0]) try:
self._dbkeywords_keyword[keyword_title].append(keyword[1]) self._dbkeywords_keyword[keyword_title].append(keyword_uuid)
except KeyError:
if _debug(): self._dbkeywords_keyword[keyword_title] = [keyword_uuid]
logging.debug(f"Finished walking through keywords")
logging.debug(pformat(self._dbkeywords_keyword))
logging.debug(pformat(self._dbkeywords_uuid))
# get details on disk volumes # get details on disk volumes
c.execute("SELECT ZUUID, ZNAME from ZFILESYSTEMVOLUME") c.execute("SELECT ZUUID, ZNAME from ZFILESYSTEMVOLUME")
for vol in c: for vol in c:
self._dbvolumes[vol[0]] = vol[1] self._dbvolumes[vol[0]] = vol[1]
if _debug():
logging.debug(f"Finished walking through volumes")
logging.debug(self._dbvolumes)
# get details about photos # get details about photos
verbose("Processing photo details.") verbose("Processing photo details.")
c.execute( c.execute(
@@ -2042,8 +1979,8 @@ class PhotosDB:
info["hidden"] = row[9] info["hidden"] = row[9]
info["favorite"] = row[10] info["favorite"] = row[10]
info["originalFilename"] = row[3] info["originalFilename"] = normalize_unicode(row[3])
info["filename"] = row[12] info["filename"] = normalize_unicode(row[12])
info["directory"] = row[11] info["directory"] = row[11]
# set latitude and longitude # set latitude and longitude
@@ -2521,48 +2458,6 @@ class PhotosDB:
# done processing, dump debug data if requested # done processing, dump debug data if requested
verbose("Done processing details from Photos library.") verbose("Done processing details from Photos library.")
if _debug():
logging.debug("Faces (_dbfaces_uuid):")
logging.debug(pformat(self._dbfaces_uuid))
logging.debug("Persons (_dbpersons_pk):")
logging.debug(pformat(self._dbpersons_pk))
logging.debug("Keywords by uuid (_dbkeywords_uuid):")
logging.debug(pformat(self._dbkeywords_uuid))
logging.debug("Keywords by keyword (_dbkeywords_keywords):")
logging.debug(pformat(self._dbkeywords_keyword))
logging.debug("Albums by uuid (_dbalbums_uuid):")
logging.debug(pformat(self._dbalbums_uuid))
logging.debug("Albums by album (_dbalbums_albums):")
logging.debug(pformat(self._dbalbums_album))
logging.debug("Album details (_dbalbum_details):")
logging.debug(pformat(self._dbalbum_details))
logging.debug("Album titles (_dbalbum_titles):")
logging.debug(pformat(self._dbalbum_titles))
logging.debug("Album folders (_dbalbum_folders):")
logging.debug(pformat(self._dbalbum_folders))
logging.debug("Album parent folders (_dbalbum_parent_folders):")
logging.debug(pformat(self._dbalbum_parent_folders))
logging.debug("Albums pk (_dbalbums_pk):")
logging.debug(pformat(self._dbalbums_pk))
logging.debug("Volumes (_dbvolumes):")
logging.debug(pformat(self._dbvolumes))
logging.debug("Photos (_dbphotos):")
logging.debug(pformat(self._dbphotos))
logging.debug("Burst Photos (dbphotos_burst:")
logging.debug(pformat(self._dbphotos_burst))
def _process_moments(self): def _process_moments(self):
"""Process data from ZMOMENT table""" """Process data from ZMOMENT table"""
@@ -2623,8 +2518,8 @@ class PhotosDB:
moment_info["modificationDate"] = row[6] moment_info["modificationDate"] = row[6]
moment_info["representativeDate"] = row[7] moment_info["representativeDate"] = row[7]
moment_info["startDate"] = row[8] moment_info["startDate"] = row[8]
moment_info["subtitle"] = row[9] moment_info["subtitle"] = normalize_unicode(row[9])
moment_info["title"] = row[10] moment_info["title"] = normalize_unicode(row[10])
moment_info["uuid"] = row[11] moment_info["uuid"] = row[11]
# if both lat/lon == -180, then it means location undefined # if both lat/lon == -180, then it means location undefined
@@ -3027,6 +2922,7 @@ class PhotosDB:
if keywords: if keywords:
keyword_set = set() keyword_set = set()
for keyword in keywords: for keyword in keywords:
keyword = normalize_unicode(keyword)
if keyword in self._dbkeywords_keyword: if keyword in self._dbkeywords_keyword:
keyword_set.update(self._dbkeywords_keyword[keyword]) keyword_set.update(self._dbkeywords_keyword[keyword])
photos_sets.append(keyword_set) photos_sets.append(keyword_set)
@@ -3034,6 +2930,7 @@ class PhotosDB:
if persons: if persons:
person_set = set() person_set = set()
for person in persons: for person in persons:
person = normalize_unicode(person)
if person in self._dbpersons_fullname: if person in self._dbpersons_fullname:
for pk in self._dbpersons_fullname[person]: for pk in self._dbpersons_fullname[person]:
try: try:
@@ -3076,8 +2973,6 @@ class PhotosDB:
): ):
info = PhotoInfo(db=self, uuid=p, info=self._dbphotos[p]) info = PhotoInfo(db=self, uuid=p, info=self._dbphotos[p])
photoinfo.append(info) photoinfo.append(info)
if _debug:
logging.debug(f"photoinfo: {pformat(photoinfo)}")
return photoinfo return photoinfo
@@ -3414,23 +3309,35 @@ class PhotosDB:
# case-insensitive # case-insensitive
for n in name: for n in name:
n = n.lower() n = n.lower()
photo_list.extend( if self._db_version >= _PHOTOS_5_VERSION:
[ # search only original_filename (#594)
p photo_list.extend(
for p in photos [p for p in photos if n in p.original_filename.lower()]
if n in p.filename.lower() )
or n in p.original_filename.lower() else:
] photo_list.extend(
) [
p
for p in photos
if n in p.filename.lower()
or n in p.original_filename.lower()
]
)
else: else:
for n in name: for n in name:
photo_list.extend( if self._db_version >= _PHOTOS_5_VERSION:
[ # search only original_filename (#594)
p photo_list.extend(
for p in photos [p for p in photos if n in p.original_filename]
if n in p.filename or n in p.original_filename )
] else:
) photo_list.extend(
[
p
for p in photos
if n in p.filename or n in p.original_filename
]
)
photos = photo_list photos = photo_list
if options.min_size: if options.min_size:

View File

@@ -8,6 +8,7 @@ from click.testing import CliRunner
import osxphotos import osxphotos
from osxphotos.exiftool import get_exiftool_path from osxphotos.exiftool import get_exiftool_path
from osxphotos.utils import normalize_unicode
CLI_PHOTOS_DB = "tests/Test-10.15.7.photoslibrary" CLI_PHOTOS_DB = "tests/Test-10.15.7.photoslibrary"
LIVE_PHOTOS_DB = "tests/Test-Cloud-10.15.1.photoslibrary" LIVE_PHOTOS_DB = "tests/Test-Cloud-10.15.1.photoslibrary"
@@ -6124,57 +6125,60 @@ def test_export_cleanup_exiftool_accented_album_name_same_filenames():
runner = CliRunner() runner = CliRunner()
cwd = os.getcwd() cwd = os.getcwd()
# pylint: disable=not-context-manager # pylint: disable=not-context-manager
with tempfile.TemporaryDirectory() as tempdir: with tempfile.TemporaryDirectory() as report_dir:
result = runner.invoke( # keep report file out of of expor dir for --cleanup
export, report_file = os.path.join(report_dir, "test.csv")
[ with tempfile.TemporaryDirectory() as tempdir:
os.path.join(cwd, CLI_PHOTOS_DB), result = runner.invoke(
tempdir, export,
"-V", [
"--cleanup", os.path.join(cwd, CLI_PHOTOS_DB),
"--directory", tempdir,
"{album[/,.|:,.]}", "-V",
"--exiftool", "--cleanup",
"--exiftool-merge-keywords", "--directory",
"--exiftool-merge-persons", "{album[/,.|:,.]}",
"--keyword-template", "--exiftool",
"{keyword}", "--exiftool-merge-keywords",
"--report", "--exiftool-merge-persons",
"test.csv", "--keyword-template",
"--skip-original-if-edited", "{keyword}",
"--update", "--report",
"--touch-file", report_file,
"--not-hidden", "--skip-original-if-edited",
], "--update",
) "--touch-file",
assert "Deleted: 0 files, 0 directories" in result.output "--not-hidden",
],
)
assert "Deleted: 0 files, 0 directories" in result.output
# do it again # do it again
result = runner.invoke( result = runner.invoke(
export, export,
[ [
os.path.join(cwd, CLI_PHOTOS_DB), os.path.join(cwd, CLI_PHOTOS_DB),
tempdir, tempdir,
"-V", "-V",
"--cleanup", "--cleanup",
"--directory", "--directory",
"{album[/,.|:,.]}", "{album[/,.|:,.]}",
"--exiftool", "--exiftool",
"--exiftool-merge-keywords", "--exiftool-merge-keywords",
"--exiftool-merge-persons", "--exiftool-merge-persons",
"--keyword-template", "--keyword-template",
"{keyword}", "{keyword}",
"--report", "--report",
"test.csv", report_file,
"--skip-original-if-edited", "--skip-original-if-edited",
"--update", "--update",
"--touch-file", "--touch-file",
"--not-hidden", "--not-hidden",
], ],
) )
assert "exported: 0, updated: 0" in result.output assert "exported: 0, updated: 0" in result.output
assert "updated EXIF data: 0" in result.output assert "updated EXIF data: 0" in result.output
assert "Deleted: 0 files, 0 directories" in result.output assert "Deleted: 0 files, 0 directories" in result.output
def test_save_load_config(): def test_save_load_config():
@@ -7132,6 +7136,30 @@ def test_query_name():
assert json_got[0]["original_filename"] == "DSC03584.dng" assert json_got[0]["original_filename"] == "DSC03584.dng"
def test_query_name_unicode():
"""test query --name with a unicode name"""
import json
import os
import os.path
import osxphotos
from osxphotos.cli import query
runner = CliRunner()
cwd = os.getcwd()
result = runner.invoke(
query,
["--json", "--db", os.path.join(cwd, PHOTOS_DB_15_7), "--name", "Frítest"],
)
assert result.exit_code == 0
json_got = json.loads(result.output)
assert len(json_got) == 4
assert normalize_unicode(json_got[0]["original_filename"]).startswith(
normalize_unicode("Frítest.jpg")
)
def test_query_name_i(): def test_query_name_i():
"""test query --name -i""" """test query --name -i"""
import json import json
@@ -7161,6 +7189,46 @@ def test_query_name_i():
assert json_got[0]["original_filename"] == "DSC03584.dng" assert json_got[0]["original_filename"] == "DSC03584.dng"
def test_query_name_original_filename():
"""test query --name only searches original filename on Photos 5+"""
import json
import os
import os.path
from osxphotos.cli import query
runner = CliRunner()
cwd = os.getcwd()
result = runner.invoke(
query,
["--json", "--db", os.path.join(cwd, PHOTOS_DB_15_7), "--name", "AA"],
)
assert result.exit_code == 0
json_got = json.loads(result.output)
assert len(json_got) == 4
def test_query_name_original_filename_i():
"""test query --name only searches original filename on Photos 5+ with -i"""
import json
import os
import os.path
from osxphotos.cli import query
runner = CliRunner()
cwd = os.getcwd()
result = runner.invoke(
query,
["--json", "--db", os.path.join(cwd, PHOTOS_DB_15_7), "--name", "aa", "-i"],
)
assert result.exit_code == 0
json_got = json.loads(result.output)
assert len(json_got) == 4
def test_export_name(): def test_export_name():
"""test export --name""" """test export --name"""
import glob import glob

View File

@@ -0,0 +1,57 @@
"""Read the "Supported File Types" table from exiftool.org and build a json file from the table"""
import json
import sys
import requests
from bs4 import BeautifulSoup
if __name__ == "__main__":
url = "https://www.exiftool.org/"
json_file = "exiftool_filetypes.json"
html_content = requests.get(url).text
soup = BeautifulSoup(html_content, "html.parser")
# uncomment to see all table classes
# print("Classes of each table:")
# for table in soup.find_all("table"):
# print(table.get("class"))
# strip footnotes in <span> tags
for span_tag in soup.findAll("span"):
span_tag.replace_with("")
# find the table for Supported File Types
table = soup.find("table", class_="sticky tight sm bm")
# get table headers
table_headers = [tx.text.lower() for tx in table.find_all("th")]
# get table data
table_data = []
for tr in table.find_all("tr"):
if row := [td.text for td in tr.find_all("td")]:
table_data.append(row)
# make a dictionary of the table data
supported_filetypes = {}
for row in table_data:
row_dict = dict(zip(table_headers, row))
for key, value in row_dict.items():
if value == "-":
row_dict[key] = None
row_dict["file type"] = row_dict["file type"].split(",")
row_dict["file type"] = [ft.strip() for ft in row_dict["file type"]]
row_dict["read"] = "R" in row_dict["support"]
row_dict["write"] = "W" in row_dict["support"]
row_dict["create"] = "C" in row_dict["support"]
filetypes = [ft.lower() for ft in row_dict["file type"]]
for filetype in filetypes:
supported_filetypes[filetype] = {"extension": filetype, **row_dict}
with open(json_file, "w") as jsonfile:
print(f"Writing {json_file}...")
json.dump(supported_filetypes, jsonfile, indent=4)