Port to non-MacOS platforms (#1026)
* Port to non-MacOS platforms * Keep NFD normalization on macOS * Update locale_util.py Fix lint error from ruff (runs in CI) * Update query.py click.Option first arg needs to be a list (different than click.option) * Dynamically normalize Unicode paths in test * Fix missing import --------- Co-authored-by: Rhet Turnbull <rturnbull@gmail.com>
This commit is contained in:
parent
0c85298c03
commit
ca3da647f2
@ -16,7 +16,6 @@ from .momentinfo import MomentInfo
|
|||||||
from .personinfo import PersonInfo
|
from .personinfo import PersonInfo
|
||||||
from .photoexporter import ExportOptions, ExportResults, PhotoExporter
|
from .photoexporter import ExportOptions, ExportResults, PhotoExporter
|
||||||
from .photoinfo import PhotoInfo
|
from .photoinfo import PhotoInfo
|
||||||
from .photosalbum import PhotosAlbum, PhotosAlbumPhotoScript
|
|
||||||
from .photosdb import PhotosDB
|
from .photosdb import PhotosDB
|
||||||
from .photosdb._photosdb_process_comments import CommentInfo, LikeInfo
|
from .photosdb._photosdb_process_comments import CommentInfo, LikeInfo
|
||||||
from .phototables import PhotoTables
|
from .phototables import PhotoTables
|
||||||
@ -25,6 +24,10 @@ from .placeinfo import PlaceInfo
|
|||||||
from .queryoptions import QueryOptions
|
from .queryoptions import QueryOptions
|
||||||
from .scoreinfo import ScoreInfo
|
from .scoreinfo import ScoreInfo
|
||||||
from .searchinfo import SearchInfo
|
from .searchinfo import SearchInfo
|
||||||
|
from .utils import is_macos
|
||||||
|
|
||||||
|
if is_macos:
|
||||||
|
from .photosalbum import PhotosAlbum, PhotosAlbumPhotoScript
|
||||||
|
|
||||||
# configure logging; every module in osxphotos should use this logger
|
# configure logging; every module in osxphotos should use this logger
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import sys
|
|||||||
from rich import print
|
from rich import print
|
||||||
from rich.traceback import install as install_traceback
|
from rich.traceback import install as install_traceback
|
||||||
|
|
||||||
|
from osxphotos.utils import is_macos
|
||||||
from osxphotos.debug import (
|
from osxphotos.debug import (
|
||||||
debug_breakpoint,
|
debug_breakpoint,
|
||||||
debug_watch,
|
debug_watch,
|
||||||
@ -44,9 +45,7 @@ if args.get("--debug", False):
|
|||||||
print("Debugging enabled", file=sys.stderr)
|
print("Debugging enabled", file=sys.stderr)
|
||||||
|
|
||||||
from .about import about
|
from .about import about
|
||||||
from .add_locations import add_locations
|
|
||||||
from .albums import albums
|
from .albums import albums
|
||||||
from .batch_edit import batch_edit
|
|
||||||
from .cli import cli_main
|
from .cli import cli_main
|
||||||
from .cli_commands import (
|
from .cli_commands import (
|
||||||
abort,
|
abort,
|
||||||
@ -67,7 +66,6 @@ from .export import export
|
|||||||
from .exportdb import exportdb
|
from .exportdb import exportdb
|
||||||
from .grep import grep
|
from .grep import grep
|
||||||
from .help import help
|
from .help import help
|
||||||
from .import_cli import import_cli
|
|
||||||
from .info import info
|
from .info import info
|
||||||
from .install_uninstall_run import install, run, uninstall
|
from .install_uninstall_run import install, run, uninstall
|
||||||
from .keywords import keywords
|
from .keywords import keywords
|
||||||
@ -76,19 +74,24 @@ from .labels import labels
|
|||||||
from .list import _list_libraries, list_libraries
|
from .list import _list_libraries, list_libraries
|
||||||
from .orphans import orphans
|
from .orphans import orphans
|
||||||
from .persons import persons
|
from .persons import persons
|
||||||
from .photo_inspect import photo_inspect
|
|
||||||
from .places import places
|
from .places import places
|
||||||
from .query import query
|
from .query import query
|
||||||
from .repl import repl
|
from .repl import repl
|
||||||
from .show_command import show
|
|
||||||
from .snap_diff import diff, snap
|
from .snap_diff import diff, snap
|
||||||
from .sync import sync
|
|
||||||
from .theme import theme
|
from .theme import theme
|
||||||
from .timewarp import timewarp
|
|
||||||
from .tutorial import tutorial
|
from .tutorial import tutorial
|
||||||
from .uuid import uuid
|
|
||||||
from .version import version
|
from .version import version
|
||||||
|
|
||||||
|
if is_macos:
|
||||||
|
from .add_locations import add_locations
|
||||||
|
from .batch_edit import batch_edit
|
||||||
|
from .import_cli import import_cli
|
||||||
|
from .photo_inspect import photo_inspect
|
||||||
|
from .show_command import show
|
||||||
|
from .sync import sync
|
||||||
|
from .timewarp import timewarp
|
||||||
|
from .uuid import uuid
|
||||||
|
|
||||||
install_traceback()
|
install_traceback()
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
|||||||
@ -5,11 +5,10 @@ from __future__ import annotations
|
|||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
import click
|
import click
|
||||||
import photoscript
|
|
||||||
|
|
||||||
import osxphotos
|
import osxphotos
|
||||||
from osxphotos.queryoptions import IncompatibleQueryOptions, query_options_from_kwargs
|
from osxphotos.queryoptions import IncompatibleQueryOptions, query_options_from_kwargs
|
||||||
from osxphotos.utils import pluralize
|
from osxphotos.utils import assert_macos, pluralize
|
||||||
|
|
||||||
from .cli_params import QUERY_OPTIONS, THEME_OPTION, TIMESTAMP_OPTION, VERBOSE_OPTION
|
from .cli_params import QUERY_OPTIONS, THEME_OPTION, TIMESTAMP_OPTION, VERBOSE_OPTION
|
||||||
from .click_rich_echo import rich_click_echo as echo
|
from .click_rich_echo import rich_click_echo as echo
|
||||||
@ -18,6 +17,10 @@ from .param_types import TimeOffset
|
|||||||
from .rich_progress import rich_progress
|
from .rich_progress import rich_progress
|
||||||
from .verbose import get_verbose_console, verbose_print
|
from .verbose import get_verbose_console, verbose_print
|
||||||
|
|
||||||
|
assert_macos()
|
||||||
|
|
||||||
|
import photoscript
|
||||||
|
|
||||||
|
|
||||||
def get_location(
|
def get_location(
|
||||||
photos: list[osxphotos.PhotoInfo], idx: int, window: datetime.timedelta
|
photos: list[osxphotos.PhotoInfo], idx: int, window: datetime.timedelta
|
||||||
|
|||||||
@ -9,11 +9,15 @@ import json
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
import click
|
import click
|
||||||
import photoscript
|
|
||||||
|
|
||||||
import osxphotos
|
import osxphotos
|
||||||
from osxphotos.phototemplate import RenderOptions
|
from osxphotos.phototemplate import RenderOptions
|
||||||
from osxphotos.sqlitekvstore import SQLiteKVStore
|
from osxphotos.sqlitekvstore import SQLiteKVStore
|
||||||
|
from osxphotos.utils import assert_macos
|
||||||
|
|
||||||
|
assert_macos()
|
||||||
|
|
||||||
|
import photoscript
|
||||||
|
|
||||||
from .cli_commands import echo, echo_error, selection_command, verbose
|
from .cli_commands import echo, echo_error, selection_command, verbose
|
||||||
from .kvstore import kvstore
|
from .kvstore import kvstore
|
||||||
|
|||||||
@ -9,11 +9,10 @@ import click
|
|||||||
|
|
||||||
from osxphotos._constants import PROFILE_SORT_KEYS
|
from osxphotos._constants import PROFILE_SORT_KEYS
|
||||||
from osxphotos._version import __version__
|
from osxphotos._version import __version__
|
||||||
|
from osxphotos.utils import is_macos
|
||||||
|
|
||||||
from .about import about
|
from .about import about
|
||||||
from .add_locations import add_locations
|
|
||||||
from .albums import albums
|
from .albums import albums
|
||||||
from .batch_edit import batch_edit
|
|
||||||
from .cli_params import DB_OPTION, DEBUG_OPTIONS, JSON_OPTION, VERSION_OPTION
|
from .cli_params import DB_OPTION, DEBUG_OPTIONS, JSON_OPTION, VERSION_OPTION
|
||||||
from .common import OSXPHOTOS_HIDDEN
|
from .common import OSXPHOTOS_HIDDEN
|
||||||
from .debug_dump import debug_dump
|
from .debug_dump import debug_dump
|
||||||
@ -24,7 +23,6 @@ from .export import export
|
|||||||
from .exportdb import exportdb
|
from .exportdb import exportdb
|
||||||
from .grep import grep
|
from .grep import grep
|
||||||
from .help import help
|
from .help import help
|
||||||
from .import_cli import import_cli
|
|
||||||
from .info import info
|
from .info import info
|
||||||
from .install_uninstall_run import install, run, uninstall
|
from .install_uninstall_run import install, run, uninstall
|
||||||
from .keywords import keywords
|
from .keywords import keywords
|
||||||
@ -32,19 +30,24 @@ from .labels import labels
|
|||||||
from .list import list_libraries
|
from .list import list_libraries
|
||||||
from .orphans import orphans
|
from .orphans import orphans
|
||||||
from .persons import persons
|
from .persons import persons
|
||||||
from .photo_inspect import photo_inspect
|
|
||||||
from .places import places
|
from .places import places
|
||||||
from .query import query
|
from .query import query
|
||||||
from .repl import repl
|
from .repl import repl
|
||||||
from .show_command import show
|
|
||||||
from .snap_diff import diff, snap
|
from .snap_diff import diff, snap
|
||||||
from .sync import sync
|
|
||||||
from .theme import theme
|
from .theme import theme
|
||||||
from .timewarp import timewarp
|
|
||||||
from .tutorial import tutorial
|
from .tutorial import tutorial
|
||||||
from .uuid import uuid
|
|
||||||
from .version import version
|
from .version import version
|
||||||
|
|
||||||
|
if is_macos:
|
||||||
|
from .add_locations import add_locations
|
||||||
|
from .batch_edit import batch_edit
|
||||||
|
from .import_cli import import_cli
|
||||||
|
from .photo_inspect import photo_inspect
|
||||||
|
from .show_command import show
|
||||||
|
from .sync import sync
|
||||||
|
from .timewarp import timewarp
|
||||||
|
from .uuid import uuid
|
||||||
|
|
||||||
|
|
||||||
# Click CLI object & context settings
|
# Click CLI object & context settings
|
||||||
class CLI_Obj:
|
class CLI_Obj:
|
||||||
@ -106,11 +109,9 @@ def cli_main(ctx, db, json_, profile, profile_sort, **kwargs):
|
|||||||
|
|
||||||
|
|
||||||
# install CLI commands
|
# install CLI commands
|
||||||
for command in [
|
commands = [
|
||||||
about,
|
about,
|
||||||
add_locations,
|
|
||||||
albums,
|
albums,
|
||||||
batch_edit,
|
|
||||||
debug_dump,
|
debug_dump,
|
||||||
diff,
|
diff,
|
||||||
docs_command,
|
docs_command,
|
||||||
@ -120,7 +121,6 @@ for command in [
|
|||||||
exportdb,
|
exportdb,
|
||||||
grep,
|
grep,
|
||||||
help,
|
help,
|
||||||
import_cli,
|
|
||||||
info,
|
info,
|
||||||
install,
|
install,
|
||||||
keywords,
|
keywords,
|
||||||
@ -128,19 +128,28 @@ for command in [
|
|||||||
list_libraries,
|
list_libraries,
|
||||||
orphans,
|
orphans,
|
||||||
persons,
|
persons,
|
||||||
photo_inspect,
|
|
||||||
places,
|
places,
|
||||||
query,
|
query,
|
||||||
repl,
|
repl,
|
||||||
run,
|
run,
|
||||||
show,
|
|
||||||
snap,
|
snap,
|
||||||
sync,
|
|
||||||
theme,
|
theme,
|
||||||
timewarp,
|
|
||||||
tutorial,
|
tutorial,
|
||||||
uninstall,
|
uninstall,
|
||||||
uuid,
|
|
||||||
version,
|
version,
|
||||||
]:
|
]
|
||||||
|
|
||||||
|
if is_macos:
|
||||||
|
commands += [
|
||||||
|
add_locations,
|
||||||
|
batch_edit,
|
||||||
|
import_cli,
|
||||||
|
photo_inspect,
|
||||||
|
show,
|
||||||
|
sync,
|
||||||
|
timewarp,
|
||||||
|
uuid,
|
||||||
|
]
|
||||||
|
|
||||||
|
for command in commands:
|
||||||
cli_main.add_command(command)
|
cli_main.add_command(command)
|
||||||
|
|||||||
@ -8,6 +8,8 @@ from typing import Any, Callable
|
|||||||
import click
|
import click
|
||||||
import contextlib
|
import contextlib
|
||||||
from textwrap import dedent
|
from textwrap import dedent
|
||||||
|
|
||||||
|
from ..utils import is_macos
|
||||||
from .common import OSXPHOTOS_HIDDEN, print_version
|
from .common import OSXPHOTOS_HIDDEN, print_version
|
||||||
from .param_types import *
|
from .param_types import *
|
||||||
|
|
||||||
@ -642,6 +644,9 @@ _QUERY_PARAMETERS_DICT = {
|
|||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if not is_macos:
|
||||||
|
del _QUERY_PARAMETERS_DICT["--selected"]
|
||||||
|
|
||||||
|
|
||||||
def QUERY_OPTIONS(
|
def QUERY_OPTIONS(
|
||||||
wrapped=None, *, exclude: list[str] | None = None
|
wrapped=None, *, exclude: list[str] | None = None
|
||||||
|
|||||||
@ -1,19 +1,34 @@
|
|||||||
"""Detect dark mode on MacOS >= 10.14"""
|
"""Detect dark mode on MacOS >= 10.14 or fake it elsewhere"""
|
||||||
|
|
||||||
import objc
|
from osxphotos.utils import is_macos
|
||||||
import Foundation
|
|
||||||
|
|
||||||
|
|
||||||
def theme():
|
if is_macos:
|
||||||
with objc.autorelease_pool():
|
import objc
|
||||||
user_defaults = Foundation.NSUserDefaults.standardUserDefaults()
|
import Foundation
|
||||||
system_theme = user_defaults.stringForKey_("AppleInterfaceStyle")
|
|
||||||
return "dark" if system_theme == "Dark" else "light"
|
|
||||||
|
|
||||||
|
|
||||||
def is_dark_mode():
|
def theme():
|
||||||
return theme() == "dark"
|
with objc.autorelease_pool():
|
||||||
|
user_defaults = Foundation.NSUserDefaults.standardUserDefaults()
|
||||||
|
system_theme = user_defaults.stringForKey_("AppleInterfaceStyle")
|
||||||
|
return "dark" if system_theme == "Dark" else "light"
|
||||||
|
|
||||||
|
|
||||||
def is_light_mode():
|
def is_dark_mode():
|
||||||
return theme() == "light"
|
return theme() == "dark"
|
||||||
|
|
||||||
|
|
||||||
|
def is_light_mode():
|
||||||
|
return theme() == "light"
|
||||||
|
else:
|
||||||
|
def theme():
|
||||||
|
return "light"
|
||||||
|
|
||||||
|
|
||||||
|
def is_dark_mode():
|
||||||
|
return theme() == "dark"
|
||||||
|
|
||||||
|
|
||||||
|
def is_light_mode():
|
||||||
|
return theme() == "light"
|
||||||
|
|||||||
@ -12,13 +12,6 @@ import time
|
|||||||
from typing import Iterable, List, Optional, Tuple
|
from typing import Iterable, List, Optional, Tuple
|
||||||
|
|
||||||
import click
|
import click
|
||||||
from osxmetadata import (
|
|
||||||
MDITEM_ATTRIBUTE_DATA,
|
|
||||||
MDITEM_ATTRIBUTE_SHORT_NAMES,
|
|
||||||
OSXMetaData,
|
|
||||||
Tag,
|
|
||||||
)
|
|
||||||
from osxmetadata.constants import _TAGS_NAMES
|
|
||||||
|
|
||||||
import osxphotos
|
import osxphotos
|
||||||
from osxphotos._constants import (
|
from osxphotos._constants import (
|
||||||
@ -47,26 +40,37 @@ from osxphotos.datetime_formatter import DateTimeFormatter
|
|||||||
from osxphotos.debug import is_debug
|
from osxphotos.debug import is_debug
|
||||||
from osxphotos.exiftool import get_exiftool_path
|
from osxphotos.exiftool import get_exiftool_path
|
||||||
from osxphotos.export_db import ExportDB, ExportDBInMemory
|
from osxphotos.export_db import ExportDB, ExportDBInMemory
|
||||||
from osxphotos.fileutil import FileUtil, FileUtilNoOp, FileUtilShUtil
|
from osxphotos.fileutil import FileUtilMacOS, FileUtilNoOp, FileUtilShUtil
|
||||||
from osxphotos.path_utils import is_valid_filepath, sanitize_filename, sanitize_filepath
|
from osxphotos.path_utils import is_valid_filepath, sanitize_filename, sanitize_filepath
|
||||||
from osxphotos.photoexporter import ExportOptions, ExportResults, PhotoExporter
|
from osxphotos.photoexporter import ExportOptions, ExportResults, PhotoExporter
|
||||||
from osxphotos.photoinfo import PhotoInfoNone
|
from osxphotos.photoinfo import PhotoInfoNone
|
||||||
from osxphotos.photokit import (
|
|
||||||
check_photokit_authorization,
|
|
||||||
request_photokit_authorization,
|
|
||||||
)
|
|
||||||
from osxphotos.photosalbum import PhotosAlbum
|
|
||||||
from osxphotos.phototemplate import PhotoTemplate, RenderOptions
|
from osxphotos.phototemplate import PhotoTemplate, RenderOptions
|
||||||
from osxphotos.queryoptions import load_uuid_from_file, query_options_from_kwargs
|
from osxphotos.queryoptions import load_uuid_from_file, query_options_from_kwargs
|
||||||
from osxphotos.uti import get_preferred_uti_extension
|
from osxphotos.uti import get_preferred_uti_extension
|
||||||
from osxphotos.utils import (
|
from osxphotos.utils import (
|
||||||
format_sec_to_hhmmss,
|
format_sec_to_hhmmss,
|
||||||
get_macos_version,
|
get_macos_version,
|
||||||
|
is_macos,
|
||||||
normalize_fs_path,
|
normalize_fs_path,
|
||||||
pluralize,
|
pluralize,
|
||||||
under_test,
|
under_test,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if is_macos:
|
||||||
|
from osxmetadata import (
|
||||||
|
MDITEM_ATTRIBUTE_DATA,
|
||||||
|
MDITEM_ATTRIBUTE_SHORT_NAMES,
|
||||||
|
OSXMetaData,
|
||||||
|
Tag,
|
||||||
|
)
|
||||||
|
from osxmetadata.constants import _TAGS_NAMES
|
||||||
|
|
||||||
|
from osxphotos.photokit import (
|
||||||
|
check_photokit_authorization,
|
||||||
|
request_photokit_authorization,
|
||||||
|
)
|
||||||
|
from osxphotos.photosalbum import PhotosAlbum
|
||||||
|
|
||||||
from .cli_commands import logger
|
from .cli_commands import logger
|
||||||
from .cli_params import (
|
from .cli_params import (
|
||||||
DB_ARGUMENT,
|
DB_ARGUMENT,
|
||||||
@ -851,7 +855,6 @@ def export(
|
|||||||
retry,
|
retry,
|
||||||
save_config,
|
save_config,
|
||||||
screenshot,
|
screenshot,
|
||||||
selected,
|
|
||||||
selfie,
|
selfie,
|
||||||
shared,
|
shared,
|
||||||
sidecar,
|
sidecar,
|
||||||
@ -883,6 +886,7 @@ def export(
|
|||||||
verbose_flag,
|
verbose_flag,
|
||||||
xattr_template,
|
xattr_template,
|
||||||
year,
|
year,
|
||||||
|
selected=False, # Isn't provided on unsupported platforms
|
||||||
# debug, # debug, watch, breakpoint handled in cli/__init__.py
|
# debug, # debug, watch, breakpoint handled in cli/__init__.py
|
||||||
# watch,
|
# watch,
|
||||||
# breakpoint,
|
# breakpoint,
|
||||||
@ -1111,7 +1115,10 @@ def export(
|
|||||||
|
|
||||||
verbose(f"osxphotos version: {__version__}")
|
verbose(f"osxphotos version: {__version__}")
|
||||||
verbose(f"Python version: {sys.version}")
|
verbose(f"Python version: {sys.version}")
|
||||||
verbose(f"Platform: {platform.platform()}, {'.'.join(get_macos_version())}")
|
if is_macos:
|
||||||
|
verbose(f"Platform: {platform.platform()}, {'.'.join(get_macos_version())}")
|
||||||
|
else:
|
||||||
|
verbose(f"Platform: {platform.platform()}")
|
||||||
verbose(f"Verbose level: {verbose_flag}")
|
verbose(f"Verbose level: {verbose_flag}")
|
||||||
|
|
||||||
# validate options
|
# validate options
|
||||||
@ -1325,7 +1332,7 @@ def export(
|
|||||||
if ramdb
|
if ramdb
|
||||||
else ExportDB(dbfile=export_db_path, export_dir=dest)
|
else ExportDB(dbfile=export_db_path, export_dir=dest)
|
||||||
)
|
)
|
||||||
fileutil = FileUtilShUtil if alt_copy else FileUtil
|
fileutil = FileUtilShUtil if alt_copy or not is_macos else FileUtilMacOS
|
||||||
|
|
||||||
if verbose:
|
if verbose:
|
||||||
if export_db.was_created:
|
if export_db.was_created:
|
||||||
@ -1713,7 +1720,7 @@ def export_photo(
|
|||||||
keyword_template=None,
|
keyword_template=None,
|
||||||
description_template=None,
|
description_template=None,
|
||||||
export_db=None,
|
export_db=None,
|
||||||
fileutil=FileUtil,
|
fileutil=FileUtilShUtil,
|
||||||
dry_run=None,
|
dry_run=None,
|
||||||
touch_file=None,
|
touch_file=None,
|
||||||
edited_suffix="_edited",
|
edited_suffix="_edited",
|
||||||
|
|||||||
@ -5,7 +5,6 @@ import re
|
|||||||
import typing as t
|
import typing as t
|
||||||
|
|
||||||
import click
|
import click
|
||||||
from osxmetadata import MDITEM_ATTRIBUTE_DATA, MDITEM_ATTRIBUTE_SHORT_NAMES
|
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
from rich.markdown import Markdown
|
from rich.markdown import Markdown
|
||||||
|
|
||||||
@ -21,6 +20,10 @@ from osxphotos.phototemplate import (
|
|||||||
TEMPLATE_SUBSTITUTIONS_PATHLIB,
|
TEMPLATE_SUBSTITUTIONS_PATHLIB,
|
||||||
get_template_help,
|
get_template_help,
|
||||||
)
|
)
|
||||||
|
from osxphotos.utils import is_macos
|
||||||
|
|
||||||
|
if is_macos:
|
||||||
|
from osxmetadata import MDITEM_ATTRIBUTE_DATA, MDITEM_ATTRIBUTE_SHORT_NAMES
|
||||||
|
|
||||||
from .click_rich_echo import rich_echo_via_pager
|
from .click_rich_echo import rich_echo_via_pager
|
||||||
from .color_themes import get_theme
|
from .color_themes import get_theme
|
||||||
@ -249,68 +252,71 @@ class ExportCommand(click.Command):
|
|||||||
+ f"rebuilding the '{OSXPHOTOS_EXPORT_DB}' database."
|
+ f"rebuilding the '{OSXPHOTOS_EXPORT_DB}' database."
|
||||||
)
|
)
|
||||||
formatter.write("\n")
|
formatter.write("\n")
|
||||||
formatter.write(
|
|
||||||
rich_text("## Extended Attributes", width=formatter.width, markdown=True)
|
|
||||||
)
|
|
||||||
formatter.write("\n")
|
|
||||||
formatter.write_text(
|
|
||||||
"""
|
|
||||||
Some options (currently '--finder-tag-template', '--finder-tag-keywords', '-xattr-template') write
|
|
||||||
additional metadata accessible by Spotlight to facilitate searching.
|
|
||||||
For example, --finder-tag-keyword writes all keywords (including any specified by '--keyword-template'
|
|
||||||
or other options) to Finder tags that are searchable in Spotlight using the syntax: 'tag:tagname'.
|
|
||||||
For example, if you have images with keyword "Travel" then using '--finder-tag-keywords' you could quickly
|
|
||||||
find those images in the Finder by typing 'tag:Travel' in the Spotlight search bar.
|
|
||||||
Finder tags are written to the 'com.apple.metadata:_kMDItemUserTags' extended attribute.
|
|
||||||
Unlike EXIF metadata, extended attributes do not modify the actual file;
|
|
||||||
the metadata is written to extended attributes associated with the file and the Spotlight metadata database.
|
|
||||||
Most cloud storage services do not synch extended attributes.
|
|
||||||
Dropbox does sync them and any changes to a file's extended attributes
|
|
||||||
will cause Dropbox to re-sync the files.
|
|
||||||
|
|
||||||
The following attributes may be used with '--xattr-template':
|
if is_macos:
|
||||||
|
formatter.write(
|
||||||
"""
|
rich_text("## Extended Attributes", width=formatter.width, markdown=True)
|
||||||
)
|
|
||||||
|
|
||||||
# build help text from all the attribute names
|
|
||||||
# passed to click.HelpFormatter.write_dl for formatting
|
|
||||||
attr_tuples = [
|
|
||||||
(
|
|
||||||
rich_text("[bold]Attribute[/bold]", width=formatter.width),
|
|
||||||
rich_text("[bold]Description[/bold]", width=formatter.width),
|
|
||||||
)
|
)
|
||||||
]
|
formatter.write("\n")
|
||||||
for attr_key in sorted(EXTENDED_ATTRIBUTE_NAMES):
|
formatter.write_text(
|
||||||
# get short and long name
|
"""
|
||||||
attr = MDITEM_ATTRIBUTE_SHORT_NAMES[attr_key]
|
Some options (currently '--finder-tag-template', '--finder-tag-keywords', '-xattr-template') write
|
||||||
short_name = MDITEM_ATTRIBUTE_DATA[attr]["short_name"]
|
additional metadata accessible by Spotlight to facilitate searching.
|
||||||
long_name = MDITEM_ATTRIBUTE_DATA[attr]["name"]
|
For example, --finder-tag-keyword writes all keywords (including any specified by '--keyword-template'
|
||||||
constant = MDITEM_ATTRIBUTE_DATA[attr]["xattr_constant"]
|
or other options) to Finder tags that are searchable in Spotlight using the syntax: 'tag:tagname'.
|
||||||
|
For example, if you have images with keyword "Travel" then using '--finder-tag-keywords' you could quickly
|
||||||
|
find those images in the Finder by typing 'tag:Travel' in the Spotlight search bar.
|
||||||
|
Finder tags are written to the 'com.apple.metadata:_kMDItemUserTags' extended attribute.
|
||||||
|
Unlike EXIF metadata, extended attributes do not modify the actual file;
|
||||||
|
the metadata is written to extended attributes associated with the file and the Spotlight metadata database.
|
||||||
|
Most cloud storage services do not synch extended attributes.
|
||||||
|
Dropbox does sync them and any changes to a file's extended attributes
|
||||||
|
will cause Dropbox to re-sync the files.
|
||||||
|
|
||||||
# get help text
|
The following attributes may be used with '--xattr-template':
|
||||||
description = MDITEM_ATTRIBUTE_DATA[attr]["description"]
|
|
||||||
type_ = MDITEM_ATTRIBUTE_DATA[attr]["help_type"]
|
|
||||||
attr_help = f"{long_name}; {constant}; {description}; {type_}"
|
|
||||||
|
|
||||||
# add to list
|
"""
|
||||||
attr_tuples.append((short_name, attr_help))
|
)
|
||||||
|
|
||||||
formatter.write_dl(attr_tuples)
|
# build help text from all the attribute names
|
||||||
formatter.write("\n")
|
# passed to click.HelpFormatter.write_dl for formatting
|
||||||
formatter.write_text(
|
attr_tuples = [
|
||||||
"For additional information on extended attributes see: https://developer.apple.com/documentation/coreservices/file_metadata/mditem/common_metadata_attribute_keys"
|
(
|
||||||
)
|
rich_text("[bold]Attribute[/bold]", width=formatter.width),
|
||||||
formatter.write("\n")
|
rich_text("[bold]Description[/bold]", width=formatter.width),
|
||||||
formatter.write(
|
)
|
||||||
rich_text("## Templating System", width=formatter.width, markdown=True)
|
]
|
||||||
)
|
for attr_key in sorted(EXTENDED_ATTRIBUTE_NAMES):
|
||||||
formatter.write("\n")
|
# get short and long name
|
||||||
help_text += formatter.getvalue()
|
attr = MDITEM_ATTRIBUTE_SHORT_NAMES[attr_key]
|
||||||
help_text += template_help(width=formatter.width)
|
short_name = MDITEM_ATTRIBUTE_DATA[attr]["short_name"]
|
||||||
formatter = click.HelpFormatter(width=HELP_WIDTH)
|
long_name = MDITEM_ATTRIBUTE_DATA[attr]["name"]
|
||||||
|
constant = MDITEM_ATTRIBUTE_DATA[attr]["xattr_constant"]
|
||||||
|
|
||||||
|
# get help text
|
||||||
|
description = MDITEM_ATTRIBUTE_DATA[attr]["description"]
|
||||||
|
type_ = MDITEM_ATTRIBUTE_DATA[attr]["help_type"]
|
||||||
|
attr_help = f"{long_name}; {constant}; {description}; {type_}"
|
||||||
|
|
||||||
|
# add to list
|
||||||
|
attr_tuples.append((short_name, attr_help))
|
||||||
|
|
||||||
|
formatter.write_dl(attr_tuples)
|
||||||
|
formatter.write("\n")
|
||||||
|
formatter.write_text(
|
||||||
|
"For additional information on extended attributes see: https://developer.apple.com/documentation/coreservices/file_metadata/mditem/common_metadata_attribute_keys"
|
||||||
|
)
|
||||||
|
formatter.write("\n")
|
||||||
|
formatter.write(
|
||||||
|
rich_text("## Templating System", width=formatter.width, markdown=True)
|
||||||
|
)
|
||||||
|
formatter.write("\n")
|
||||||
|
help_text += formatter.getvalue()
|
||||||
|
help_text += template_help(width=formatter.width)
|
||||||
|
formatter = click.HelpFormatter(width=HELP_WIDTH)
|
||||||
|
|
||||||
|
formatter.write("\n")
|
||||||
|
|
||||||
formatter.write("\n")
|
|
||||||
formatter.write_text(
|
formatter.write_text(
|
||||||
"With the --directory and --filename options you may specify a template for the "
|
"With the --directory and --filename options you may specify a template for the "
|
||||||
+ "export directory or filename, respectively. "
|
+ "export directory or filename, respectively. "
|
||||||
|
|||||||
@ -20,7 +20,6 @@ from textwrap import dedent
|
|||||||
from typing import Callable, Dict, List, Optional, Tuple, Union
|
from typing import Callable, Dict, List, Optional, Tuple, Union
|
||||||
|
|
||||||
import click
|
import click
|
||||||
from photoscript import Photo, PhotosLibrary
|
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
from rich.markdown import Markdown
|
from rich.markdown import Markdown
|
||||||
from strpdatetime import strpdatetime
|
from strpdatetime import strpdatetime
|
||||||
@ -43,7 +42,11 @@ from osxphotos.photoinfo import PhotoInfoNone
|
|||||||
from osxphotos.photosalbum import PhotosAlbumPhotoScript
|
from osxphotos.photosalbum import PhotosAlbumPhotoScript
|
||||||
from osxphotos.phototemplate import PhotoTemplate, RenderOptions
|
from osxphotos.phototemplate import PhotoTemplate, RenderOptions
|
||||||
from osxphotos.sqlitekvstore import SQLiteKVStore
|
from osxphotos.sqlitekvstore import SQLiteKVStore
|
||||||
from osxphotos.utils import pluralize
|
from osxphotos.utils import assert_macos, pluralize
|
||||||
|
|
||||||
|
assert_macos()
|
||||||
|
|
||||||
|
from photoscript import Photo, PhotosLibrary
|
||||||
|
|
||||||
from .cli_params import THEME_OPTION
|
from .cli_params import THEME_OPTION
|
||||||
from .click_rich_echo import rich_click_echo, rich_echo_error
|
from .click_rich_echo import rich_click_echo, rich_echo_error
|
||||||
|
|||||||
@ -13,8 +13,6 @@ from typing import Generator, List, Optional, Tuple
|
|||||||
|
|
||||||
import bitmath
|
import bitmath
|
||||||
import click
|
import click
|
||||||
from applescript import ScriptError
|
|
||||||
from photoscript import PhotosLibrary
|
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
from rich.layout import Layout
|
from rich.layout import Layout
|
||||||
from rich.live import Live
|
from rich.live import Live
|
||||||
@ -23,8 +21,14 @@ from rich.panel import Panel
|
|||||||
from osxphotos import PhotoInfo, PhotosDB
|
from osxphotos import PhotoInfo, PhotosDB
|
||||||
from osxphotos._constants import _UNKNOWN_PERSON, search_category_factory
|
from osxphotos._constants import _UNKNOWN_PERSON, search_category_factory
|
||||||
from osxphotos.rich_utils import add_rich_markup_tag
|
from osxphotos.rich_utils import add_rich_markup_tag
|
||||||
|
from osxphotos.utils import assert_macos, dd_to_dms_str
|
||||||
|
|
||||||
|
assert_macos()
|
||||||
|
|
||||||
|
from applescript import ScriptError
|
||||||
|
from photoscript import PhotosLibrary
|
||||||
|
|
||||||
from osxphotos.text_detection import detect_text as detect_text_in_photo
|
from osxphotos.text_detection import detect_text as detect_text_in_photo
|
||||||
from osxphotos.utils import dd_to_dms_str
|
|
||||||
|
|
||||||
from .cli_params import DB_OPTION, THEME_OPTION
|
from .cli_params import DB_OPTION, THEME_OPTION
|
||||||
from .color_themes import get_theme
|
from .color_themes import get_theme
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
"""query command for osxphotos CLI"""
|
"""query command for osxphotos CLI"""
|
||||||
|
|
||||||
|
import sys
|
||||||
import click
|
import click
|
||||||
|
|
||||||
import osxphotos
|
import osxphotos
|
||||||
@ -9,9 +10,12 @@ from osxphotos.cli.click_rich_echo import (
|
|||||||
set_rich_theme,
|
set_rich_theme,
|
||||||
)
|
)
|
||||||
from osxphotos.debug import set_debug
|
from osxphotos.debug import set_debug
|
||||||
from osxphotos.photosalbum import PhotosAlbum
|
|
||||||
from osxphotos.phototemplate import RenderOptions
|
from osxphotos.phototemplate import RenderOptions
|
||||||
from osxphotos.queryoptions import query_options_from_kwargs
|
from osxphotos.queryoptions import query_options_from_kwargs
|
||||||
|
from osxphotos.utils import assert_macos, is_macos
|
||||||
|
|
||||||
|
if is_macos:
|
||||||
|
from osxphotos.photosalbum import PhotosAlbum
|
||||||
|
|
||||||
from .cli_params import (
|
from .cli_params import (
|
||||||
DB_ARGUMENT,
|
DB_ARGUMENT,
|
||||||
@ -20,6 +24,7 @@ from .cli_params import (
|
|||||||
FIELD_OPTION,
|
FIELD_OPTION,
|
||||||
JSON_OPTION,
|
JSON_OPTION,
|
||||||
QUERY_OPTIONS,
|
QUERY_OPTIONS,
|
||||||
|
make_click_option_decorator,
|
||||||
)
|
)
|
||||||
from .color_themes import get_default_theme
|
from .color_themes import get_default_theme
|
||||||
from .common import CLI_COLOR_ERROR, CLI_COLOR_WARNING, OSXPHOTOS_HIDDEN, get_photos_db
|
from .common import CLI_COLOR_ERROR, CLI_COLOR_WARNING, OSXPHOTOS_HIDDEN, get_photos_db
|
||||||
@ -27,20 +32,23 @@ from .list import _list_libraries
|
|||||||
from .print_photo_info import print_photo_fields, print_photo_info
|
from .print_photo_info import print_photo_fields, print_photo_info
|
||||||
from .verbose import get_verbose_console
|
from .verbose import get_verbose_console
|
||||||
|
|
||||||
|
MACOS_OPTIONS = make_click_option_decorator(*[
|
||||||
|
click.Option(
|
||||||
|
["--add-to-album"],
|
||||||
|
metavar="ALBUM",
|
||||||
|
help="Add all photos from query to album ALBUM in Photos. Album ALBUM will be created "
|
||||||
|
"if it doesn't exist. All photos in the query results will be added to this album. "
|
||||||
|
"This only works if the Photos library being queried is the last-opened (default) library in Photos. "
|
||||||
|
"This feature is currently experimental. I don't know how well it will work on large query sets.",
|
||||||
|
),
|
||||||
|
] if is_macos else [])
|
||||||
|
|
||||||
@click.command()
|
@click.command()
|
||||||
@DB_OPTION
|
@DB_OPTION
|
||||||
@JSON_OPTION
|
@JSON_OPTION
|
||||||
@QUERY_OPTIONS
|
@QUERY_OPTIONS
|
||||||
@DELETED_OPTIONS
|
@DELETED_OPTIONS
|
||||||
@click.option(
|
@MACOS_OPTIONS
|
||||||
"--add-to-album",
|
|
||||||
metavar="ALBUM",
|
|
||||||
help="Add all photos from query to album ALBUM in Photos. Album ALBUM will be created "
|
|
||||||
"if it doesn't exist. All photos in the query results will be added to this album. "
|
|
||||||
"This only works if the Photos library being queried is the last-opened (default) library in Photos. "
|
|
||||||
"This feature is currently experimental. I don't know how well it will work on large query sets.",
|
|
||||||
)
|
|
||||||
@click.option(
|
@click.option(
|
||||||
"--quiet",
|
"--quiet",
|
||||||
is_flag=True,
|
is_flag=True,
|
||||||
@ -70,8 +78,8 @@ def query(
|
|||||||
json_,
|
json_,
|
||||||
print_template,
|
print_template,
|
||||||
quiet,
|
quiet,
|
||||||
add_to_album,
|
|
||||||
photos_library,
|
photos_library,
|
||||||
|
add_to_album=False,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
"""Query the Photos database using 1 or more search options;
|
"""Query the Photos database using 1 or more search options;
|
||||||
@ -124,6 +132,8 @@ def query(
|
|||||||
cli_json = cli_obj.json if cli_obj is not None else None
|
cli_json = cli_obj.json if cli_obj is not None else None
|
||||||
|
|
||||||
if add_to_album and photos:
|
if add_to_album and photos:
|
||||||
|
assert_macos()
|
||||||
|
|
||||||
album_query = PhotosAlbum(add_to_album, verbose=None)
|
album_query = PhotosAlbum(add_to_album, verbose=None)
|
||||||
photo_len = len(photos)
|
photo_len = len(photos)
|
||||||
photo_word = "photos" if photo_len > 1 else "photo"
|
photo_word = "photos" if photo_len > 1 else "photo"
|
||||||
|
|||||||
@ -10,8 +10,6 @@ import time
|
|||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
import click
|
import click
|
||||||
import photoscript
|
|
||||||
from applescript import ScriptError
|
|
||||||
from rich import pretty, print
|
from rich import pretty, print
|
||||||
|
|
||||||
import osxphotos
|
import osxphotos
|
||||||
@ -25,6 +23,11 @@ from osxphotos.queryoptions import (
|
|||||||
QueryOptions,
|
QueryOptions,
|
||||||
query_options_from_kwargs,
|
query_options_from_kwargs,
|
||||||
)
|
)
|
||||||
|
from osxphotos.utils import assert_macos, is_macos
|
||||||
|
|
||||||
|
if is_macos:
|
||||||
|
import photoscript
|
||||||
|
from applescript import ScriptError
|
||||||
|
|
||||||
from .cli_params import DB_ARGUMENT, DB_OPTION, DELETED_OPTIONS, QUERY_OPTIONS
|
from .cli_params import DB_ARGUMENT, DB_OPTION, DELETED_OPTIONS, QUERY_OPTIONS
|
||||||
from .common import get_photos_db
|
from .common import get_photos_db
|
||||||
@ -55,7 +58,8 @@ def repl(ctx, cli_obj, db, emacs, beta, **kwargs):
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from objexplore import explore
|
from objexplore import explore
|
||||||
from photoscript import Album, Photo, PhotosLibrary
|
if is_macos:
|
||||||
|
from photoscript import Album, Photo, PhotosLibrary
|
||||||
from rich import inspect as _inspect
|
from rich import inspect as _inspect
|
||||||
|
|
||||||
from osxphotos import ExifTool, PhotoInfo, PhotosDB
|
from osxphotos import ExifTool, PhotoInfo, PhotosDB
|
||||||
@ -194,6 +198,7 @@ def _get_selected(photosdb):
|
|||||||
"""get list of PhotoInfo objects for photos selected in Photos"""
|
"""get list of PhotoInfo objects for photos selected in Photos"""
|
||||||
|
|
||||||
def get_selected():
|
def get_selected():
|
||||||
|
assert_macos()
|
||||||
try:
|
try:
|
||||||
selected = photoscript.PhotosLibrary().selection
|
selected = photoscript.PhotosLibrary().selection
|
||||||
except ScriptError as e:
|
except ScriptError as e:
|
||||||
@ -209,6 +214,7 @@ def _get_selected(photosdb):
|
|||||||
|
|
||||||
|
|
||||||
def _spotlight_photo(photo: PhotoInfo):
|
def _spotlight_photo(photo: PhotoInfo):
|
||||||
|
assert_macos()
|
||||||
photo_ = photoscript.Photo(photo.uuid)
|
photo_ = photoscript.Photo(photo.uuid)
|
||||||
photo_.spotlight()
|
photo_.spotlight()
|
||||||
|
|
||||||
|
|||||||
@ -7,12 +7,15 @@ import click
|
|||||||
|
|
||||||
from osxphotos._constants import UUID_PATTERN
|
from osxphotos._constants import UUID_PATTERN
|
||||||
from osxphotos.export_db_utils import get_uuid_for_filepath
|
from osxphotos.export_db_utils import get_uuid_for_filepath
|
||||||
|
from osxphotos.photosdb.photosdb_utils import get_photos_library_version
|
||||||
|
from osxphotos.utils import get_last_library_path, assert_macos
|
||||||
|
|
||||||
|
assert_macos()
|
||||||
|
|
||||||
from osxphotos.photoscript_utils import (
|
from osxphotos.photoscript_utils import (
|
||||||
photoscript_object_from_name,
|
photoscript_object_from_name,
|
||||||
photoscript_object_from_uuid,
|
photoscript_object_from_uuid,
|
||||||
)
|
)
|
||||||
from osxphotos.photosdb.photosdb_utils import get_photos_library_version
|
|
||||||
from osxphotos.utils import get_last_library_path
|
|
||||||
|
|
||||||
from .cli_commands import echo, echo_error
|
from .cli_commands import echo, echo_error
|
||||||
from .cli_params import DB_OPTION
|
from .cli_params import DB_OPTION
|
||||||
|
|||||||
@ -9,7 +9,6 @@ import pathlib
|
|||||||
from typing import Any, Callable, Literal
|
from typing import Any, Callable, Literal
|
||||||
|
|
||||||
import click
|
import click
|
||||||
import photoscript
|
|
||||||
|
|
||||||
from osxphotos import PhotoInfo, PhotosDB, __version__
|
from osxphotos import PhotoInfo, PhotosDB, __version__
|
||||||
from osxphotos.photoinfo import PhotoInfoNone
|
from osxphotos.photoinfo import PhotoInfoNone
|
||||||
@ -22,7 +21,11 @@ from osxphotos.queryoptions import (
|
|||||||
query_options_from_kwargs,
|
query_options_from_kwargs,
|
||||||
)
|
)
|
||||||
from osxphotos.sqlitekvstore import SQLiteKVStore
|
from osxphotos.sqlitekvstore import SQLiteKVStore
|
||||||
from osxphotos.utils import pluralize
|
from osxphotos.utils import assert_macos, pluralize
|
||||||
|
|
||||||
|
assert_macos()
|
||||||
|
|
||||||
|
import photoscript
|
||||||
|
|
||||||
from .cli_params import (
|
from .cli_params import (
|
||||||
DB_OPTION,
|
DB_OPTION,
|
||||||
|
|||||||
@ -7,7 +7,6 @@ from functools import partial
|
|||||||
from textwrap import dedent
|
from textwrap import dedent
|
||||||
|
|
||||||
import click
|
import click
|
||||||
from photoscript import PhotosLibrary
|
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
|
|
||||||
from osxphotos._constants import APP_NAME
|
from osxphotos._constants import APP_NAME
|
||||||
@ -25,9 +24,13 @@ from osxphotos.photodates import (
|
|||||||
update_photo_from_function,
|
update_photo_from_function,
|
||||||
update_photo_time_for_new_timezone,
|
update_photo_time_for_new_timezone,
|
||||||
)
|
)
|
||||||
from osxphotos.photosalbum import PhotosAlbumPhotoScript
|
|
||||||
from osxphotos.phototz import PhotoTimeZone, PhotoTimeZoneUpdater
|
from osxphotos.phototz import PhotoTimeZone, PhotoTimeZoneUpdater
|
||||||
from osxphotos.utils import noop, pluralize
|
from osxphotos.utils import assert_macos, noop, pluralize
|
||||||
|
|
||||||
|
assert_macos()
|
||||||
|
|
||||||
|
from photoscript import PhotosLibrary
|
||||||
|
from osxphotos.photosalbum import PhotosAlbumPhotoScript
|
||||||
|
|
||||||
from .cli_params import THEME_OPTION, TIMESTAMP_OPTION, VERBOSE_OPTION
|
from .cli_params import THEME_OPTION, TIMESTAMP_OPTION, VERBOSE_OPTION
|
||||||
from .click_rich_echo import rich_click_echo as echo
|
from .click_rich_echo import rich_click_echo as echo
|
||||||
|
|||||||
@ -1,6 +1,11 @@
|
|||||||
"""uuid command for osxphotos CLI"""
|
"""uuid command for osxphotos CLI"""
|
||||||
|
|
||||||
import click
|
import click
|
||||||
|
|
||||||
|
from osxphotos.utils import assert_macos
|
||||||
|
|
||||||
|
assert_macos()
|
||||||
|
|
||||||
import photoscript
|
import photoscript
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -5,12 +5,15 @@ from typing import Callable, List, Optional, Tuple
|
|||||||
|
|
||||||
from osxphotos import PhotosDB
|
from osxphotos import PhotosDB
|
||||||
from osxphotos.exiftool import ExifTool
|
from osxphotos.exiftool import ExifTool
|
||||||
from photoscript import Photo
|
|
||||||
|
|
||||||
from .datetime_utils import datetime_naive_to_local, datetime_to_new_tz
|
from .datetime_utils import datetime_naive_to_local, datetime_to_new_tz
|
||||||
|
from .utils import assert_macos, noop
|
||||||
|
|
||||||
|
assert_macos()
|
||||||
|
|
||||||
|
from photoscript import Photo
|
||||||
from .exif_datetime_updater import get_exif_date_time_offset
|
from .exif_datetime_updater import get_exif_date_time_offset
|
||||||
from .phototz import PhotoTimeZone
|
from .phototz import PhotoTimeZone
|
||||||
from .utils import noop
|
|
||||||
|
|
||||||
ExifDiff = namedtuple(
|
ExifDiff = namedtuple(
|
||||||
"ExifDiff",
|
"ExifDiff",
|
||||||
|
|||||||
@ -9,9 +9,11 @@ import typing as t
|
|||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from tempfile import TemporaryDirectory
|
from tempfile import TemporaryDirectory
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
from .imageconverter import ImageConverter
|
from .imageconverter import ImageConverter
|
||||||
|
from .utils import is_macos, normalize_fs_path
|
||||||
|
|
||||||
|
if is_macos:
|
||||||
|
import Foundation
|
||||||
|
|
||||||
__all__ = ["FileUtilABC", "FileUtilMacOS", "FileUtilShUtil", "FileUtil", "FileUtilNoOp"]
|
__all__ = ["FileUtilABC", "FileUtilMacOS", "FileUtilShUtil", "FileUtil", "FileUtilNoOp"]
|
||||||
|
|
||||||
@ -90,6 +92,9 @@ class FileUtilMacOS(FileUtilABC):
|
|||||||
if src is None or dest is None:
|
if src is None or dest is None:
|
||||||
raise ValueError("src and dest must not be None", src, dest)
|
raise ValueError("src and dest must not be None", src, dest)
|
||||||
|
|
||||||
|
src = normalize_fs_path(src)
|
||||||
|
dest = normalize_fs_path(dest)
|
||||||
|
|
||||||
if not os.path.isfile(src):
|
if not os.path.isfile(src):
|
||||||
raise FileNotFoundError("src file does not appear to exist", src)
|
raise FileNotFoundError("src file does not appear to exist", src)
|
||||||
|
|
||||||
@ -115,6 +120,9 @@ class FileUtilMacOS(FileUtilABC):
|
|||||||
OSError if copy fails
|
OSError if copy fails
|
||||||
TypeError if either path is None
|
TypeError if either path is None
|
||||||
"""
|
"""
|
||||||
|
src = normalize_fs_path(src)
|
||||||
|
dest = normalize_fs_path(dest)
|
||||||
|
|
||||||
if not isinstance(src, pathlib.Path):
|
if not isinstance(src, pathlib.Path):
|
||||||
src = pathlib.Path(src)
|
src = pathlib.Path(src)
|
||||||
|
|
||||||
@ -135,6 +143,7 @@ class FileUtilMacOS(FileUtilABC):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def unlink(cls, filepath):
|
def unlink(cls, filepath):
|
||||||
"""unlink filepath; if it's pathlib.Path, use Path.unlink, otherwise use os.unlink"""
|
"""unlink filepath; if it's pathlib.Path, use Path.unlink, otherwise use os.unlink"""
|
||||||
|
filepath = normalize_fs_path(filepath)
|
||||||
if isinstance(filepath, pathlib.Path):
|
if isinstance(filepath, pathlib.Path):
|
||||||
filepath.unlink()
|
filepath.unlink()
|
||||||
else:
|
else:
|
||||||
@ -143,6 +152,7 @@ class FileUtilMacOS(FileUtilABC):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def rmdir(cls, dirpath):
|
def rmdir(cls, dirpath):
|
||||||
"""remove directory filepath; dirpath must be empty"""
|
"""remove directory filepath; dirpath must be empty"""
|
||||||
|
dirpath = normalize_fs_path(dirpath)
|
||||||
if isinstance(dirpath, pathlib.Path):
|
if isinstance(dirpath, pathlib.Path):
|
||||||
dirpath.rmdir()
|
dirpath.rmdir()
|
||||||
else:
|
else:
|
||||||
@ -151,6 +161,7 @@ class FileUtilMacOS(FileUtilABC):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def utime(cls, path, times):
|
def utime(cls, path, times):
|
||||||
"""Set the access and modified time of path."""
|
"""Set the access and modified time of path."""
|
||||||
|
path = normalize_fs_path(path)
|
||||||
os.utime(path, times=times)
|
os.utime(path, times=times)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -166,6 +177,9 @@ class FileUtilMacOS(FileUtilABC):
|
|||||||
Does not do a byte-by-byte comparison.
|
Does not do a byte-by-byte comparison.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
f1 = normalize_fs_path(f1)
|
||||||
|
f2 = normalize_fs_path(f2)
|
||||||
|
|
||||||
s1 = cls._sig(os.stat(f1))
|
s1 = cls._sig(os.stat(f1))
|
||||||
if mtime1 is not None:
|
if mtime1 is not None:
|
||||||
s1 = (s1[0], s1[1], int(mtime1))
|
s1 = (s1[0], s1[1], int(mtime1))
|
||||||
@ -188,6 +202,7 @@ class FileUtilMacOS(FileUtilABC):
|
|||||||
if not s2:
|
if not s2:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
f1 = normalize_fs_path(f1)
|
||||||
s1 = cls._sig(os.stat(f1))
|
s1 = cls._sig(os.stat(f1))
|
||||||
if s1[0] != stat.S_IFREG or s2[0] != stat.S_IFREG:
|
if s1[0] != stat.S_IFREG or s2[0] != stat.S_IFREG:
|
||||||
return False
|
return False
|
||||||
@ -196,6 +211,7 @@ class FileUtilMacOS(FileUtilABC):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def file_sig(cls, f1):
|
def file_sig(cls, f1):
|
||||||
"""return os.stat signature for file f1 as tuple of (mode, size, mtime)"""
|
"""return os.stat signature for file f1 as tuple of (mode, size, mtime)"""
|
||||||
|
f1 = normalize_fs_path(f1)
|
||||||
return cls._sig(os.stat(f1))
|
return cls._sig(os.stat(f1))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -210,6 +226,8 @@ class FileUtilMacOS(FileUtilABC):
|
|||||||
Returns:
|
Returns:
|
||||||
True if success, otherwise False
|
True if success, otherwise False
|
||||||
"""
|
"""
|
||||||
|
src_file = normalize_fs_path(src_file)
|
||||||
|
dest_file = normalize_fs_path(dest_file)
|
||||||
converter = ImageConverter()
|
converter = ImageConverter()
|
||||||
return converter.write_jpeg(
|
return converter.write_jpeg(
|
||||||
src_file, dest_file, compression_quality=compression_quality
|
src_file, dest_file, compression_quality=compression_quality
|
||||||
@ -227,6 +245,8 @@ class FileUtilMacOS(FileUtilABC):
|
|||||||
Name of renamed file (dest)
|
Name of renamed file (dest)
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
src = normalize_fs_path(src)
|
||||||
|
dest = normalize_fs_path(dest)
|
||||||
os.rename(str(src), str(dest))
|
os.rename(str(src), str(dest))
|
||||||
return dest
|
return dest
|
||||||
|
|
||||||
@ -271,6 +291,9 @@ class FileUtilShUtil(FileUtilMacOS):
|
|||||||
OSError if copy fails
|
OSError if copy fails
|
||||||
TypeError if either path is None
|
TypeError if either path is None
|
||||||
"""
|
"""
|
||||||
|
src = normalize_fs_path(src)
|
||||||
|
dest = normalize_fs_path(dest)
|
||||||
|
|
||||||
if not isinstance(src, pathlib.Path):
|
if not isinstance(src, pathlib.Path):
|
||||||
src = pathlib.Path(src)
|
src = pathlib.Path(src)
|
||||||
|
|
||||||
@ -288,7 +311,7 @@ class FileUtilShUtil(FileUtilMacOS):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
class FileUtil(FileUtilMacOS):
|
class FileUtil(FileUtilShUtil):
|
||||||
"""Various file utilities"""
|
"""Various file utilities"""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|||||||
@ -5,16 +5,20 @@
|
|||||||
# reference: https://stackoverflow.com/questions/59330149/coreimage-ciimage-write-jpg-is-shifting-colors-macos/59334308#59334308
|
# reference: https://stackoverflow.com/questions/59330149/coreimage-ciimage-write-jpg-is-shifting-colors-macos/59334308#59334308
|
||||||
|
|
||||||
import pathlib
|
import pathlib
|
||||||
|
import sys
|
||||||
import objc
|
|
||||||
import Metal
|
|
||||||
import Quartz
|
|
||||||
from Cocoa import NSURL
|
|
||||||
from Foundation import NSDictionary
|
|
||||||
|
|
||||||
# needed to capture system-level stderr
|
# needed to capture system-level stderr
|
||||||
from wurlitzer import pipes
|
from wurlitzer import pipes
|
||||||
|
|
||||||
|
from .utils import is_macos
|
||||||
|
|
||||||
|
if is_macos:
|
||||||
|
import objc
|
||||||
|
import Metal
|
||||||
|
import Quartz
|
||||||
|
from Cocoa import NSURL
|
||||||
|
from Foundation import NSDictionary
|
||||||
|
|
||||||
__all__ = ["ImageConversionError", "ImageConverter"]
|
__all__ = ["ImageConversionError", "ImageConverter"]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,20 @@
|
|||||||
""" utility functions for validating/sanitizing path components """
|
""" utility functions for validating/sanitizing path components
|
||||||
|
|
||||||
|
This module also performs Unicode normalization. For a quick summary, there are
|
||||||
|
multiple ways to write more complex characters in Unicode. This causes problems
|
||||||
|
when e.g. checking if a file already exists and you have multiple sources for
|
||||||
|
the same string with different encodings. This sadly happens in Photos, but
|
||||||
|
isn't a problem on macOS, since macOS does normalization behind-the-scenes (see
|
||||||
|
https://eclecticlight.co/2021/05/08/explainer-unicode-normalization-and-apfs/).
|
||||||
|
This causes problems on other platforms, so we normalize as part of filename
|
||||||
|
sanitization functions and rely on them being called every time a unique
|
||||||
|
filename is needed.
|
||||||
|
"""
|
||||||
|
|
||||||
import pathvalidate
|
import pathvalidate
|
||||||
|
|
||||||
|
from osxphotos.utils import normalize_unicode
|
||||||
|
|
||||||
from ._constants import MAX_DIRNAME_LEN, MAX_FILENAME_LEN
|
from ._constants import MAX_DIRNAME_LEN, MAX_FILENAME_LEN
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
@ -24,7 +37,7 @@ def is_valid_filepath(filepath):
|
|||||||
|
|
||||||
|
|
||||||
def sanitize_filename(filename, replacement=":"):
|
def sanitize_filename(filename, replacement=":"):
|
||||||
"""replace any illegal characters in a filename and truncate filename if needed
|
"""replace any illegal characters in a filename, truncate filename if needed and normalize Unicode to NFC form
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
filename: str, filename to sanitze
|
filename: str, filename to sanitze
|
||||||
@ -35,6 +48,7 @@ def sanitize_filename(filename, replacement=":"):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
if filename:
|
if filename:
|
||||||
|
filename = normalize_unicode(filename)
|
||||||
filename = filename.replace("/", replacement)
|
filename = filename.replace("/", replacement)
|
||||||
if len(filename) > MAX_FILENAME_LEN:
|
if len(filename) > MAX_FILENAME_LEN:
|
||||||
parts = filename.split(".")
|
parts = filename.split(".")
|
||||||
@ -54,7 +68,7 @@ def sanitize_filename(filename, replacement=":"):
|
|||||||
|
|
||||||
|
|
||||||
def sanitize_dirname(dirname, replacement=":"):
|
def sanitize_dirname(dirname, replacement=":"):
|
||||||
"""replace any illegal characters in a directory name and truncate directory name if needed
|
"""replace any illegal characters in a directory name, truncate directory name if needed, and normalize Unicode to NFC form
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
dirname: str, directory name to sanitize
|
dirname: str, directory name to sanitize
|
||||||
@ -69,7 +83,7 @@ def sanitize_dirname(dirname, replacement=":"):
|
|||||||
|
|
||||||
|
|
||||||
def sanitize_pathpart(pathpart, replacement=":"):
|
def sanitize_pathpart(pathpart, replacement=":"):
|
||||||
"""replace any illegal characters in a path part (either directory or filename without extension) and truncate name if needed
|
"""replace any illegal characters in a path part (either directory or filename without extension), truncate name if needed, and normalize Unicode to NFC form
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
pathpart: str, path part to sanitize
|
pathpart: str, path part to sanitize
|
||||||
@ -82,6 +96,7 @@ def sanitize_pathpart(pathpart, replacement=":"):
|
|||||||
pathpart = (
|
pathpart = (
|
||||||
pathpart.replace("/", replacement) if replacement is not None else pathpart
|
pathpart.replace("/", replacement) if replacement is not None else pathpart
|
||||||
)
|
)
|
||||||
|
pathpart = normalize_unicode(pathpart)
|
||||||
if len(pathpart) > MAX_DIRNAME_LEN:
|
if len(pathpart) > MAX_DIRNAME_LEN:
|
||||||
drop = len(pathpart) - MAX_DIRNAME_LEN
|
drop = len(pathpart) - MAX_DIRNAME_LEN
|
||||||
pathpart = pathpart[:-drop]
|
pathpart = pathpart[:-drop]
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import logging
|
|||||||
import os
|
import os
|
||||||
import pathlib
|
import pathlib
|
||||||
import re
|
import re
|
||||||
|
import sys
|
||||||
import typing as t
|
import typing as t
|
||||||
from collections import namedtuple # pylint: disable=syntax-error
|
from collections import namedtuple # pylint: disable=syntax-error
|
||||||
from dataclasses import asdict, dataclass
|
from dataclasses import asdict, dataclass
|
||||||
@ -14,7 +15,6 @@ from datetime import datetime
|
|||||||
from enum import Enum
|
from enum import Enum
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
|
|
||||||
import photoscript
|
|
||||||
from mako.template import Template
|
from mako.template import Template
|
||||||
|
|
||||||
from ._constants import (
|
from ._constants import (
|
||||||
@ -35,26 +35,33 @@ from .datetime_utils import datetime_tz_to_utc
|
|||||||
from .exiftool import ExifTool, ExifToolCaching, exiftool_can_write, get_exiftool_path
|
from .exiftool import ExifTool, ExifToolCaching, exiftool_can_write, get_exiftool_path
|
||||||
from .export_db import ExportDB, ExportDBTemp
|
from .export_db import ExportDB, ExportDBTemp
|
||||||
from .fileutil import FileUtil
|
from .fileutil import FileUtil
|
||||||
from .photokit import (
|
|
||||||
PHOTOS_VERSION_CURRENT,
|
|
||||||
PHOTOS_VERSION_ORIGINAL,
|
|
||||||
PHOTOS_VERSION_UNADJUSTED,
|
|
||||||
PhotoKitFetchFailed,
|
|
||||||
PhotoLibrary,
|
|
||||||
)
|
|
||||||
from .phototemplate import RenderOptions
|
from .phototemplate import RenderOptions
|
||||||
from .rich_utils import add_rich_markup_tag
|
from .rich_utils import add_rich_markup_tag
|
||||||
from .uti import get_preferred_uti_extension
|
from .uti import get_preferred_uti_extension
|
||||||
from .utils import (
|
from .utils import (
|
||||||
|
is_macos,
|
||||||
hexdigest,
|
hexdigest,
|
||||||
increment_filename,
|
increment_filename,
|
||||||
increment_filename_with_count,
|
increment_filename_with_count,
|
||||||
lineno,
|
lineno,
|
||||||
list_directory,
|
list_directory,
|
||||||
lock_filename,
|
lock_filename,
|
||||||
|
normalize_fs_path,
|
||||||
unlock_filename,
|
unlock_filename,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if is_macos:
|
||||||
|
import photoscript
|
||||||
|
|
||||||
|
from .photokit import (
|
||||||
|
PHOTOS_VERSION_CURRENT,
|
||||||
|
PHOTOS_VERSION_ORIGINAL,
|
||||||
|
PHOTOS_VERSION_UNADJUSTED,
|
||||||
|
PhotoKitFetchFailed,
|
||||||
|
PhotoLibrary,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"ExportError",
|
"ExportError",
|
||||||
"ExportOptions",
|
"ExportOptions",
|
||||||
@ -721,7 +728,6 @@ class PhotoExporter:
|
|||||||
self, src: pathlib.Path, dest: pathlib.Path, options: ExportOptions
|
self, src: pathlib.Path, dest: pathlib.Path, options: ExportOptions
|
||||||
) -> t.Literal[True, False]:
|
) -> t.Literal[True, False]:
|
||||||
"""Return True if photo should be updated, else False"""
|
"""Return True if photo should be updated, else False"""
|
||||||
|
|
||||||
# NOTE: The order of certain checks is important
|
# NOTE: The order of certain checks is important
|
||||||
# read the comments below to understand why before changing
|
# read the comments below to understand why before changing
|
||||||
|
|
||||||
@ -1181,7 +1187,7 @@ class PhotoExporter:
|
|||||||
try:
|
try:
|
||||||
fileutil.copy(src, dest_str)
|
fileutil.copy(src, dest_str)
|
||||||
verbose(
|
verbose(
|
||||||
f"Exported {self._filename(self.photo.original_filename)} to {self._filepath(dest_str)}"
|
f"Exported {self._filename(self.photo.original_filename)} to {self._filepath(normalize_fs_path(dest_str))}"
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise ExportError(
|
raise ExportError(
|
||||||
|
|||||||
@ -20,7 +20,6 @@ from types import SimpleNamespace
|
|||||||
from typing import Any, Dict, Optional
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
from osxmetadata import OSXMetaData
|
|
||||||
|
|
||||||
import osxphotos
|
import osxphotos
|
||||||
|
|
||||||
@ -65,9 +64,12 @@ from .placeinfo import PlaceInfo4, PlaceInfo5
|
|||||||
from .query_builder import get_query
|
from .query_builder import get_query
|
||||||
from .scoreinfo import ScoreInfo
|
from .scoreinfo import ScoreInfo
|
||||||
from .searchinfo import SearchInfo
|
from .searchinfo import SearchInfo
|
||||||
from .text_detection import detect_text
|
|
||||||
from .uti import get_preferred_uti_extension, get_uti_for_extension
|
from .uti import get_preferred_uti_extension, get_uti_for_extension
|
||||||
from .utils import _get_resource_loc, hexdigest, list_directory
|
from .utils import _get_resource_loc, assert_macos, is_macos, hexdigest, list_directory
|
||||||
|
|
||||||
|
if is_macos:
|
||||||
|
from osxmetadata import OSXMetaData
|
||||||
|
from .text_detection import detect_text
|
||||||
|
|
||||||
__all__ = ["PhotoInfo", "PhotoInfoNone", "frozen_photoinfo_factory"]
|
__all__ = ["PhotoInfo", "PhotoInfoNone", "frozen_photoinfo_factory"]
|
||||||
|
|
||||||
@ -1461,6 +1463,8 @@ class PhotoInfo:
|
|||||||
|
|
||||||
def _detected_text(self):
|
def _detected_text(self):
|
||||||
"""detect text in photo, either from cached extended attribute or by attempting text detection"""
|
"""detect text in photo, either from cached extended attribute or by attempting text detection"""
|
||||||
|
assert_macos()
|
||||||
|
|
||||||
path = (
|
path = (
|
||||||
self.path_edited if self.hasadjustments and self.path_edited else self.path
|
self.path_edited if self.hasadjustments and self.path_edited else self.path
|
||||||
)
|
)
|
||||||
|
|||||||
@ -19,9 +19,12 @@
|
|||||||
|
|
||||||
import copy
|
import copy
|
||||||
import pathlib
|
import pathlib
|
||||||
|
import sys
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
assert(sys.platform == "darwin")
|
||||||
|
|
||||||
import AVFoundation
|
import AVFoundation
|
||||||
import CoreServices
|
import CoreServices
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|||||||
@ -2,12 +2,15 @@
|
|||||||
|
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
|
|
||||||
import photoscript
|
|
||||||
from more_itertools import chunked
|
from more_itertools import chunked
|
||||||
from photoscript import Album, Folder, Photo, PhotosLibrary
|
|
||||||
|
|
||||||
from .photoinfo import PhotoInfo
|
from .photoinfo import PhotoInfo
|
||||||
from .utils import noop, pluralize
|
from .utils import assert_macos, noop, pluralize
|
||||||
|
|
||||||
|
assert_macos()
|
||||||
|
|
||||||
|
import photoscript
|
||||||
|
from photoscript import Album, Folder, Photo, PhotosLibrary
|
||||||
|
|
||||||
__all__ = ["PhotosAlbum", "PhotosAlbumPhotoScript"]
|
__all__ = ["PhotosAlbum", "PhotosAlbumPhotoScript"]
|
||||||
|
|
||||||
|
|||||||
@ -4,6 +4,10 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import sqlite3
|
import sqlite3
|
||||||
|
|
||||||
|
from .utils import assert_macos
|
||||||
|
|
||||||
|
assert_macos()
|
||||||
|
|
||||||
import photoscript
|
import photoscript
|
||||||
|
|
||||||
from ._constants import _DB_TABLE_NAMES, _PHOTOS_5_ALBUM_KIND, _PHOTOS_5_FOLDER_KIND
|
from ._constants import _DB_TABLE_NAMES, _PHOTOS_5_ALBUM_KIND, _PHOTOS_5_FOLDER_KIND
|
||||||
|
|||||||
@ -21,7 +21,6 @@ from typing import Any, List, Optional
|
|||||||
from unicodedata import normalize
|
from unicodedata import normalize
|
||||||
|
|
||||||
import bitmath
|
import bitmath
|
||||||
import photoscript
|
|
||||||
from rich import print
|
from rich import print
|
||||||
|
|
||||||
from .._constants import (
|
from .._constants import (
|
||||||
@ -62,6 +61,7 @@ from ..rich_utils import add_rich_markup_tag
|
|||||||
from ..sqlite_utils import sqlite_db_is_locked, sqlite_open_ro
|
from ..sqlite_utils import sqlite_db_is_locked, sqlite_open_ro
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
_check_file_exists,
|
_check_file_exists,
|
||||||
|
is_macos,
|
||||||
get_macos_version,
|
get_macos_version,
|
||||||
get_last_library_path,
|
get_last_library_path,
|
||||||
noop,
|
noop,
|
||||||
@ -69,6 +69,9 @@ from ..utils import (
|
|||||||
)
|
)
|
||||||
from .photosdb_utils import get_db_model_version, get_db_version
|
from .photosdb_utils import get_db_model_version, get_db_version
|
||||||
|
|
||||||
|
if is_macos:
|
||||||
|
import photoscript
|
||||||
|
|
||||||
logger = logging.getLogger("osxphotos")
|
logger = logging.getLogger("osxphotos")
|
||||||
|
|
||||||
__all__ = ["PhotosDB"]
|
__all__ = ["PhotosDB"]
|
||||||
@ -118,8 +121,8 @@ class PhotosDB:
|
|||||||
|
|
||||||
# Check OS version
|
# Check OS version
|
||||||
system = platform.system()
|
system = platform.system()
|
||||||
(ver, major, _) = get_macos_version()
|
(ver, major, _) = get_macos_version() if is_macos else (None, None, None)
|
||||||
if system != "Darwin" or ((ver, major) not in _TESTED_OS_VERSIONS):
|
if system == "Darwin" and ((ver, major) not in _TESTED_OS_VERSIONS):
|
||||||
logging.warning(
|
logging.warning(
|
||||||
f"WARNING: This module has only been tested with macOS versions "
|
f"WARNING: This module has only been tested with macOS versions "
|
||||||
f"[{', '.join(f'{v}.{m}' for (v, m) in _TESTED_OS_VERSIONS)}]: "
|
f"[{', '.join(f'{v}.{m}' for (v, m) in _TESTED_OS_VERSIONS)}]: "
|
||||||
|
|||||||
@ -21,7 +21,6 @@ from ._version import __version__
|
|||||||
from .datetime_formatter import DateTimeFormatter
|
from .datetime_formatter import DateTimeFormatter
|
||||||
from .exiftool import ExifToolCaching
|
from .exiftool import ExifToolCaching
|
||||||
from .path_utils import sanitize_dirname, sanitize_filename, sanitize_pathpart
|
from .path_utils import sanitize_dirname, sanitize_filename, sanitize_pathpart
|
||||||
from .text_detection import detect_text
|
|
||||||
from .utils import expand_and_validate_filepath, load_function, uuid_to_shortuuid
|
from .utils import expand_and_validate_filepath, load_function, uuid_to_shortuuid
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
|||||||
@ -1,8 +1,13 @@
|
|||||||
""" Use Apple's Vision Framework via PyObjC to perform text detection on images (macOS 10.15+ only) """
|
""" Use Apple's Vision Framework via PyObjC to perform text detection on images (macOS 10.15+ only) """
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
import sys
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
|
|
||||||
|
from .utils import assert_macos, get_macos_version
|
||||||
|
|
||||||
|
assert_macos()
|
||||||
|
|
||||||
import objc
|
import objc
|
||||||
import Quartz
|
import Quartz
|
||||||
from Cocoa import NSURL
|
from Cocoa import NSURL
|
||||||
@ -11,8 +16,6 @@ from Foundation import NSDictionary
|
|||||||
# needed to capture system-level stderr
|
# needed to capture system-level stderr
|
||||||
from wurlitzer import pipes
|
from wurlitzer import pipes
|
||||||
|
|
||||||
from .utils import get_macos_version
|
|
||||||
|
|
||||||
__all__ = ["detect_text", "make_request_handler"]
|
__all__ = ["detect_text", "make_request_handler"]
|
||||||
|
|
||||||
ver, major, minor = get_macos_version()
|
ver, major, minor = get_macos_version()
|
||||||
|
|||||||
@ -2,13 +2,7 @@
|
|||||||
|
|
||||||
from typing import Union
|
from typing import Union
|
||||||
|
|
||||||
import Foundation
|
from .utils import is_macos
|
||||||
import objc
|
|
||||||
|
|
||||||
|
|
||||||
def known_timezone_names():
|
|
||||||
"""Get list of valid timezones on macOS"""
|
|
||||||
return Foundation.NSTimeZone.knownTimeZoneNames()
|
|
||||||
|
|
||||||
|
|
||||||
def format_offset_time(offset: int) -> str:
|
def format_offset_time(offset: int) -> str:
|
||||||
@ -19,43 +13,107 @@ def format_offset_time(offset: int) -> str:
|
|||||||
return f"{sign}{hours:02d}:{minutes:02d}"
|
return f"{sign}{hours:02d}:{minutes:02d}"
|
||||||
|
|
||||||
|
|
||||||
class Timezone:
|
if is_macos:
|
||||||
"""Create Timezone object from either name (str) or offset from GMT (int)"""
|
import Foundation
|
||||||
|
import objc
|
||||||
|
|
||||||
def __init__(self, tz: Union[str, int]):
|
|
||||||
with objc.autorelease_pool():
|
def known_timezone_names():
|
||||||
|
"""Get list of valid timezones on macOS"""
|
||||||
|
return Foundation.NSTimeZone.knownTimeZoneNames()
|
||||||
|
|
||||||
|
|
||||||
|
class Timezone:
|
||||||
|
"""Create Timezone object from either name (str) or offset from GMT (int)"""
|
||||||
|
|
||||||
|
def __init__(self, tz: Union[str, int]):
|
||||||
|
with objc.autorelease_pool():
|
||||||
|
if isinstance(tz, str):
|
||||||
|
self.timezone = Foundation.NSTimeZone.timeZoneWithName_(tz)
|
||||||
|
self._name = tz
|
||||||
|
elif isinstance(tz, int):
|
||||||
|
self.timezone = Foundation.NSTimeZone.timeZoneForSecondsFromGMT_(tz)
|
||||||
|
self._name = self.timezone.name()
|
||||||
|
else:
|
||||||
|
raise TypeError("Timezone must be a string or an int")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self) -> str:
|
||||||
|
return self._name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def offset(self) -> int:
|
||||||
|
return self.timezone.secondsFromGMT()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def offset_str(self) -> str:
|
||||||
|
return format_offset_time(self.offset)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def abbreviation(self) -> str:
|
||||||
|
return self.timezone.abbreviation()
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
if isinstance(other, Timezone):
|
||||||
|
return self.timezone == other.timezone
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
import zoneinfo
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
def known_timezone_names():
|
||||||
|
"""Get list of valid timezones"""
|
||||||
|
return zoneinfo.available_timezones()
|
||||||
|
|
||||||
|
|
||||||
|
class Timezone:
|
||||||
|
"""Create Timezone object from either name (str) or offset from GMT (int)"""
|
||||||
|
|
||||||
|
def __init__(self, tz: Union[str, int]):
|
||||||
if isinstance(tz, str):
|
if isinstance(tz, str):
|
||||||
self.timezone = Foundation.NSTimeZone.timeZoneWithName_(tz)
|
self.timezone = zoneinfo.ZoneInfo(tz)
|
||||||
self._name = tz
|
self._name = tz
|
||||||
elif isinstance(tz, int):
|
elif isinstance(tz, int):
|
||||||
self.timezone = Foundation.NSTimeZone.timeZoneForSecondsFromGMT_(tz)
|
if tz > 0:
|
||||||
self._name = self.timezone.name()
|
name = f"Etc/GMT+{tz // 3600}"
|
||||||
|
else:
|
||||||
|
name = f"Etc/GMT-{-tz // 3600}"
|
||||||
|
self.timezone = zoneinfo.ZoneInfo(name)
|
||||||
|
self._name = self.timezone.key
|
||||||
else:
|
else:
|
||||||
raise TypeError("Timezone must be a string or an int")
|
raise TypeError("Timezone must be a string or an int")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self) -> str:
|
def name(self) -> str:
|
||||||
return self._name
|
return self._name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def offset(self) -> int:
|
def offset(self) -> int:
|
||||||
return self.timezone.secondsFromGMT()
|
td = self.timezone.utcoffset(datetime.now())
|
||||||
|
assert td
|
||||||
|
return int(td.total_seconds())
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def offset_str(self) -> str:
|
def offset_str(self) -> str:
|
||||||
return format_offset_time(self.offset)
|
return format_offset_time(self.offset)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def abbreviation(self) -> str:
|
def abbreviation(self) -> str:
|
||||||
return self.timezone.abbreviation()
|
return self.timezone.key
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
if isinstance(other, Timezone):
|
if isinstance(other, Timezone):
|
||||||
return self.timezone == other.timezone
|
return self.timezone == other.timezone
|
||||||
return False
|
return False
|
||||||
|
|||||||
@ -1,7 +1,5 @@
|
|||||||
""" get UTI for a given file extension and the preferred extension for a given UTI
|
""" get UTI for a given file extension and the preferred extension for a given UTI
|
||||||
|
|
||||||
Implementation note: runs only on macOS
|
|
||||||
|
|
||||||
On macOS <= 11 (Big Sur), uses objective C CoreServices methods
|
On macOS <= 11 (Big Sur), uses objective C CoreServices methods
|
||||||
UTTypeCopyPreferredTagWithClass and UTTypeCreatePreferredIdentifierForTag to retrieve the
|
UTTypeCopyPreferredTagWithClass and UTTypeCreatePreferredIdentifierForTag to retrieve the
|
||||||
UTI and the extension. These are deprecated in 10.15 (Catalina) and no longer supported on Monterey.
|
UTI and the extension. These are deprecated in 10.15 (Catalina) and no longer supported on Monterey.
|
||||||
@ -13,6 +11,8 @@ Implementation note: runs only on macOS
|
|||||||
works for the extension -> UTI lookup. On Monterey, if there is no cached value for UTI -> extension lookup,
|
works for the extension -> UTI lookup. On Monterey, if there is no cached value for UTI -> extension lookup,
|
||||||
returns None.
|
returns None.
|
||||||
|
|
||||||
|
Outside of macOS uses only the hardcoded list of UTIs.
|
||||||
|
|
||||||
It's a bit hacky but best I can think of to make this robust on different versions of macOS. PRs welcome.
|
It's a bit hacky but best I can think of to make this robust on different versions of macOS. PRs welcome.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@ -21,12 +21,14 @@ from __future__ import annotations
|
|||||||
import csv
|
import csv
|
||||||
import re
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
import CoreServices
|
from .utils import assert_macos, is_macos, get_macos_version
|
||||||
import objc
|
|
||||||
|
|
||||||
from .utils import get_macos_version
|
if is_macos:
|
||||||
|
import CoreServices
|
||||||
|
import objc
|
||||||
|
|
||||||
__all__ = ["get_preferred_uti_extension", "get_uti_for_extension"]
|
__all__ = ["get_preferred_uti_extension", "get_uti_for_extension"]
|
||||||
|
|
||||||
@ -518,11 +520,10 @@ def _load_uti_dict():
|
|||||||
EXT_UTI_DICT[row["extension"]] = row["UTI"]
|
EXT_UTI_DICT[row["extension"]] = row["UTI"]
|
||||||
UTI_EXT_DICT[row["UTI"]] = row["preferred_extension"]
|
UTI_EXT_DICT[row["UTI"]] = row["preferred_extension"]
|
||||||
|
|
||||||
|
|
||||||
_load_uti_dict()
|
_load_uti_dict()
|
||||||
|
|
||||||
# OS version for determining which methods can be used
|
# OS version for determining which methods can be used
|
||||||
OS_VER, OS_MAJOR, _ = (int(x) for x in get_macos_version())
|
OS_VER, OS_MAJOR, _ = (int(x) for x in get_macos_version()) if is_macos else (None, None, None)
|
||||||
|
|
||||||
|
|
||||||
def _get_uti_from_mdls(extension):
|
def _get_uti_from_mdls(extension):
|
||||||
@ -532,6 +533,9 @@ def _get_uti_from_mdls(extension):
|
|||||||
# mdls -name kMDItemContentType foo.3fr
|
# mdls -name kMDItemContentType foo.3fr
|
||||||
# kMDItemContentType = "com.hasselblad.3fr-raw-image"
|
# kMDItemContentType = "com.hasselblad.3fr-raw-image"
|
||||||
|
|
||||||
|
if not is_macos:
|
||||||
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with tempfile.NamedTemporaryFile(suffix="." + extension) as temp:
|
with tempfile.NamedTemporaryFile(suffix="." + extension) as temp:
|
||||||
output = subprocess.check_output(
|
output = subprocess.check_output(
|
||||||
@ -573,7 +577,7 @@ def get_preferred_uti_extension(uti: str) -> str | None:
|
|||||||
uti: UTI str, e.g. 'public.jpeg'
|
uti: UTI str, e.g. 'public.jpeg'
|
||||||
returns: preferred extension as str or None if cannot be determined"""
|
returns: preferred extension as str or None if cannot be determined"""
|
||||||
|
|
||||||
if (OS_VER, OS_MAJOR) <= (10, 16):
|
if is_macos and (OS_VER, OS_MAJOR) <= (10, 16):
|
||||||
# reference: https://developer.apple.com/documentation/coreservices/1442744-uttypecopypreferredtagwithclass?language=objc
|
# reference: https://developer.apple.com/documentation/coreservices/1442744-uttypecopypreferredtagwithclass?language=objc
|
||||||
# deprecated in Catalina+, likely won't work at all on macOS 12
|
# deprecated in Catalina+, likely won't work at all on macOS 12
|
||||||
with objc.autorelease_pool():
|
with objc.autorelease_pool():
|
||||||
@ -602,7 +606,7 @@ def get_uti_for_extension(extension):
|
|||||||
if extension[0] == ".":
|
if extension[0] == ".":
|
||||||
extension = extension[1:]
|
extension = extension[1:]
|
||||||
|
|
||||||
if (OS_VER, OS_MAJOR) <= (10, 16):
|
if is_macos and (OS_VER, OS_MAJOR) <= (10, 16):
|
||||||
# https://developer.apple.com/documentation/coreservices/1448939-uttypecreatepreferredidentifierf
|
# https://developer.apple.com/documentation/coreservices/1448939-uttypecreatepreferredidentifierf
|
||||||
with objc.autorelease_pool():
|
with objc.autorelease_pool():
|
||||||
uti = CoreServices.UTTypeCreatePreferredIdentifierForTag(
|
uti = CoreServices.UTTypeCreatePreferredIdentifierForTag(
|
||||||
|
|||||||
@ -16,10 +16,9 @@ import sys
|
|||||||
import unicodedata
|
import unicodedata
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
from plistlib import load as plistload
|
from plistlib import load as plistload
|
||||||
from typing import Callable, List, Optional, Tuple, Union
|
from typing import Any, Callable, List, Optional, Tuple, TypeVar, Union
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
import CoreFoundation
|
|
||||||
import requests
|
import requests
|
||||||
import shortuuid
|
import shortuuid
|
||||||
|
|
||||||
@ -28,6 +27,8 @@ from ._constants import UNICODE_FORMAT
|
|||||||
logger = logging.getLogger("osxphotos")
|
logger = logging.getLogger("osxphotos")
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
"is_macos",
|
||||||
|
"assert_macos",
|
||||||
"dd_to_dms_str",
|
"dd_to_dms_str",
|
||||||
"expand_and_validate_filepath",
|
"expand_and_validate_filepath",
|
||||||
"get_last_library_path",
|
"get_last_library_path",
|
||||||
@ -53,6 +54,16 @@ __all__ = [
|
|||||||
VERSION_INFO_URL = "https://pypi.org/pypi/osxphotos/json"
|
VERSION_INFO_URL = "https://pypi.org/pypi/osxphotos/json"
|
||||||
|
|
||||||
|
|
||||||
|
is_macos = sys.platform == "darwin"
|
||||||
|
|
||||||
|
def assert_macos():
|
||||||
|
assert is_macos, "This feature only runs on macOS"
|
||||||
|
|
||||||
|
|
||||||
|
if is_macos:
|
||||||
|
import CoreFoundation
|
||||||
|
|
||||||
|
|
||||||
def noop(*args, **kwargs):
|
def noop(*args, **kwargs):
|
||||||
"""do nothing (no operation)"""
|
"""do nothing (no operation)"""
|
||||||
pass
|
pass
|
||||||
@ -67,6 +78,7 @@ def lineno(filename):
|
|||||||
|
|
||||||
|
|
||||||
def get_macos_version():
|
def get_macos_version():
|
||||||
|
assert_macos()
|
||||||
# returns tuple of str containing OS version
|
# returns tuple of str containing OS version
|
||||||
# e.g. 10.13.6 = ("10", "13", "6")
|
# e.g. 10.13.6 = ("10", "13", "6")
|
||||||
version = platform.mac_ver()[0].split(".")
|
version = platform.mac_ver()[0].split(".")
|
||||||
@ -166,6 +178,8 @@ def get_system_library_path():
|
|||||||
"""return the path to the system Photos library as string"""
|
"""return the path to the system Photos library as string"""
|
||||||
""" only works on MacOS 10.15 """
|
""" only works on MacOS 10.15 """
|
||||||
""" on earlier versions, returns None """
|
""" on earlier versions, returns None """
|
||||||
|
if not is_macos:
|
||||||
|
return None
|
||||||
_, major, _ = get_macos_version()
|
_, major, _ = get_macos_version()
|
||||||
if int(major) < 15:
|
if int(major) < 15:
|
||||||
logger.debug(
|
logger.debug(
|
||||||
@ -241,6 +255,8 @@ def get_last_library_path():
|
|||||||
def list_photo_libraries():
|
def list_photo_libraries():
|
||||||
"""returns list of Photos libraries found on the system"""
|
"""returns list of Photos libraries found on the system"""
|
||||||
""" on MacOS < 10.15, this may omit some libraries """
|
""" on MacOS < 10.15, this may omit some libraries """
|
||||||
|
if not is_macos:
|
||||||
|
return []
|
||||||
|
|
||||||
# On 10.15, mdfind appears to find all libraries
|
# On 10.15, mdfind appears to find all libraries
|
||||||
# On older MacOS versions, mdfind appears to ignore some libraries
|
# On older MacOS versions, mdfind appears to ignore some libraries
|
||||||
@ -263,11 +279,14 @@ def list_photo_libraries():
|
|||||||
return lib_list
|
return lib_list
|
||||||
|
|
||||||
|
|
||||||
def normalize_fs_path(path: str) -> str:
|
T = TypeVar("T", bound=Union[str, pathlib.Path])
|
||||||
|
def normalize_fs_path(path: T) -> T:
|
||||||
"""Normalize filesystem paths with unicode in them"""
|
"""Normalize filesystem paths with unicode in them"""
|
||||||
# macOS HFS+ uses NFD, APFS doesn't normalize but stick with NFD
|
form = "NFD" if is_macos else "NFC"
|
||||||
# ref: https://eclecticlight.co/2021/05/08/explainer-unicode-normalization-and-apfs/
|
if isinstance(path, pathlib.Path):
|
||||||
return unicodedata.normalize("NFD", path)
|
return pathlib.Path(unicodedata.normalize(form, str(path)))
|
||||||
|
else:
|
||||||
|
return unicodedata.normalize(form, path)
|
||||||
|
|
||||||
|
|
||||||
# def findfiles(pattern, path):
|
# def findfiles(pattern, path):
|
||||||
@ -356,7 +375,7 @@ def list_directory(
|
|||||||
return files
|
return files
|
||||||
|
|
||||||
|
|
||||||
def normalize_unicode(value):
|
def normalize_unicode(value) -> Any:
|
||||||
"""normalize unicode data"""
|
"""normalize unicode data"""
|
||||||
if value is None:
|
if value is None:
|
||||||
return None
|
return None
|
||||||
|
|||||||
@ -5,14 +5,19 @@ import shutil
|
|||||||
import tempfile
|
import tempfile
|
||||||
import time
|
import time
|
||||||
|
|
||||||
import photoscript
|
|
||||||
import pytest
|
import pytest
|
||||||
from applescript import AppleScript
|
|
||||||
from photoscript.utils import ditto
|
from osxphotos.utils import is_macos
|
||||||
|
|
||||||
|
if is_macos:
|
||||||
|
import photoscript
|
||||||
|
from applescript import AppleScript
|
||||||
|
from photoscript.utils import ditto
|
||||||
|
|
||||||
|
from .test_catalina_10_15_7 import UUID_DICT_LOCAL
|
||||||
|
|
||||||
from osxphotos.exiftool import _ExifToolProc
|
from osxphotos.exiftool import _ExifToolProc
|
||||||
|
|
||||||
from .test_catalina_10_15_7 import UUID_DICT_LOCAL
|
|
||||||
|
|
||||||
# run timewarp tests (configured with --timewarp)
|
# run timewarp tests (configured with --timewarp)
|
||||||
TEST_TIMEWARP = False
|
TEST_TIMEWARP = False
|
||||||
@ -34,6 +39,9 @@ NO_CLEANUP = False
|
|||||||
|
|
||||||
|
|
||||||
def get_os_version():
|
def get_os_version():
|
||||||
|
if not is_macos:
|
||||||
|
return (None, None, None)
|
||||||
|
|
||||||
import platform
|
import platform
|
||||||
|
|
||||||
# returns tuple containing OS version
|
# returns tuple containing OS version
|
||||||
@ -53,7 +61,7 @@ def get_os_version():
|
|||||||
return (ver, major, minor)
|
return (ver, major, minor)
|
||||||
|
|
||||||
|
|
||||||
OS_VER = get_os_version()
|
OS_VER = get_os_version() if is_macos else [None, None]
|
||||||
if OS_VER[0] == "10" and OS_VER[1] == "15":
|
if OS_VER[0] == "10" and OS_VER[1] == "15":
|
||||||
# Catalina
|
# Catalina
|
||||||
TEST_LIBRARY = "tests/Test-10.15.7.photoslibrary"
|
TEST_LIBRARY = "tests/Test-10.15.7.photoslibrary"
|
||||||
@ -77,28 +85,28 @@ else:
|
|||||||
TEST_LIBRARY_ADD_LOCATIONS = None
|
TEST_LIBRARY_ADD_LOCATIONS = None
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session", autouse=True)
|
@pytest.fixture(scope="session", autouse=is_macos)
|
||||||
def setup_photos_timewarp():
|
def setup_photos_timewarp():
|
||||||
if not TEST_TIMEWARP:
|
if not TEST_TIMEWARP:
|
||||||
return
|
return
|
||||||
copy_photos_library(TEST_LIBRARY_TIMEWARP, delay=5)
|
copy_photos_library(TEST_LIBRARY_TIMEWARP, delay=5)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session", autouse=True)
|
@pytest.fixture(scope="session", autouse=is_macos)
|
||||||
def setup_photos_import():
|
def setup_photos_import():
|
||||||
if not TEST_IMPORT:
|
if not TEST_IMPORT:
|
||||||
return
|
return
|
||||||
copy_photos_library(TEST_LIBRARY_IMPORT, delay=10)
|
copy_photos_library(TEST_LIBRARY_IMPORT, delay=10)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session", autouse=True)
|
@pytest.fixture(scope="session", autouse=is_macos)
|
||||||
def setup_photos_sync():
|
def setup_photos_sync():
|
||||||
if not TEST_SYNC:
|
if not TEST_SYNC:
|
||||||
return
|
return
|
||||||
copy_photos_library(TEST_LIBRARY_SYNC, delay=10)
|
copy_photos_library(TEST_LIBRARY_SYNC, delay=10)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session", autouse=True)
|
@pytest.fixture(scope="session", autouse=is_macos)
|
||||||
def setup_photos_add_locations():
|
def setup_photos_add_locations():
|
||||||
if not TEST_ADD_LOCATIONS:
|
if not TEST_ADD_LOCATIONS:
|
||||||
return
|
return
|
||||||
@ -312,7 +320,10 @@ def addalbum_library():
|
|||||||
|
|
||||||
def copy_photos_library_to_path(photos_library_path: str, dest_path: str) -> str:
|
def copy_photos_library_to_path(photos_library_path: str, dest_path: str) -> str:
|
||||||
"""Copy a photos library to a folder"""
|
"""Copy a photos library to a folder"""
|
||||||
ditto(photos_library_path, dest_path)
|
if is_macos:
|
||||||
|
ditto(photos_library_path, dest_path)
|
||||||
|
else:
|
||||||
|
shutil.copytree(photos_library_path, dest_path)
|
||||||
return dest_path
|
return dest_path
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
16
tests/locale_util.py
Normal file
16
tests/locale_util.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
""" Helpers for running locale-dependent tests """
|
||||||
|
|
||||||
|
import contextlib
|
||||||
|
import locale
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
def setlocale(typ, name):
|
||||||
|
try:
|
||||||
|
with contextlib.suppress(Exception):
|
||||||
|
locale.setlocale(typ, name)
|
||||||
|
# On Linux UTF-8 locales are separate
|
||||||
|
locale.setlocale(typ, f"{name}.UTF-8")
|
||||||
|
except locale.Error:
|
||||||
|
pytest.skip(f"Locale {name} not available")
|
||||||
@ -15,9 +15,9 @@ import pytest
|
|||||||
import osxphotos
|
import osxphotos
|
||||||
from osxphotos._constants import _UNKNOWN_PERSON
|
from osxphotos._constants import _UNKNOWN_PERSON
|
||||||
from osxphotos.photoexporter import PhotoExporter
|
from osxphotos.photoexporter import PhotoExporter
|
||||||
from osxphotos.utils import get_macos_version
|
from osxphotos.utils import is_macos, get_macos_version
|
||||||
|
|
||||||
OS_VERSION = get_macos_version()
|
OS_VERSION = get_macos_version() if is_macos else (None, None, None)
|
||||||
SKIP_TEST = "OSXPHOTOS_TEST_EXPORT" not in os.environ or OS_VERSION[1] != "15"
|
SKIP_TEST = "OSXPHOTOS_TEST_EXPORT" not in os.environ or OS_VERSION[1] != "15"
|
||||||
PHOTOS_DB_LOCAL = os.path.expanduser("~/Pictures/Photos Library.photoslibrary")
|
PHOTOS_DB_LOCAL = os.path.expanduser("~/Pictures/Photos Library.photoslibrary")
|
||||||
|
|
||||||
@ -1448,6 +1448,7 @@ def test_multi_uuid(photosdb):
|
|||||||
assert len(photos) == 2
|
assert len(photos) == 2
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(not is_macos, reason="Only works on macOS")
|
||||||
def test_detected_text(photosdb):
|
def test_detected_text(photosdb):
|
||||||
"""test PhotoInfo.detected_text"""
|
"""test PhotoInfo.detected_text"""
|
||||||
for uuid, expected_text in UUID_DETECTED_TEXT.items():
|
for uuid, expected_text in UUID_DETECTED_TEXT.items():
|
||||||
|
|||||||
@ -14,10 +14,10 @@ import subprocess
|
|||||||
import tempfile
|
import tempfile
|
||||||
import time
|
import time
|
||||||
from tempfile import TemporaryDirectory
|
from tempfile import TemporaryDirectory
|
||||||
|
from bitmath import contextlib
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from click.testing import CliRunner
|
from click.testing import CliRunner
|
||||||
from osxmetadata import OSXMetaData, Tag
|
|
||||||
|
|
||||||
import osxphotos
|
import osxphotos
|
||||||
from osxphotos._constants import OSXPHOTOS_EXPORT_DB
|
from osxphotos._constants import OSXPHOTOS_EXPORT_DB
|
||||||
@ -36,9 +36,16 @@ from osxphotos.cli import (
|
|||||||
)
|
)
|
||||||
from osxphotos.exiftool import ExifTool, get_exiftool_path
|
from osxphotos.exiftool import ExifTool, get_exiftool_path
|
||||||
from osxphotos.fileutil import FileUtil
|
from osxphotos.fileutil import FileUtil
|
||||||
from osxphotos.utils import noop, normalize_fs_path, normalize_unicode
|
from osxphotos.utils import is_macos, noop, normalize_fs_path, normalize_unicode
|
||||||
|
if is_macos:
|
||||||
|
from osxmetadata import OSXMetaData, Tag
|
||||||
|
|
||||||
from .conftest import copy_photos_library_to_path
|
from .conftest import copy_photos_library_to_path
|
||||||
|
from .locale_util import setlocale
|
||||||
|
|
||||||
|
def _normalize_fs_paths(paths):
|
||||||
|
"""Small helper to prepare path strings for test"""
|
||||||
|
return [normalize_fs_path(p) for p in paths]
|
||||||
|
|
||||||
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"
|
||||||
@ -66,7 +73,7 @@ SKIP_UUID_FILE = "tests/skip_uuid_from_file.txt"
|
|||||||
|
|
||||||
CLI_OUTPUT_QUERY_UUID = '[{"uuid": "D79B8D77-BFFC-460B-9312-034F2877D35B", "filename": "D79B8D77-BFFC-460B-9312-034F2877D35B.jpeg", "original_filename": "Pumkins2.jpg", "date": "2018-09-28T16:07:07-04:00", "description": "Girl holding pumpkin", "title": "I found one!", "keywords": ["Kids"], "albums": ["Pumpkin Farm", "Test Album", "Multi Keyword"], "persons": ["Katie"], "path": "/tests/Test-10.15.7.photoslibrary/originals/D/D79B8D77-BFFC-460B-9312-034F2877D35B.jpeg", "ismissing": false, "hasadjustments": false, "external_edit": false, "favorite": false, "hidden": false, "latitude": 41.256566, "longitude": -95.940257, "path_edited": null, "shared": false, "isphoto": true, "ismovie": false, "uti": "public.jpeg", "burst": false, "live_photo": false, "path_live_photo": null, "iscloudasset": false, "incloud": null}]'
|
CLI_OUTPUT_QUERY_UUID = '[{"uuid": "D79B8D77-BFFC-460B-9312-034F2877D35B", "filename": "D79B8D77-BFFC-460B-9312-034F2877D35B.jpeg", "original_filename": "Pumkins2.jpg", "date": "2018-09-28T16:07:07-04:00", "description": "Girl holding pumpkin", "title": "I found one!", "keywords": ["Kids"], "albums": ["Pumpkin Farm", "Test Album", "Multi Keyword"], "persons": ["Katie"], "path": "/tests/Test-10.15.7.photoslibrary/originals/D/D79B8D77-BFFC-460B-9312-034F2877D35B.jpeg", "ismissing": false, "hasadjustments": false, "external_edit": false, "favorite": false, "hidden": false, "latitude": 41.256566, "longitude": -95.940257, "path_edited": null, "shared": false, "isphoto": true, "ismovie": false, "uti": "public.jpeg", "burst": false, "live_photo": false, "path_live_photo": null, "iscloudasset": false, "incloud": null}]'
|
||||||
|
|
||||||
CLI_EXPORT_FILENAMES = [
|
CLI_EXPORT_FILENAMES = _normalize_fs_paths([
|
||||||
"[2020-08-29] AAF035 (1).jpg",
|
"[2020-08-29] AAF035 (1).jpg",
|
||||||
"[2020-08-29] AAF035 (2).jpg",
|
"[2020-08-29] AAF035 (2).jpg",
|
||||||
"[2020-08-29] AAF035 (3).jpg",
|
"[2020-08-29] AAF035 (3).jpg",
|
||||||
@ -100,10 +107,10 @@ CLI_EXPORT_FILENAMES = [
|
|||||||
"wedding.jpg",
|
"wedding.jpg",
|
||||||
"winebottle (1).jpeg",
|
"winebottle (1).jpeg",
|
||||||
"winebottle.jpeg",
|
"winebottle.jpeg",
|
||||||
]
|
])
|
||||||
|
|
||||||
|
|
||||||
CLI_EXPORT_FILENAMES_DRY_RUN = [
|
CLI_EXPORT_FILENAMES_DRY_RUN = _normalize_fs_paths([
|
||||||
"[2020-08-29] AAF035.jpg",
|
"[2020-08-29] AAF035.jpg",
|
||||||
"DSC03584.dng",
|
"DSC03584.dng",
|
||||||
"Frítest_edited.jpeg",
|
"Frítest_edited.jpeg",
|
||||||
@ -130,15 +137,25 @@ CLI_EXPORT_FILENAMES_DRY_RUN = [
|
|||||||
"wedding.jpg",
|
"wedding.jpg",
|
||||||
"winebottle.jpeg",
|
"winebottle.jpeg",
|
||||||
"winebottle.jpeg",
|
"winebottle.jpeg",
|
||||||
]
|
])
|
||||||
|
|
||||||
CLI_EXPORT_IGNORE_SIGNATURE_FILENAMES = ["Tulips.jpg", "wedding.jpg"]
|
CLI_EXPORT_IGNORE_SIGNATURE_FILENAMES = _normalize_fs_paths([
|
||||||
|
"Tulips.jpg",
|
||||||
|
"wedding.jpg"
|
||||||
|
])
|
||||||
|
|
||||||
CLI_EXPORT_FILENAMES_ALBUM = ["Pumkins1.jpg", "Pumkins2.jpg", "Pumpkins3.jpg"]
|
CLI_EXPORT_FILENAMES_ALBUM = _normalize_fs_paths([
|
||||||
|
"Pumkins1.jpg",
|
||||||
|
"Pumkins2.jpg",
|
||||||
|
"Pumpkins3.jpg"
|
||||||
|
])
|
||||||
|
|
||||||
CLI_EXPORT_FILENAMES_ALBUM_UNICODE = ["IMG_4547.jpg"]
|
CLI_EXPORT_FILENAMES_ALBUM_UNICODE = _normalize_fs_paths(["IMG_4547.jpg"])
|
||||||
|
|
||||||
CLI_EXPORT_FILENAMES_DELETED_TWIN = ["wedding.jpg", "wedding_edited.jpeg"]
|
CLI_EXPORT_FILENAMES_DELETED_TWIN = _normalize_fs_paths([
|
||||||
|
"wedding.jpg",
|
||||||
|
"wedding_edited.jpeg"
|
||||||
|
])
|
||||||
|
|
||||||
CLI_EXPORT_EDITED_SUFFIX = "_bearbeiten"
|
CLI_EXPORT_EDITED_SUFFIX = "_bearbeiten"
|
||||||
CLI_EXPORT_EDITED_SUFFIX_TEMPLATE = "{edited?_edited,}"
|
CLI_EXPORT_EDITED_SUFFIX_TEMPLATE = "{edited?_edited,}"
|
||||||
@ -146,7 +163,7 @@ CLI_EXPORT_ORIGINAL_SUFFIX = "_original"
|
|||||||
CLI_EXPORT_ORIGINAL_SUFFIX_TEMPLATE = "{edited?_original,}"
|
CLI_EXPORT_ORIGINAL_SUFFIX_TEMPLATE = "{edited?_original,}"
|
||||||
CLI_EXPORT_PREVIEW_SUFFIX = "_lowres"
|
CLI_EXPORT_PREVIEW_SUFFIX = "_lowres"
|
||||||
|
|
||||||
CLI_EXPORT_FILENAMES_EDITED_SUFFIX = [
|
CLI_EXPORT_FILENAMES_EDITED_SUFFIX = _normalize_fs_paths([
|
||||||
"[2020-08-29] AAF035 (1).jpg",
|
"[2020-08-29] AAF035 (1).jpg",
|
||||||
"[2020-08-29] AAF035 (2).jpg",
|
"[2020-08-29] AAF035 (2).jpg",
|
||||||
"[2020-08-29] AAF035 (3).jpg",
|
"[2020-08-29] AAF035 (3).jpg",
|
||||||
@ -180,9 +197,9 @@ CLI_EXPORT_FILENAMES_EDITED_SUFFIX = [
|
|||||||
"wedding.jpg",
|
"wedding.jpg",
|
||||||
"winebottle (1).jpeg",
|
"winebottle (1).jpeg",
|
||||||
"winebottle.jpeg",
|
"winebottle.jpeg",
|
||||||
]
|
])
|
||||||
|
|
||||||
CLI_EXPORT_FILENAMES_EDITED_SUFFIX_TEMPLATE = [
|
CLI_EXPORT_FILENAMES_EDITED_SUFFIX_TEMPLATE = _normalize_fs_paths([
|
||||||
"[2020-08-29] AAF035 (1).jpg",
|
"[2020-08-29] AAF035 (1).jpg",
|
||||||
"[2020-08-29] AAF035 (2).jpg",
|
"[2020-08-29] AAF035 (2).jpg",
|
||||||
"[2020-08-29] AAF035 (3).jpg",
|
"[2020-08-29] AAF035 (3).jpg",
|
||||||
@ -216,9 +233,9 @@ CLI_EXPORT_FILENAMES_EDITED_SUFFIX_TEMPLATE = [
|
|||||||
"wedding.jpg",
|
"wedding.jpg",
|
||||||
"winebottle (1).jpeg",
|
"winebottle (1).jpeg",
|
||||||
"winebottle.jpeg",
|
"winebottle.jpeg",
|
||||||
]
|
])
|
||||||
|
|
||||||
CLI_EXPORT_FILENAMES_ORIGINAL_SUFFIX = [
|
CLI_EXPORT_FILENAMES_ORIGINAL_SUFFIX = _normalize_fs_paths([
|
||||||
"[2020-08-29] AAF035_original (1).jpg",
|
"[2020-08-29] AAF035_original (1).jpg",
|
||||||
"[2020-08-29] AAF035_original (2).jpg",
|
"[2020-08-29] AAF035_original (2).jpg",
|
||||||
"[2020-08-29] AAF035_original (3).jpg",
|
"[2020-08-29] AAF035_original (3).jpg",
|
||||||
@ -252,9 +269,9 @@ CLI_EXPORT_FILENAMES_ORIGINAL_SUFFIX = [
|
|||||||
"wedding_original.jpg",
|
"wedding_original.jpg",
|
||||||
"winebottle_original (1).jpeg",
|
"winebottle_original (1).jpeg",
|
||||||
"winebottle_original.jpeg",
|
"winebottle_original.jpeg",
|
||||||
]
|
])
|
||||||
|
|
||||||
CLI_EXPORT_FILENAMES_ORIGINAL_SUFFIX_TEMPLATE = [
|
CLI_EXPORT_FILENAMES_ORIGINAL_SUFFIX_TEMPLATE = _normalize_fs_paths([
|
||||||
"[2020-08-29] AAF035 (1).jpg",
|
"[2020-08-29] AAF035 (1).jpg",
|
||||||
"[2020-08-29] AAF035 (2).jpg",
|
"[2020-08-29] AAF035 (2).jpg",
|
||||||
"[2020-08-29] AAF035 (3).jpg",
|
"[2020-08-29] AAF035 (3).jpg",
|
||||||
@ -288,7 +305,7 @@ CLI_EXPORT_FILENAMES_ORIGINAL_SUFFIX_TEMPLATE = [
|
|||||||
"wedding_original.jpg",
|
"wedding_original.jpg",
|
||||||
"winebottle (1).jpeg",
|
"winebottle (1).jpeg",
|
||||||
"winebottle.jpeg",
|
"winebottle.jpeg",
|
||||||
]
|
])
|
||||||
|
|
||||||
CLI_EXPORT_FILENAMES_CURRENT = [
|
CLI_EXPORT_FILENAMES_CURRENT = [
|
||||||
"1793FAAB-DE75-4E25-886C-2BD66C780D6A_edited.jpeg", # Frítest.jpg
|
"1793FAAB-DE75-4E25-886C-2BD66C780D6A_edited.jpeg", # Frítest.jpg
|
||||||
@ -326,7 +343,7 @@ CLI_EXPORT_FILENAMES_CURRENT = [
|
|||||||
"F207D5DE-EFAD-4217-8424-0764AAC971D0.jpeg",
|
"F207D5DE-EFAD-4217-8424-0764AAC971D0.jpeg",
|
||||||
]
|
]
|
||||||
|
|
||||||
CLI_EXPORT_FILENAMES_CONVERT_TO_JPEG = [
|
CLI_EXPORT_FILENAMES_CONVERT_TO_JPEG = _normalize_fs_paths([
|
||||||
"[2020-08-29] AAF035 (1).jpg",
|
"[2020-08-29] AAF035 (1).jpg",
|
||||||
"[2020-08-29] AAF035 (2).jpg",
|
"[2020-08-29] AAF035 (2).jpg",
|
||||||
"[2020-08-29] AAF035 (3).jpg",
|
"[2020-08-29] AAF035 (3).jpg",
|
||||||
@ -360,9 +377,9 @@ CLI_EXPORT_FILENAMES_CONVERT_TO_JPEG = [
|
|||||||
"wedding.jpg",
|
"wedding.jpg",
|
||||||
"winebottle (1).jpeg",
|
"winebottle (1).jpeg",
|
||||||
"winebottle.jpeg",
|
"winebottle.jpeg",
|
||||||
]
|
])
|
||||||
|
|
||||||
CLI_EXPORT_FILENAMES_CONVERT_TO_JPEG_SKIP_RAW = [
|
CLI_EXPORT_FILENAMES_CONVERT_TO_JPEG_SKIP_RAW = _normalize_fs_paths([
|
||||||
"[2020-08-29] AAF035 (1).jpg",
|
"[2020-08-29] AAF035 (1).jpg",
|
||||||
"[2020-08-29] AAF035 (2).jpg",
|
"[2020-08-29] AAF035 (2).jpg",
|
||||||
"[2020-08-29] AAF035 (3).jpg",
|
"[2020-08-29] AAF035 (3).jpg",
|
||||||
@ -394,26 +411,26 @@ CLI_EXPORT_FILENAMES_CONVERT_TO_JPEG_SKIP_RAW = [
|
|||||||
"wedding.jpg",
|
"wedding.jpg",
|
||||||
"winebottle (1).jpeg",
|
"winebottle (1).jpeg",
|
||||||
"winebottle.jpeg",
|
"winebottle.jpeg",
|
||||||
]
|
])
|
||||||
|
|
||||||
CLI_EXPORT_CONVERT_TO_JPEG_LARGE_FILE = "DSC03584.jpeg"
|
CLI_EXPORT_CONVERT_TO_JPEG_LARGE_FILE = "DSC03584.jpeg"
|
||||||
|
|
||||||
CLI_EXPORTED_DIRECTORY_TEMPLATE_FILENAMES1 = [
|
CLI_EXPORTED_DIRECTORY_TEMPLATE_FILENAMES1 = _normalize_fs_paths([
|
||||||
"2019/April/wedding.jpg",
|
"2019/April/wedding.jpg",
|
||||||
"2019/July/Tulips.jpg",
|
"2019/July/Tulips.jpg",
|
||||||
"2018/October/St James Park.jpg",
|
"2018/October/St James Park.jpg",
|
||||||
"2018/September/Pumpkins3.jpg",
|
"2018/September/Pumpkins3.jpg",
|
||||||
"2018/September/Pumkins2.jpg",
|
"2018/September/Pumkins2.jpg",
|
||||||
"2018/September/Pumkins1.jpg",
|
"2018/September/Pumkins1.jpg",
|
||||||
]
|
])
|
||||||
|
|
||||||
CLI_EXPORTED_DIRECTORY_TEMPLATE_FILENAMES_LOCALE = [
|
CLI_EXPORTED_DIRECTORY_TEMPLATE_FILENAMES_LOCALE = _normalize_fs_paths([
|
||||||
"2019/September/IMG_9975.JPEG",
|
"2019/September/IMG_9975.JPEG",
|
||||||
"2020/Februar/IMG_1064.JPEG",
|
"2020/Februar/IMG_1064.JPEG",
|
||||||
"2016/März/IMG_3984.JPEG",
|
"2016/März/IMG_3984.JPEG",
|
||||||
]
|
])
|
||||||
|
|
||||||
CLI_EXPORTED_DIRECTORY_TEMPLATE_FILENAMES_ALBUM1 = [
|
CLI_EXPORTED_DIRECTORY_TEMPLATE_FILENAMES_ALBUM1 = _normalize_fs_paths([
|
||||||
"Multi Keyword/wedding.jpg",
|
"Multi Keyword/wedding.jpg",
|
||||||
"_/Tulips.jpg",
|
"_/Tulips.jpg",
|
||||||
"_/St James Park.jpg",
|
"_/St James Park.jpg",
|
||||||
@ -421,9 +438,9 @@ CLI_EXPORTED_DIRECTORY_TEMPLATE_FILENAMES_ALBUM1 = [
|
|||||||
"Pumpkin Farm/Pumkins2.jpg",
|
"Pumpkin Farm/Pumkins2.jpg",
|
||||||
"Pumpkin Farm/Pumkins1.jpg",
|
"Pumpkin Farm/Pumkins1.jpg",
|
||||||
"Test Album/Pumkins1.jpg",
|
"Test Album/Pumkins1.jpg",
|
||||||
]
|
])
|
||||||
|
|
||||||
CLI_EXPORTED_DIRECTORY_TEMPLATE_FILENAMES_ALBUM2 = [
|
CLI_EXPORTED_DIRECTORY_TEMPLATE_FILENAMES_ALBUM2 = _normalize_fs_paths([
|
||||||
"Multi Keyword/wedding.jpg",
|
"Multi Keyword/wedding.jpg",
|
||||||
"NOALBUM/Tulips.jpg",
|
"NOALBUM/Tulips.jpg",
|
||||||
"NOALBUM/St James Park.jpg",
|
"NOALBUM/St James Park.jpg",
|
||||||
@ -431,28 +448,28 @@ CLI_EXPORTED_DIRECTORY_TEMPLATE_FILENAMES_ALBUM2 = [
|
|||||||
"Pumpkin Farm/Pumkins2.jpg",
|
"Pumpkin Farm/Pumkins2.jpg",
|
||||||
"Pumpkin Farm/Pumkins1.jpg",
|
"Pumpkin Farm/Pumkins1.jpg",
|
||||||
"Test Album/Pumkins1.jpg",
|
"Test Album/Pumkins1.jpg",
|
||||||
]
|
])
|
||||||
|
|
||||||
CLI_EXPORTED_DIRECTORY_TEMPLATE_FILENAMES2 = [
|
CLI_EXPORTED_DIRECTORY_TEMPLATE_FILENAMES2 = _normalize_fs_paths([
|
||||||
"St James's Park, Great Britain, Westminster, England, United Kingdom/St James Park.jpg",
|
"St James's Park, Great Britain, Westminster, England, United Kingdom/St James Park.jpg",
|
||||||
"_/Pumpkins3.jpg",
|
"_/Pumpkins3.jpg",
|
||||||
"Omaha, Nebraska, United States/Pumkins2.jpg",
|
"Omaha, Nebraska, United States/Pumkins2.jpg",
|
||||||
"_/Pumkins1.jpg",
|
"_/Pumkins1.jpg",
|
||||||
"_/Tulips.jpg",
|
"_/Tulips.jpg",
|
||||||
"_/wedding.jpg",
|
"_/wedding.jpg",
|
||||||
]
|
])
|
||||||
|
|
||||||
CLI_EXPORTED_DIRECTORY_TEMPLATE_FILENAMES3 = [
|
CLI_EXPORTED_DIRECTORY_TEMPLATE_FILENAMES3 = _normalize_fs_paths([
|
||||||
"2019/{foo}/wedding.jpg",
|
"2019/{foo}/wedding.jpg",
|
||||||
"2019/{foo}/Tulips.jpg",
|
"2019/{foo}/Tulips.jpg",
|
||||||
"2018/{foo}/St James Park.jpg",
|
"2018/{foo}/St James Park.jpg",
|
||||||
"2018/{foo}/Pumpkins3.jpg",
|
"2018/{foo}/Pumpkins3.jpg",
|
||||||
"2018/{foo}/Pumkins2.jpg",
|
"2018/{foo}/Pumkins2.jpg",
|
||||||
"2018/{foo}/Pumkins1.jpg",
|
"2018/{foo}/Pumkins1.jpg",
|
||||||
]
|
])
|
||||||
|
|
||||||
|
|
||||||
CLI_EXPORTED_FILENAME_TEMPLATE_FILENAMES1 = [
|
CLI_EXPORTED_FILENAME_TEMPLATE_FILENAMES1 = _normalize_fs_paths([
|
||||||
"2019-wedding.jpg",
|
"2019-wedding.jpg",
|
||||||
"2019-wedding_edited.jpeg",
|
"2019-wedding_edited.jpeg",
|
||||||
"2019-Tulips.jpg",
|
"2019-Tulips.jpg",
|
||||||
@ -461,9 +478,9 @@ CLI_EXPORTED_FILENAME_TEMPLATE_FILENAMES1 = [
|
|||||||
"2018-Pumpkins3.jpg",
|
"2018-Pumpkins3.jpg",
|
||||||
"2018-Pumkins2.jpg",
|
"2018-Pumkins2.jpg",
|
||||||
"2018-Pumkins1.jpg",
|
"2018-Pumkins1.jpg",
|
||||||
]
|
])
|
||||||
|
|
||||||
CLI_EXPORTED_FILENAME_TEMPLATE_FILENAMES2 = [
|
CLI_EXPORTED_FILENAME_TEMPLATE_FILENAMES2 = _normalize_fs_paths([
|
||||||
"Folder1_SubFolder2_AlbumInFolder-IMG_4547.jpg",
|
"Folder1_SubFolder2_AlbumInFolder-IMG_4547.jpg",
|
||||||
"Folder1_SubFolder2_AlbumInFolder-wedding.jpg",
|
"Folder1_SubFolder2_AlbumInFolder-wedding.jpg",
|
||||||
"Folder1_SubFolder2_AlbumInFolder-wedding_edited.jpeg",
|
"Folder1_SubFolder2_AlbumInFolder-wedding_edited.jpeg",
|
||||||
@ -484,18 +501,18 @@ CLI_EXPORTED_FILENAME_TEMPLATE_FILENAMES2 = [
|
|||||||
"None-IMG_1693.tif",
|
"None-IMG_1693.tif",
|
||||||
"I have a deleted twin-wedding.jpg",
|
"I have a deleted twin-wedding.jpg",
|
||||||
"I have a deleted twin-wedding_edited.jpeg",
|
"I have a deleted twin-wedding_edited.jpeg",
|
||||||
]
|
])
|
||||||
|
|
||||||
CLI_EXPORTED_FILENAME_TEMPLATE_FILENAMES_PATHSEP = [
|
CLI_EXPORTED_FILENAME_TEMPLATE_FILENAMES_PATHSEP = _normalize_fs_paths([
|
||||||
"2018-10 - Sponsion, Museum, Frühstück, Römermuseum/IMG_4547.jpg",
|
"2018-10 - Sponsion, Museum, Frühstück, Römermuseum/IMG_4547.jpg",
|
||||||
"Folder1/SubFolder2/AlbumInFolder/IMG_4547.jpg",
|
"Folder1/SubFolder2/AlbumInFolder/IMG_4547.jpg",
|
||||||
"2019-10:11 Paris Clermont/IMG_4547.jpg",
|
"2019-10:11 Paris Clermont/IMG_4547.jpg",
|
||||||
]
|
])
|
||||||
|
|
||||||
|
|
||||||
CLI_EXPORTED_FILENAME_TEMPLATE_FILENAMES_KEYWORD_PATHSEP = [
|
CLI_EXPORTED_FILENAME_TEMPLATE_FILENAMES_KEYWORD_PATHSEP = _normalize_fs_paths([
|
||||||
"foo:bar/foo:bar_IMG_3092.heic"
|
"foo:bar/foo:bar_IMG_3092.heic"
|
||||||
]
|
])
|
||||||
|
|
||||||
CLI_EXPORTED_FILENAME_TEMPLATE_LONG_DESCRIPTION = [
|
CLI_EXPORTED_FILENAME_TEMPLATE_LONG_DESCRIPTION = [
|
||||||
"Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo"
|
"Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo"
|
||||||
@ -519,46 +536,59 @@ CLI_EXPORT_BY_DATE_TOUCH_UUID = [
|
|||||||
"F12384F6-CD17-4151-ACBA-AE0E3688539E", # Pumkins1.jpg
|
"F12384F6-CD17-4151-ACBA-AE0E3688539E", # Pumkins1.jpg
|
||||||
]
|
]
|
||||||
CLI_EXPORT_BY_DATE_TOUCH_TIMES = [1538165373, 1538163349]
|
CLI_EXPORT_BY_DATE_TOUCH_TIMES = [1538165373, 1538163349]
|
||||||
CLI_EXPORT_BY_DATE_NEED_TOUCH = [
|
CLI_EXPORT_BY_DATE_NEED_TOUCH = _normalize_fs_paths([
|
||||||
"2018/09/28/Pumkins2.jpg",
|
"2018/09/28/Pumkins2.jpg",
|
||||||
"2018/10/13/St James Park.jpg",
|
"2018/10/13/St James Park.jpg",
|
||||||
]
|
])
|
||||||
CLI_EXPORT_BY_DATE_NEED_TOUCH_UUID = [
|
CLI_EXPORT_BY_DATE_NEED_TOUCH_UUID = [
|
||||||
"D79B8D77-BFFC-460B-9312-034F2877D35B",
|
"D79B8D77-BFFC-460B-9312-034F2877D35B",
|
||||||
"DC99FBDD-7A52-4100-A5BB-344131646C30",
|
"DC99FBDD-7A52-4100-A5BB-344131646C30",
|
||||||
]
|
]
|
||||||
CLI_EXPORT_BY_DATE_NEED_TOUCH_TIMES = [1538165227, 1539436692]
|
CLI_EXPORT_BY_DATE_NEED_TOUCH_TIMES = [1538165227, 1539436692]
|
||||||
CLI_EXPORT_BY_DATE = ["2018/09/28/Pumpkins3.jpg", "2018/09/28/Pumkins1.jpg"]
|
CLI_EXPORT_BY_DATE = _normalize_fs_paths([
|
||||||
|
"2018/09/28/Pumpkins3.jpg",
|
||||||
|
"2018/09/28/Pumkins1.jpg",
|
||||||
|
])
|
||||||
|
|
||||||
CLI_EXPORT_SIDECAR_FILENAMES = ["Pumkins2.jpg", "Pumkins2.jpg.json", "Pumkins2.jpg.xmp"]
|
CLI_EXPORT_SIDECAR_FILENAMES = _normalize_fs_paths([
|
||||||
CLI_EXPORT_SIDECAR_DROP_EXT_FILENAMES = [
|
"Pumkins2.jpg",
|
||||||
|
"Pumkins2.jpg.json",
|
||||||
|
"Pumkins2.jpg.xmp",
|
||||||
|
])
|
||||||
|
CLI_EXPORT_SIDECAR_DROP_EXT_FILENAMES = _normalize_fs_paths([
|
||||||
"Pumkins2.jpg",
|
"Pumkins2.jpg",
|
||||||
"Pumkins2.json",
|
"Pumkins2.json",
|
||||||
"Pumkins2.xmp",
|
"Pumkins2.xmp",
|
||||||
]
|
])
|
||||||
|
|
||||||
CLI_EXPORT_LIVE = [
|
CLI_EXPORT_LIVE = [
|
||||||
"51F2BEF7-431A-4D31-8AC1-3284A57826AE.jpeg",
|
"51F2BEF7-431A-4D31-8AC1-3284A57826AE.jpeg",
|
||||||
"51F2BEF7-431A-4D31-8AC1-3284A57826AE.mov",
|
"51F2BEF7-431A-4D31-8AC1-3284A57826AE.mov",
|
||||||
]
|
]
|
||||||
|
|
||||||
CLI_EXPORT_LIVE_ORIGINAL = ["IMG_0728.JPG", "IMG_0728.mov"]
|
CLI_EXPORT_LIVE_ORIGINAL = _normalize_fs_paths([
|
||||||
|
"IMG_0728.JPG",
|
||||||
|
"IMG_0728.mov",
|
||||||
|
])
|
||||||
|
|
||||||
CLI_EXPORT_RAW = ["441DFE2A-A69B-4C79-A69B-3F51D1B9B29C.cr2"]
|
CLI_EXPORT_RAW = ["441DFE2A-A69B-4C79-A69B-3F51D1B9B29C.cr2"]
|
||||||
CLI_EXPORT_RAW_ORIGINAL = ["IMG_0476_2.CR2"]
|
CLI_EXPORT_RAW_ORIGINAL = _normalize_fs_paths(["IMG_0476_2.CR2"])
|
||||||
CLI_EXPORT_RAW_EDITED = [
|
CLI_EXPORT_RAW_EDITED = [
|
||||||
"441DFE2A-A69B-4C79-A69B-3F51D1B9B29C.cr2",
|
"441DFE2A-A69B-4C79-A69B-3F51D1B9B29C.cr2",
|
||||||
"441DFE2A-A69B-4C79-A69B-3F51D1B9B29C_edited.jpeg",
|
"441DFE2A-A69B-4C79-A69B-3F51D1B9B29C_edited.jpeg",
|
||||||
]
|
]
|
||||||
CLI_EXPORT_RAW_EDITED_ORIGINAL = ["IMG_0476_2.CR2", "IMG_0476_2_edited.jpeg"]
|
CLI_EXPORT_RAW_EDITED_ORIGINAL = _normalize_fs_paths([
|
||||||
|
"IMG_0476_2.CR2",
|
||||||
|
"IMG_0476_2_edited.jpeg",
|
||||||
|
])
|
||||||
|
|
||||||
CLI_UUID_DICT_15_7 = {
|
CLI_UUID_DICT_15_7 = {
|
||||||
"intrash": "71E3E212-00EB-430D-8A63-5E294B268554",
|
"intrash": "71E3E212-00EB-430D-8A63-5E294B268554",
|
||||||
"template": "F12384F6-CD17-4151-ACBA-AE0E3688539E",
|
"template": "F12384F6-CD17-4151-ACBA-AE0E3688539E",
|
||||||
}
|
}
|
||||||
|
|
||||||
CLI_TEMPLATE_SIDECAR_FILENAME = "Pumkins1.jpg.json"
|
CLI_TEMPLATE_SIDECAR_FILENAME = normalize_fs_path("Pumkins1.jpg.json")
|
||||||
CLI_TEMPLATE_FILENAME = "Pumkins1.jpg"
|
CLI_TEMPLATE_FILENAME = normalize_fs_path("Pumkins1.jpg")
|
||||||
|
|
||||||
CLI_UUID_DICT_14_6 = {"intrash": "3tljdX43R8+k6peNHVrJNQ"}
|
CLI_UUID_DICT_14_6 = {"intrash": "3tljdX43R8+k6peNHVrJNQ"}
|
||||||
|
|
||||||
@ -960,12 +990,12 @@ UUID_UNICODE_TITLE = [
|
|||||||
"D1D4040D-D141-44E8-93EA-E403D9F63E07", # Frítest.jpg
|
"D1D4040D-D141-44E8-93EA-E403D9F63E07", # Frítest.jpg
|
||||||
]
|
]
|
||||||
|
|
||||||
EXPORT_UNICODE_TITLE_FILENAMES = [
|
EXPORT_UNICODE_TITLE_FILENAMES = _normalize_fs_paths([
|
||||||
"Frítest.jpg",
|
"Frítest.jpg",
|
||||||
"Frítest (1).jpg",
|
"Frítest (1).jpg",
|
||||||
"Frítest (2).jpg",
|
"Frítest (2).jpg",
|
||||||
"Frítest (3).jpg",
|
"Frítest (3).jpg",
|
||||||
]
|
])
|
||||||
|
|
||||||
# data for --report
|
# data for --report
|
||||||
UUID_REPORT = [
|
UUID_REPORT = [
|
||||||
@ -987,12 +1017,12 @@ 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
|
UUID_LIVE_EDITED = "136A78FA-1B90-46CC-88A7-CCA3331F0353" # IMG_4813.HEIC
|
||||||
CLI_EXPORT_LIVE_EDITED = [
|
CLI_EXPORT_LIVE_EDITED = _normalize_fs_paths([
|
||||||
"IMG_4813.HEIC",
|
"IMG_4813.HEIC",
|
||||||
"IMG_4813.mov",
|
"IMG_4813.mov",
|
||||||
"IMG_4813_edited.jpeg",
|
"IMG_4813_edited.jpeg",
|
||||||
"IMG_4813_edited.mov",
|
"IMG_4813_edited.mov",
|
||||||
]
|
])
|
||||||
|
|
||||||
UUID_FAVORITE = "E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51"
|
UUID_FAVORITE = "E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51"
|
||||||
FILE_FAVORITE = "wedding.jpg"
|
FILE_FAVORITE = "wedding.jpg"
|
||||||
@ -1804,12 +1834,25 @@ def test_export_preview_update():
|
|||||||
assert len(files) == 2 # preview + original
|
assert len(files) == 2 # preview + original
|
||||||
|
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def isolated_filesystem_here():
|
||||||
|
cwd = os.getcwd()
|
||||||
|
tempdir = tempfile.mkdtemp(dir=cwd) # type: ignore[type-var]
|
||||||
|
os.chdir(tempdir)
|
||||||
|
|
||||||
|
try:
|
||||||
|
yield tempdir
|
||||||
|
finally:
|
||||||
|
os.chdir(cwd)
|
||||||
|
shutil.rmtree(tempdir)
|
||||||
|
|
||||||
|
|
||||||
def test_export_as_hardlink():
|
def test_export_as_hardlink():
|
||||||
|
|
||||||
runner = CliRunner()
|
runner = CliRunner()
|
||||||
cwd = os.getcwd()
|
cwd = os.getcwd()
|
||||||
# pylint: disable=not-context-manager
|
# pylint: disable=not-context-manager
|
||||||
with runner.isolated_filesystem():
|
with isolated_filesystem_here():
|
||||||
result = runner.invoke(
|
result = runner.invoke(
|
||||||
export,
|
export,
|
||||||
[os.path.join(cwd, CLI_PHOTOS_DB), ".", "--export-as-hardlink", "-V"],
|
[os.path.join(cwd, CLI_PHOTOS_DB), ".", "--export-as-hardlink", "-V"],
|
||||||
@ -1829,7 +1872,7 @@ def test_export_as_hardlink_samefile():
|
|||||||
photo = photosdb.photos(uuid=[CLI_EXPORT_UUID])[0]
|
photo = photosdb.photos(uuid=[CLI_EXPORT_UUID])[0]
|
||||||
|
|
||||||
# pylint: disable=not-context-manager
|
# pylint: disable=not-context-manager
|
||||||
with runner.isolated_filesystem():
|
with isolated_filesystem_here():
|
||||||
result = runner.invoke(
|
result = runner.invoke(
|
||||||
export,
|
export,
|
||||||
[
|
[
|
||||||
@ -1854,7 +1897,7 @@ def test_export_using_hardlinks_incompat_options():
|
|||||||
photo = photosdb.photos(uuid=[CLI_EXPORT_UUID])[0]
|
photo = photosdb.photos(uuid=[CLI_EXPORT_UUID])[0]
|
||||||
|
|
||||||
# pylint: disable=not-context-manager
|
# pylint: disable=not-context-manager
|
||||||
with runner.isolated_filesystem():
|
with isolated_filesystem_here():
|
||||||
result = runner.invoke(
|
result = runner.invoke(
|
||||||
export,
|
export,
|
||||||
[
|
[
|
||||||
@ -3717,7 +3760,7 @@ def test_export_raw_edited_original():
|
|||||||
def test_export_directory_template_1():
|
def test_export_directory_template_1():
|
||||||
# test export using directory template
|
# test export using directory template
|
||||||
|
|
||||||
locale.setlocale(locale.LC_ALL, "en_US")
|
setlocale(locale.LC_ALL, "en_US")
|
||||||
|
|
||||||
runner = CliRunner()
|
runner = CliRunner()
|
||||||
cwd = os.getcwd()
|
cwd = os.getcwd()
|
||||||
@ -3837,7 +3880,7 @@ def test_export_directory_template_locale():
|
|||||||
with runner.isolated_filesystem():
|
with runner.isolated_filesystem():
|
||||||
# set locale environment
|
# set locale environment
|
||||||
os.environ["LC_ALL"] = "de_DE.UTF-8"
|
os.environ["LC_ALL"] = "de_DE.UTF-8"
|
||||||
locale.setlocale(locale.LC_ALL, "")
|
setlocale(locale.LC_ALL, "")
|
||||||
result = runner.invoke(
|
result = runner.invoke(
|
||||||
export,
|
export,
|
||||||
[
|
[
|
||||||
@ -3857,7 +3900,7 @@ def test_export_directory_template_locale():
|
|||||||
def test_export_filename_template_1():
|
def test_export_filename_template_1():
|
||||||
"""export photos using filename template"""
|
"""export photos using filename template"""
|
||||||
|
|
||||||
locale.setlocale(locale.LC_ALL, "en_US")
|
setlocale(locale.LC_ALL, "en_US")
|
||||||
|
|
||||||
runner = CliRunner()
|
runner = CliRunner()
|
||||||
cwd = os.getcwd()
|
cwd = os.getcwd()
|
||||||
@ -3882,7 +3925,7 @@ def test_export_filename_template_1():
|
|||||||
def test_export_filename_template_2():
|
def test_export_filename_template_2():
|
||||||
"""export photos using filename template with folder_album and path_sep"""
|
"""export photos using filename template with folder_album and path_sep"""
|
||||||
|
|
||||||
locale.setlocale(locale.LC_ALL, "en_US")
|
setlocale(locale.LC_ALL, "en_US")
|
||||||
|
|
||||||
runner = CliRunner()
|
runner = CliRunner()
|
||||||
cwd = os.getcwd()
|
cwd = os.getcwd()
|
||||||
@ -3907,7 +3950,7 @@ def test_export_filename_template_2():
|
|||||||
def test_export_filename_template_strip():
|
def test_export_filename_template_strip():
|
||||||
"""export photos using filename template with --strip"""
|
"""export photos using filename template with --strip"""
|
||||||
|
|
||||||
locale.setlocale(locale.LC_ALL, "en_US")
|
setlocale(locale.LC_ALL, "en_US")
|
||||||
|
|
||||||
runner = CliRunner()
|
runner = CliRunner()
|
||||||
cwd = os.getcwd()
|
cwd = os.getcwd()
|
||||||
@ -3933,7 +3976,7 @@ def test_export_filename_template_strip():
|
|||||||
def test_export_filename_template_pathsep_in_name_1():
|
def test_export_filename_template_pathsep_in_name_1():
|
||||||
"""export photos using filename template with folder_album and "/" in album name"""
|
"""export photos using filename template with folder_album and "/" in album name"""
|
||||||
|
|
||||||
locale.setlocale(locale.LC_ALL, "en_US")
|
setlocale(locale.LC_ALL, "en_US")
|
||||||
|
|
||||||
runner = CliRunner()
|
runner = CliRunner()
|
||||||
cwd = os.getcwd()
|
cwd = os.getcwd()
|
||||||
@ -3960,7 +4003,7 @@ def test_export_filename_template_pathsep_in_name_1():
|
|||||||
def test_export_filename_template_pathsep_in_name_2():
|
def test_export_filename_template_pathsep_in_name_2():
|
||||||
"""export photos using filename template with keyword and "/" in keyword"""
|
"""export photos using filename template with keyword and "/" in keyword"""
|
||||||
|
|
||||||
locale.setlocale(locale.LC_ALL, "en_US")
|
setlocale(locale.LC_ALL, "en_US")
|
||||||
|
|
||||||
runner = CliRunner()
|
runner = CliRunner()
|
||||||
cwd = os.getcwd()
|
cwd = os.getcwd()
|
||||||
@ -3988,7 +4031,7 @@ def test_export_filename_template_pathsep_in_name_2():
|
|||||||
def test_export_filename_template_long_description():
|
def test_export_filename_template_long_description():
|
||||||
"""export photos using filename template with description that exceeds max length"""
|
"""export photos using filename template with description that exceeds max length"""
|
||||||
|
|
||||||
locale.setlocale(locale.LC_ALL, "en_US")
|
setlocale(locale.LC_ALL, "en_US")
|
||||||
|
|
||||||
runner = CliRunner()
|
runner = CliRunner()
|
||||||
cwd = os.getcwd()
|
cwd = os.getcwd()
|
||||||
@ -4664,7 +4707,6 @@ def test_export_force_update():
|
|||||||
export, [os.path.join(cwd, photos_db_path), ".", "--force-update"]
|
export, [os.path.join(cwd, photos_db_path), ".", "--force-update"]
|
||||||
)
|
)
|
||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
print(result.output)
|
|
||||||
assert (
|
assert (
|
||||||
f"Processed: {PHOTOS_NOT_IN_TRASH_LEN_15_7} photos, exported: 0, updated: 0, skipped: {PHOTOS_NOT_IN_TRASH_LEN_15_7+PHOTOS_EDITED_15_7}, updated EXIF data: 0, missing: 3, error: 0"
|
f"Processed: {PHOTOS_NOT_IN_TRASH_LEN_15_7} photos, exported: 0, updated: 0, skipped: {PHOTOS_NOT_IN_TRASH_LEN_15_7+PHOTOS_EDITED_15_7}, updated EXIF data: 0, missing: 3, error: 0"
|
||||||
in result.output
|
in result.output
|
||||||
@ -4891,7 +4933,7 @@ def test_export_update_hardlink():
|
|||||||
runner = CliRunner()
|
runner = CliRunner()
|
||||||
cwd = os.getcwd()
|
cwd = os.getcwd()
|
||||||
# pylint: disable=not-context-manager
|
# pylint: disable=not-context-manager
|
||||||
with runner.isolated_filesystem():
|
with isolated_filesystem_here():
|
||||||
# basic export
|
# basic export
|
||||||
result = runner.invoke(
|
result = runner.invoke(
|
||||||
export,
|
export,
|
||||||
@ -4924,7 +4966,7 @@ def test_export_update_hardlink_exiftool():
|
|||||||
runner = CliRunner()
|
runner = CliRunner()
|
||||||
cwd = os.getcwd()
|
cwd = os.getcwd()
|
||||||
# pylint: disable=not-context-manager
|
# pylint: disable=not-context-manager
|
||||||
with runner.isolated_filesystem():
|
with isolated_filesystem_here():
|
||||||
# basic export
|
# basic export
|
||||||
result = runner.invoke(
|
result = runner.invoke(
|
||||||
export,
|
export,
|
||||||
@ -5069,7 +5111,7 @@ def test_export_then_hardlink():
|
|||||||
runner = CliRunner()
|
runner = CliRunner()
|
||||||
cwd = os.getcwd()
|
cwd = os.getcwd()
|
||||||
# pylint: disable=not-context-manager
|
# pylint: disable=not-context-manager
|
||||||
with runner.isolated_filesystem():
|
with isolated_filesystem_here():
|
||||||
# basic export
|
# basic export
|
||||||
result = runner.invoke(export, [os.path.join(cwd, CLI_PHOTOS_DB), ".", "-V"])
|
result = runner.invoke(export, [os.path.join(cwd, CLI_PHOTOS_DB), ".", "-V"])
|
||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
@ -5177,7 +5219,7 @@ def test_export_update_edits_dry_run():
|
|||||||
def test_export_directory_template_1_dry_run():
|
def test_export_directory_template_1_dry_run():
|
||||||
"""test export using directory template with dry-run flag"""
|
"""test export using directory template with dry-run flag"""
|
||||||
|
|
||||||
locale.setlocale(locale.LC_ALL, "en_US")
|
setlocale(locale.LC_ALL, "en_US")
|
||||||
|
|
||||||
runner = CliRunner()
|
runner = CliRunner()
|
||||||
cwd = os.getcwd()
|
cwd = os.getcwd()
|
||||||
@ -6052,7 +6094,7 @@ def test_export_as_hardlink_download_missing():
|
|||||||
runner = CliRunner()
|
runner = CliRunner()
|
||||||
cwd = os.getcwd()
|
cwd = os.getcwd()
|
||||||
# pylint: disable=not-context-manager
|
# pylint: disable=not-context-manager
|
||||||
with runner.isolated_filesystem():
|
with isolated_filesystem_here():
|
||||||
result = runner.invoke(
|
result = runner.invoke(
|
||||||
export,
|
export,
|
||||||
[
|
[
|
||||||
@ -6877,6 +6919,7 @@ def test_export_finder_tag_keywords_dry_run():
|
|||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(not is_macos, reason="Only works on macOS")
|
||||||
def test_export_finder_tag_keywords():
|
def test_export_finder_tag_keywords():
|
||||||
"""test --finder-tag-keywords"""
|
"""test --finder-tag-keywords"""
|
||||||
|
|
||||||
@ -6951,6 +6994,7 @@ def test_export_finder_tag_keywords():
|
|||||||
assert sorted(md.tags) == sorted(expected)
|
assert sorted(md.tags) == sorted(expected)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(not is_macos, reason="Only works on macOS")
|
||||||
def test_export_finder_tag_template():
|
def test_export_finder_tag_template():
|
||||||
"""test --finder-tag-template"""
|
"""test --finder-tag-template"""
|
||||||
|
|
||||||
@ -7028,6 +7072,7 @@ def test_export_finder_tag_template():
|
|||||||
assert sorted(md.tags) == sorted(expected)
|
assert sorted(md.tags) == sorted(expected)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(not is_macos, reason="Only works on macOS")
|
||||||
def test_export_finder_tag_template_multiple():
|
def test_export_finder_tag_template_multiple():
|
||||||
"""test --finder-tag-template used more than once"""
|
"""test --finder-tag-template used more than once"""
|
||||||
|
|
||||||
@ -7061,6 +7106,7 @@ def test_export_finder_tag_template_multiple():
|
|||||||
assert sorted(md.tags) == sorted(expected)
|
assert sorted(md.tags) == sorted(expected)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(not is_macos, reason="Only works on macOS")
|
||||||
def test_export_finder_tag_template_keywords():
|
def test_export_finder_tag_template_keywords():
|
||||||
"""test --finder-tag-template with --finder-tag-keywords"""
|
"""test --finder-tag-template with --finder-tag-keywords"""
|
||||||
|
|
||||||
@ -7093,6 +7139,7 @@ def test_export_finder_tag_template_keywords():
|
|||||||
assert sorted(md.tags) == sorted(expected)
|
assert sorted(md.tags) == sorted(expected)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(not is_macos, reason="Only works on macOS")
|
||||||
def test_export_finder_tag_template_multi_field():
|
def test_export_finder_tag_template_multi_field():
|
||||||
"""test --finder-tag-template with multiple fields (issue #422)"""
|
"""test --finder-tag-template with multiple fields (issue #422)"""
|
||||||
|
|
||||||
@ -7157,6 +7204,7 @@ def test_export_xattr_template_dry_run():
|
|||||||
assert "Writing extended attribute" in result.output
|
assert "Writing extended attribute" in result.output
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(not is_macos, reason="Only works on macOS")
|
||||||
def test_export_xattr_template():
|
def test_export_xattr_template():
|
||||||
"""test --xattr template"""
|
"""test --xattr template"""
|
||||||
|
|
||||||
@ -7655,14 +7703,14 @@ def test_query_name_unicode():
|
|||||||
cwd = os.getcwd()
|
cwd = os.getcwd()
|
||||||
result = runner.invoke(
|
result = runner.invoke(
|
||||||
query,
|
query,
|
||||||
["--json", "--db", os.path.join(cwd, PHOTOS_DB_15_7), "--name", "Frítest"],
|
["--json", "--db", os.path.join(cwd, PHOTOS_DB_15_7), "--name", "Frítest"],
|
||||||
)
|
)
|
||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
json_got = json.loads(result.output)
|
json_got = json.loads(result.output)
|
||||||
|
|
||||||
assert len(json_got) == 4
|
assert len(json_got) == 4
|
||||||
assert normalize_unicode(json_got[0]["original_filename"]).startswith(
|
assert normalize_unicode(json_got[0]["original_filename"]).startswith(
|
||||||
normalize_unicode("Frítest.jpg")
|
normalize_unicode("Frítest.jpg")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,10 +1,14 @@
|
|||||||
"""Test osxphotos add-locations command"""
|
"""Test osxphotos add-locations command"""
|
||||||
|
|
||||||
import photoscript
|
|
||||||
import pytest
|
import pytest
|
||||||
from click.testing import CliRunner
|
from click.testing import CliRunner
|
||||||
|
|
||||||
from osxphotos.cli.add_locations import add_locations
|
from osxphotos.utils import is_macos
|
||||||
|
if is_macos:
|
||||||
|
import photoscript
|
||||||
|
from osxphotos.cli.add_locations import add_locations
|
||||||
|
else:
|
||||||
|
pytest.skip(allow_module_level=True)
|
||||||
|
|
||||||
UUID_TEST_PHOTO_1 = "F12384F6-CD17-4151-ACBA-AE0E3688539E" # Pumkins1.jpg
|
UUID_TEST_PHOTO_1 = "F12384F6-CD17-4151-ACBA-AE0E3688539E" # Pumkins1.jpg
|
||||||
UUID_TEST_PHOTO_LOCATION = "D79B8D77-BFFC-460B-9312-034F2877D35B" # Pumkins2.jpg
|
UUID_TEST_PHOTO_LOCATION = "D79B8D77-BFFC-460B-9312-034F2877D35B" # Pumkins2.jpg
|
||||||
|
|||||||
@ -2,10 +2,15 @@
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
import photoscript
|
|
||||||
import pytest
|
import pytest
|
||||||
from click.testing import CliRunner
|
from click.testing import CliRunner
|
||||||
|
|
||||||
|
from osxphotos.utils import is_macos
|
||||||
|
if is_macos:
|
||||||
|
import photoscript
|
||||||
|
else:
|
||||||
|
pytest.skip(allow_module_level=True)
|
||||||
|
|
||||||
UUID_EXPORT = {"3DD2C897-F19E-4CA6-8C22-B027D5A71907": {"filename": "IMG_4547.jpg"}}
|
UUID_EXPORT = {"3DD2C897-F19E-4CA6-8C22-B027D5A71907": {"filename": "IMG_4547.jpg"}}
|
||||||
UUID_MISSING = {
|
UUID_MISSING = {
|
||||||
"8E1D7BC9-9321-44F9-8CFB-4083F6B9232A": {"filename": "IMG_2000.JPGssss"}
|
"8E1D7BC9-9321-44F9-8CFB-4083F6B9232A": {"filename": "IMG_2000.JPGssss"}
|
||||||
|
|||||||
@ -24,7 +24,6 @@ TEST_RUN_SCRIPT = "examples/cli_example_1.py"
|
|||||||
def runner() -> CliRunner:
|
def runner() -> CliRunner:
|
||||||
return CliRunner()
|
return CliRunner()
|
||||||
|
|
||||||
|
|
||||||
from osxphotos.cli import (
|
from osxphotos.cli import (
|
||||||
about,
|
about,
|
||||||
albums,
|
albums,
|
||||||
@ -42,10 +41,14 @@ from osxphotos.cli import (
|
|||||||
places,
|
places,
|
||||||
theme,
|
theme,
|
||||||
tutorial,
|
tutorial,
|
||||||
uuid,
|
|
||||||
version,
|
version,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from osxphotos.utils import is_macos
|
||||||
|
|
||||||
|
if is_macos:
|
||||||
|
from osxphotos.cli import uuid
|
||||||
|
|
||||||
|
|
||||||
def test_about(runner: CliRunner):
|
def test_about(runner: CliRunner):
|
||||||
with runner.isolated_filesystem():
|
with runner.isolated_filesystem():
|
||||||
@ -68,9 +71,8 @@ def test_about(runner: CliRunner):
|
|||||||
persons,
|
persons,
|
||||||
places,
|
places,
|
||||||
tutorial,
|
tutorial,
|
||||||
uuid,
|
|
||||||
version,
|
version,
|
||||||
],
|
] + ([uuid] if is_macos else []),
|
||||||
)
|
)
|
||||||
def test_cli_comands(runner: CliRunner, command: Callable[..., Any]):
|
def test_cli_comands(runner: CliRunner, command: Callable[..., Any]):
|
||||||
with runner.isolated_filesystem():
|
with runner.isolated_filesystem():
|
||||||
|
|||||||
@ -5,12 +5,17 @@ from __future__ import annotations
|
|||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
|
|
||||||
import photoscript
|
|
||||||
import pytest
|
import pytest
|
||||||
from click.testing import CliRunner
|
from click.testing import CliRunner
|
||||||
|
|
||||||
import osxphotos
|
import osxphotos
|
||||||
from osxphotos.cli.batch_edit import batch_edit
|
from osxphotos.utils import is_macos
|
||||||
|
|
||||||
|
if is_macos:
|
||||||
|
import photoscript
|
||||||
|
from osxphotos.cli.batch_edit import batch_edit
|
||||||
|
else:
|
||||||
|
pytest.skip(allow_module_level=True)
|
||||||
|
|
||||||
# set timezone to avoid issues with comparing dates
|
# set timezone to avoid issues with comparing dates
|
||||||
os.environ["TZ"] = "US/Pacific"
|
os.environ["TZ"] = "US/Pacific"
|
||||||
|
|||||||
@ -14,17 +14,23 @@ from tempfile import TemporaryDirectory
|
|||||||
from typing import Dict
|
from typing import Dict
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from click.testing import CliRunner
|
from click.testing import CliRunner
|
||||||
from photoscript import Photo
|
|
||||||
from pytest import MonkeyPatch, approx
|
from pytest import MonkeyPatch, approx
|
||||||
|
|
||||||
from osxphotos import PhotosDB, QueryOptions
|
from osxphotos import PhotosDB, QueryOptions
|
||||||
from osxphotos._constants import UUID_PATTERN
|
from osxphotos._constants import UUID_PATTERN
|
||||||
from osxphotos.cli.import_cli import import_cli
|
|
||||||
from osxphotos.datetime_utils import datetime_remove_tz
|
from osxphotos.datetime_utils import datetime_remove_tz
|
||||||
from osxphotos.exiftool import get_exiftool_path
|
from osxphotos.exiftool import get_exiftool_path
|
||||||
|
from osxphotos.utils import is_macos
|
||||||
from tests.conftest import get_os_version
|
from tests.conftest import get_os_version
|
||||||
|
|
||||||
|
if is_macos:
|
||||||
|
from photoscript import Photo
|
||||||
|
from osxphotos.cli.import_cli import import_cli
|
||||||
|
else:
|
||||||
|
pytest.skip(allow_module_level=True)
|
||||||
|
|
||||||
TERMINAL_WIDTH = 250
|
TERMINAL_WIDTH = 250
|
||||||
|
|
||||||
TEST_IMAGES_DIR = "tests/test-images"
|
TEST_IMAGES_DIR = "tests/test-images"
|
||||||
|
|||||||
@ -2,11 +2,15 @@
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
import photoscript
|
|
||||||
import pytest
|
import pytest
|
||||||
from click.testing import CliRunner
|
from click.testing import CliRunner
|
||||||
|
|
||||||
from osxphotos.cli.sync import sync
|
from osxphotos.utils import is_macos
|
||||||
|
if is_macos:
|
||||||
|
import photoscript
|
||||||
|
from osxphotos.cli.sync import sync
|
||||||
|
else:
|
||||||
|
pytest.skip(allow_module_level=True)
|
||||||
|
|
||||||
UUID_TEST_PHOTO_1 = "D79B8D77-BFFC-460B-9312-034F2877D35B" # Pumkins2.jpg
|
UUID_TEST_PHOTO_1 = "D79B8D77-BFFC-460B-9312-034F2877D35B" # Pumkins2.jpg
|
||||||
UUID_TEST_PHOTO_2 = "E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51" # wedding.jpg
|
UUID_TEST_PHOTO_2 = "E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51" # wedding.jpg
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
""" test datetime_formatter.DateTimeFormatter """
|
""" test datetime_formatter.DateTimeFormatter """
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from .locale_util import setlocale
|
||||||
|
|
||||||
|
|
||||||
def test_datetime_formatter_1():
|
def test_datetime_formatter_1():
|
||||||
"""Test DateTimeFormatter """
|
"""Test DateTimeFormatter """
|
||||||
@ -8,7 +10,7 @@ def test_datetime_formatter_1():
|
|||||||
import locale
|
import locale
|
||||||
from osxphotos.datetime_formatter import DateTimeFormatter
|
from osxphotos.datetime_formatter import DateTimeFormatter
|
||||||
|
|
||||||
locale.setlocale(locale.LC_ALL, "en_US")
|
setlocale(locale.LC_ALL, "en_US")
|
||||||
|
|
||||||
dt = datetime.datetime(2020, 5, 23, 12, 42, 33)
|
dt = datetime.datetime(2020, 5, 23, 12, 42, 33)
|
||||||
dtf = DateTimeFormatter(dt)
|
dtf = DateTimeFormatter(dt)
|
||||||
@ -32,7 +34,7 @@ def test_datetime_formatter_2():
|
|||||||
import locale
|
import locale
|
||||||
from osxphotos.datetime_formatter import DateTimeFormatter
|
from osxphotos.datetime_formatter import DateTimeFormatter
|
||||||
|
|
||||||
locale.setlocale(locale.LC_ALL, "en_US")
|
setlocale(locale.LC_ALL, "en_US")
|
||||||
|
|
||||||
dt = datetime.datetime(2020, 5, 23, 14, 42, 33)
|
dt = datetime.datetime(2020, 5, 23, 14, 42, 33)
|
||||||
dtf = DateTimeFormatter(dt)
|
dtf = DateTimeFormatter(dt)
|
||||||
@ -56,7 +58,7 @@ def test_datetime_formatter_3():
|
|||||||
import locale
|
import locale
|
||||||
from osxphotos.datetime_formatter import DateTimeFormatter
|
from osxphotos.datetime_formatter import DateTimeFormatter
|
||||||
|
|
||||||
locale.setlocale(locale.LC_ALL, "en_US")
|
setlocale(locale.LC_ALL, "en_US")
|
||||||
|
|
||||||
dt = datetime.datetime(2020, 5, 2, 9, 3, 6)
|
dt = datetime.datetime(2020, 5, 2, 9, 3, 6)
|
||||||
dtf = DateTimeFormatter(dt)
|
dtf = DateTimeFormatter(dt)
|
||||||
|
|||||||
@ -538,13 +538,13 @@ def test_exiftool_terminate():
|
|||||||
|
|
||||||
ps = subprocess.run(["ps"], capture_output=True)
|
ps = subprocess.run(["ps"], capture_output=True)
|
||||||
stdout = ps.stdout.decode("utf-8")
|
stdout = ps.stdout.decode("utf-8")
|
||||||
assert "exiftool -stay_open" in stdout
|
assert "exiftool" in stdout
|
||||||
|
|
||||||
osxphotos.exiftool.terminate_exiftool()
|
osxphotos.exiftool.terminate_exiftool()
|
||||||
|
|
||||||
ps = subprocess.run(["ps"], capture_output=True)
|
ps = subprocess.run(["ps"], capture_output=True)
|
||||||
stdout = ps.stdout.decode("utf-8")
|
stdout = ps.stdout.decode("utf-8")
|
||||||
assert "exiftool -stay_open" not in stdout
|
assert "exiftool" not in stdout
|
||||||
|
|
||||||
# verify we can create a new instance after termination
|
# verify we can create a new instance after termination
|
||||||
exif2 = osxphotos.exiftool.ExifTool(TEST_FILE_ONE_KEYWORD)
|
exif2 = osxphotos.exiftool.ExifTool(TEST_FILE_ONE_KEYWORD)
|
||||||
|
|||||||
@ -2,9 +2,9 @@ import os
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from osxphotos._constants import _UNKNOWN_PERSON
|
from osxphotos._constants import _UNKNOWN_PERSON
|
||||||
from osxphotos.utils import get_macos_version
|
from osxphotos.utils import is_macos, get_macos_version
|
||||||
|
|
||||||
OS_VERSION = get_macos_version()
|
OS_VERSION = get_macos_version() if is_macos else (None, None, None)
|
||||||
SKIP_TEST = "OSXPHOTOS_TEST_EXPORT" not in os.environ or OS_VERSION[1] != "15"
|
SKIP_TEST = "OSXPHOTOS_TEST_EXPORT" not in os.environ or OS_VERSION[1] != "15"
|
||||||
PHOTOS_DB = os.path.expanduser("~/Pictures/Photos Library.photoslibrary")
|
PHOTOS_DB = os.path.expanduser("~/Pictures/Photos Library.photoslibrary")
|
||||||
pytestmark = pytest.mark.skipif(
|
pytestmark = pytest.mark.skipif(
|
||||||
|
|||||||
@ -74,10 +74,12 @@ def test_hardlink_file_valid():
|
|||||||
|
|
||||||
temp_dir = tempfile.TemporaryDirectory(prefix="osxphotos_")
|
temp_dir = tempfile.TemporaryDirectory(prefix="osxphotos_")
|
||||||
src = "tests/test-images/wedding.jpg"
|
src = "tests/test-images/wedding.jpg"
|
||||||
|
src2 = os.path.join(temp_dir.name, "wedding_src.jpg")
|
||||||
dest = os.path.join(temp_dir.name, "wedding.jpg")
|
dest = os.path.join(temp_dir.name, "wedding.jpg")
|
||||||
FileUtil.hardlink(src, dest)
|
FileUtil.copy(src, src2)
|
||||||
|
FileUtil.hardlink(src2, dest)
|
||||||
assert os.path.isfile(dest)
|
assert os.path.isfile(dest)
|
||||||
assert os.path.samefile(src, dest)
|
assert os.path.samefile(src2, dest)
|
||||||
|
|
||||||
|
|
||||||
def test_unlink_file():
|
def test_unlink_file():
|
||||||
|
|||||||
@ -14,9 +14,9 @@ import pytest
|
|||||||
import osxphotos
|
import osxphotos
|
||||||
from osxphotos._constants import _UNKNOWN_PERSON
|
from osxphotos._constants import _UNKNOWN_PERSON
|
||||||
from osxphotos.photoexporter import PhotoExporter
|
from osxphotos.photoexporter import PhotoExporter
|
||||||
from osxphotos.utils import get_macos_version
|
from osxphotos.utils import is_macos, get_macos_version
|
||||||
|
|
||||||
OS_VERSION = get_macos_version()
|
OS_VERSION = get_macos_version() if is_macos else (None, None, None)
|
||||||
# SKIP_TEST = "OSXPHOTOS_TEST_EXPORT" not in os.environ or OS_VERSION[1] != "17"
|
# SKIP_TEST = "OSXPHOTOS_TEST_EXPORT" not in os.environ or OS_VERSION[1] != "17"
|
||||||
SKIP_TEST = True # don't run any of the local library tests
|
SKIP_TEST = True # don't run any of the local library tests
|
||||||
PHOTOS_DB_LOCAL = os.path.expanduser("~/Pictures/Photos Library.photoslibrary")
|
PHOTOS_DB_LOCAL = os.path.expanduser("~/Pictures/Photos Library.photoslibrary")
|
||||||
|
|||||||
@ -6,15 +6,19 @@ import tempfile
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from osxphotos.photokit import (
|
from osxphotos.utils import is_macos
|
||||||
LivePhotoAsset,
|
if is_macos:
|
||||||
PhotoAsset,
|
from osxphotos.photokit import (
|
||||||
PhotoLibrary,
|
LivePhotoAsset,
|
||||||
VideoAsset,
|
PhotoAsset,
|
||||||
PHOTOS_VERSION_CURRENT,
|
PhotoLibrary,
|
||||||
PHOTOS_VERSION_ORIGINAL,
|
VideoAsset,
|
||||||
PHOTOS_VERSION_UNADJUSTED,
|
PHOTOS_VERSION_CURRENT,
|
||||||
)
|
PHOTOS_VERSION_ORIGINAL,
|
||||||
|
PHOTOS_VERSION_UNADJUSTED,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
pytest.skip(allow_module_level=True)
|
||||||
|
|
||||||
skip_test = "OSXPHOTOS_TEST_EXPORT" not in os.environ
|
skip_test = "OSXPHOTOS_TEST_EXPORT" not in os.environ
|
||||||
pytestmark = pytest.mark.skipif(
|
pytestmark = pytest.mark.skipif(
|
||||||
|
|||||||
@ -15,7 +15,9 @@ from osxphotos.phototemplate import (
|
|||||||
RenderOptions,
|
RenderOptions,
|
||||||
)
|
)
|
||||||
from osxphotos.photoinfo import PhotoInfoNone
|
from osxphotos.photoinfo import PhotoInfoNone
|
||||||
|
from osxphotos.utils import is_macos
|
||||||
from .photoinfo_mock import PhotoInfoMock
|
from .photoinfo_mock import PhotoInfoMock
|
||||||
|
from .locale_util import setlocale
|
||||||
|
|
||||||
try:
|
try:
|
||||||
exiftool = get_exiftool_path()
|
exiftool = get_exiftool_path()
|
||||||
@ -549,6 +551,8 @@ def test_lookup_multi(photosdb_places):
|
|||||||
lookup_str = re.match(r"\{([^\\,}]+)\}", subst).group(1)
|
lookup_str = re.match(r"\{([^\\,}]+)\}", subst).group(1)
|
||||||
if subst in ["{exiftool}", "{photo}", "{function}", "{format}"]:
|
if subst in ["{exiftool}", "{photo}", "{function}", "{format}"]:
|
||||||
continue
|
continue
|
||||||
|
if subst == "{detected_text}" and not is_macos:
|
||||||
|
continue
|
||||||
lookup = template.get_template_value_multi(
|
lookup = template.get_template_value_multi(
|
||||||
lookup_str,
|
lookup_str,
|
||||||
path_sep=os.path.sep,
|
path_sep=os.path.sep,
|
||||||
@ -562,7 +566,7 @@ def test_subst(photosdb_places):
|
|||||||
"""Test that substitutions are correct"""
|
"""Test that substitutions are correct"""
|
||||||
import locale
|
import locale
|
||||||
|
|
||||||
locale.setlocale(locale.LC_ALL, "en_US")
|
setlocale(locale.LC_ALL, "en_US")
|
||||||
photo = photosdb_places.photos(uuid=[UUID_DICT["place_dc"]])[0]
|
photo = photosdb_places.photos(uuid=[UUID_DICT["place_dc"]])[0]
|
||||||
|
|
||||||
for template in TEMPLATE_VALUES:
|
for template in TEMPLATE_VALUES:
|
||||||
@ -574,7 +578,7 @@ def test_subst_date_modified(photosdb_places):
|
|||||||
"""Test that substitutions are correct for date modified"""
|
"""Test that substitutions are correct for date modified"""
|
||||||
import locale
|
import locale
|
||||||
|
|
||||||
locale.setlocale(locale.LC_ALL, "en_US")
|
setlocale(locale.LC_ALL, "en_US")
|
||||||
photo = photosdb_places.photos(uuid=[UUID_DICT["date_modified"]])[0]
|
photo = photosdb_places.photos(uuid=[UUID_DICT["date_modified"]])[0]
|
||||||
|
|
||||||
for template in TEMPLATE_VALUES_DATE_MODIFIED:
|
for template in TEMPLATE_VALUES_DATE_MODIFIED:
|
||||||
@ -586,7 +590,7 @@ def test_subst_date_not_modified(photosdb_places):
|
|||||||
"""Test that substitutions are correct for date modified when photo isn't modified"""
|
"""Test that substitutions are correct for date modified when photo isn't modified"""
|
||||||
import locale
|
import locale
|
||||||
|
|
||||||
locale.setlocale(locale.LC_ALL, "en_US")
|
setlocale(locale.LC_ALL, "en_US")
|
||||||
photo = photosdb_places.photos(uuid=[UUID_DICT["date_not_modified"]])[0]
|
photo = photosdb_places.photos(uuid=[UUID_DICT["date_not_modified"]])[0]
|
||||||
|
|
||||||
for template in TEMPLATE_VALUES_DATE_NOT_MODIFIED:
|
for template in TEMPLATE_VALUES_DATE_NOT_MODIFIED:
|
||||||
@ -600,7 +604,7 @@ def test_subst_locale_1(photosdb_places):
|
|||||||
|
|
||||||
# osxphotos.template sets local on load so set the environment first
|
# osxphotos.template sets local on load so set the environment first
|
||||||
# set locale to DE
|
# set locale to DE
|
||||||
locale.setlocale(locale.LC_ALL, "de_DE.UTF-8")
|
setlocale(locale.LC_ALL, "de_DE.UTF-8")
|
||||||
|
|
||||||
photo = photosdb_places.photos(uuid=[UUID_DICT["place_dc"]])[0]
|
photo = photosdb_places.photos(uuid=[UUID_DICT["place_dc"]])[0]
|
||||||
|
|
||||||
@ -614,6 +618,9 @@ def test_subst_locale_2(photosdb_places):
|
|||||||
import locale
|
import locale
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
# Check if locale is available
|
||||||
|
setlocale(locale.LC_ALL, "de_DE.UTF-8")
|
||||||
|
|
||||||
# osxphotos.template sets local on load so set the environment first
|
# osxphotos.template sets local on load so set the environment first
|
||||||
os.environ["LANG"] = "de_DE.UTF-8"
|
os.environ["LANG"] = "de_DE.UTF-8"
|
||||||
os.environ["LC_COLLATE"] = "de_DE.UTF-8"
|
os.environ["LC_COLLATE"] = "de_DE.UTF-8"
|
||||||
@ -634,7 +641,7 @@ def test_subst_default_val(photosdb_places):
|
|||||||
"""Test substitution with default value specified"""
|
"""Test substitution with default value specified"""
|
||||||
import locale
|
import locale
|
||||||
|
|
||||||
locale.setlocale(locale.LC_ALL, "en_US")
|
setlocale(locale.LC_ALL, "en_US")
|
||||||
photo = photosdb_places.photos(uuid=[UUID_DICT["place_dc"]])[0]
|
photo = photosdb_places.photos(uuid=[UUID_DICT["place_dc"]])[0]
|
||||||
|
|
||||||
template = "{place.name.area_of_interest,UNKNOWN}"
|
template = "{place.name.area_of_interest,UNKNOWN}"
|
||||||
@ -646,7 +653,7 @@ def test_subst_default_val_2(photosdb_places):
|
|||||||
"""Test substitution with ',' but no default value"""
|
"""Test substitution with ',' but no default value"""
|
||||||
import locale
|
import locale
|
||||||
|
|
||||||
locale.setlocale(locale.LC_ALL, "en_US")
|
setlocale(locale.LC_ALL, "en_US")
|
||||||
photo = photosdb_places.photos(uuid=[UUID_DICT["place_dc"]])[0]
|
photo = photosdb_places.photos(uuid=[UUID_DICT["place_dc"]])[0]
|
||||||
|
|
||||||
template = "{place.name.area_of_interest,}"
|
template = "{place.name.area_of_interest,}"
|
||||||
@ -658,7 +665,7 @@ def test_subst_unknown_val(photosdb_places):
|
|||||||
"""Test substitution with unknown value specified"""
|
"""Test substitution with unknown value specified"""
|
||||||
import locale
|
import locale
|
||||||
|
|
||||||
locale.setlocale(locale.LC_ALL, "en_US")
|
setlocale(locale.LC_ALL, "en_US")
|
||||||
photo = photosdb_places.photos(uuid=[UUID_DICT["place_dc"]])[0]
|
photo = photosdb_places.photos(uuid=[UUID_DICT["place_dc"]])[0]
|
||||||
|
|
||||||
template = "{created.year}/{foo}"
|
template = "{created.year}/{foo}"
|
||||||
@ -682,7 +689,7 @@ def test_subst_unknown_val_with_default(photosdb_places):
|
|||||||
"""Test substitution with unknown value specified"""
|
"""Test substitution with unknown value specified"""
|
||||||
import locale
|
import locale
|
||||||
|
|
||||||
locale.setlocale(locale.LC_ALL, "en_US")
|
setlocale(locale.LC_ALL, "en_US")
|
||||||
photo = photosdb_places.photos(uuid=[UUID_DICT["place_dc"]])[0]
|
photo = photosdb_places.photos(uuid=[UUID_DICT["place_dc"]])[0]
|
||||||
|
|
||||||
template = "{created.year}/{foo,bar}"
|
template = "{created.year}/{foo,bar}"
|
||||||
@ -929,7 +936,7 @@ def test_subst_strftime(photosdb_places):
|
|||||||
"""Test that strftime substitutions are correct"""
|
"""Test that strftime substitutions are correct"""
|
||||||
import locale
|
import locale
|
||||||
|
|
||||||
locale.setlocale(locale.LC_ALL, "en_US")
|
setlocale(locale.LC_ALL, "en_US")
|
||||||
photo = photosdb_places.photos(uuid=[UUID_DICT["place_dc"]])[0]
|
photo = photosdb_places.photos(uuid=[UUID_DICT["place_dc"]])[0]
|
||||||
|
|
||||||
rendered, unmatched = photo.render_template("{created.strftime,%Y-%m-%d-%H%M%S}")
|
rendered, unmatched = photo.render_template("{created.strftime,%Y-%m-%d-%H%M%S}")
|
||||||
@ -1287,6 +1294,7 @@ def test_album_seq(photosdb):
|
|||||||
assert rendered[0] == value
|
assert rendered[0] == value
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(not is_macos, reason="Only works on macOS")
|
||||||
def test_detected_text(photosdb):
|
def test_detected_text(photosdb):
|
||||||
"""Test {detected_text} template"""
|
"""Test {detected_text} template"""
|
||||||
photo = photosdb.get_photo(UUID_DETECTED_TEXT)
|
photo = photosdb.get_photo(UUID_DETECTED_TEXT)
|
||||||
|
|||||||
@ -5,6 +5,8 @@ import pytest
|
|||||||
import osxphotos
|
import osxphotos
|
||||||
from osxphotos.phototemplate import RenderOptions
|
from osxphotos.phototemplate import RenderOptions
|
||||||
|
|
||||||
|
from .locale_util import setlocale
|
||||||
|
|
||||||
PHOTOS_DB_PLACES = (
|
PHOTOS_DB_PLACES = (
|
||||||
"./tests/Test-Places-Catalina-10_15_1.photoslibrary/database/photos.db"
|
"./tests/Test-Places-Catalina-10_15_1.photoslibrary/database/photos.db"
|
||||||
)
|
)
|
||||||
@ -48,7 +50,7 @@ def test_subst_today(photosdb):
|
|||||||
"""Test that substitutions are correct for {today.x}"""
|
"""Test that substitutions are correct for {today.x}"""
|
||||||
import locale
|
import locale
|
||||||
|
|
||||||
locale.setlocale(locale.LC_ALL, "en_US")
|
setlocale(locale.LC_ALL, "en_US")
|
||||||
photo = photosdb.photos(uuid=[UUID_DICT["place_dc"]])[0]
|
photo = photosdb.photos(uuid=[UUID_DICT["place_dc"]])[0]
|
||||||
|
|
||||||
photo_template = osxphotos.PhotoTemplate(photo)
|
photo_template = osxphotos.PhotoTemplate(photo)
|
||||||
@ -64,7 +66,7 @@ def test_subst_strftime_today(photosdb):
|
|||||||
"""Test that strftime substitutions are correct for {today.strftime}"""
|
"""Test that strftime substitutions are correct for {today.strftime}"""
|
||||||
import locale
|
import locale
|
||||||
|
|
||||||
locale.setlocale(locale.LC_ALL, "en_US")
|
setlocale(locale.LC_ALL, "en_US")
|
||||||
photo = photosdb.photos(uuid=[UUID_DICT["place_dc"]])[0]
|
photo = photosdb.photos(uuid=[UUID_DICT["place_dc"]])[0]
|
||||||
|
|
||||||
photo_template = osxphotos.PhotoTemplate(photo)
|
photo_template = osxphotos.PhotoTemplate(photo)
|
||||||
|
|||||||
@ -8,6 +8,7 @@ from osxphotos.uti import (
|
|||||||
get_preferred_uti_extension,
|
get_preferred_uti_extension,
|
||||||
get_uti_for_extension,
|
get_uti_for_extension,
|
||||||
)
|
)
|
||||||
|
from osxphotos.utils import is_macos
|
||||||
|
|
||||||
EXT_DICT = {"heic": "public.heic", "jpg": "public.jpeg", ".jpg": "public.jpeg"}
|
EXT_DICT = {"heic": "public.heic", "jpg": "public.jpeg", ".jpg": "public.jpeg"}
|
||||||
UTI_DICT = {"public.heic": "heic", "public.jpeg": "jpeg"}
|
UTI_DICT = {"public.heic": "heic", "public.jpeg": "jpeg"}
|
||||||
@ -43,12 +44,14 @@ def test_get_uti_for_extension_no_objc():
|
|||||||
osxphotos.uti.OS_VER = OLD_VER
|
osxphotos.uti.OS_VER = OLD_VER
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(not is_macos, reason="Only works on macOS")
|
||||||
def test_get_uti_from_mdls():
|
def test_get_uti_from_mdls():
|
||||||
"""get _get_uti_from_mdls"""
|
"""get _get_uti_from_mdls"""
|
||||||
for ext in EXT_DICT:
|
for ext in EXT_DICT:
|
||||||
assert _get_uti_from_mdls(ext) == EXT_DICT[ext]
|
assert _get_uti_from_mdls(ext) == EXT_DICT[ext]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(not is_macos, reason="Only works on macOS")
|
||||||
def test_get_uti_not_in_dict():
|
def test_get_uti_not_in_dict():
|
||||||
"""get UTI when objc is not available and it's not in the EXT_UTI_DICT"""
|
"""get UTI when objc is not available and it's not in the EXT_UTI_DICT"""
|
||||||
# monkey patch the EXT_UTI_DICT
|
# monkey patch the EXT_UTI_DICT
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user