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 .photoexporter import ExportOptions, ExportResults, PhotoExporter
|
||||
from .photoinfo import PhotoInfo
|
||||
from .photosalbum import PhotosAlbum, PhotosAlbumPhotoScript
|
||||
from .photosdb import PhotosDB
|
||||
from .photosdb._photosdb_process_comments import CommentInfo, LikeInfo
|
||||
from .phototables import PhotoTables
|
||||
@ -25,6 +24,10 @@ from .placeinfo import PlaceInfo
|
||||
from .queryoptions import QueryOptions
|
||||
from .scoreinfo import ScoreInfo
|
||||
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
|
||||
logging.basicConfig(
|
||||
|
||||
@ -5,6 +5,7 @@ import sys
|
||||
from rich import print
|
||||
from rich.traceback import install as install_traceback
|
||||
|
||||
from osxphotos.utils import is_macos
|
||||
from osxphotos.debug import (
|
||||
debug_breakpoint,
|
||||
debug_watch,
|
||||
@ -44,9 +45,7 @@ if args.get("--debug", False):
|
||||
print("Debugging enabled", file=sys.stderr)
|
||||
|
||||
from .about import about
|
||||
from .add_locations import add_locations
|
||||
from .albums import albums
|
||||
from .batch_edit import batch_edit
|
||||
from .cli import cli_main
|
||||
from .cli_commands import (
|
||||
abort,
|
||||
@ -67,7 +66,6 @@ from .export import export
|
||||
from .exportdb import exportdb
|
||||
from .grep import grep
|
||||
from .help import help
|
||||
from .import_cli import import_cli
|
||||
from .info import info
|
||||
from .install_uninstall_run import install, run, uninstall
|
||||
from .keywords import keywords
|
||||
@ -76,19 +74,24 @@ from .labels import labels
|
||||
from .list import _list_libraries, list_libraries
|
||||
from .orphans import orphans
|
||||
from .persons import persons
|
||||
from .photo_inspect import photo_inspect
|
||||
from .places import places
|
||||
from .query import query
|
||||
from .repl import repl
|
||||
from .show_command import show
|
||||
from .snap_diff import diff, snap
|
||||
from .sync import sync
|
||||
from .theme import theme
|
||||
from .timewarp import timewarp
|
||||
from .tutorial import tutorial
|
||||
from .uuid import uuid
|
||||
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()
|
||||
|
||||
__all__ = [
|
||||
|
||||
@ -5,11 +5,10 @@ from __future__ import annotations
|
||||
import datetime
|
||||
|
||||
import click
|
||||
import photoscript
|
||||
|
||||
import osxphotos
|
||||
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 .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 .verbose import get_verbose_console, verbose_print
|
||||
|
||||
assert_macos()
|
||||
|
||||
import photoscript
|
||||
|
||||
|
||||
def get_location(
|
||||
photos: list[osxphotos.PhotoInfo], idx: int, window: datetime.timedelta
|
||||
|
||||
@ -9,11 +9,15 @@ import json
|
||||
import sys
|
||||
|
||||
import click
|
||||
import photoscript
|
||||
|
||||
import osxphotos
|
||||
from osxphotos.phototemplate import RenderOptions
|
||||
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 .kvstore import kvstore
|
||||
|
||||
@ -9,11 +9,10 @@ import click
|
||||
|
||||
from osxphotos._constants import PROFILE_SORT_KEYS
|
||||
from osxphotos._version import __version__
|
||||
from osxphotos.utils import is_macos
|
||||
|
||||
from .about import about
|
||||
from .add_locations import add_locations
|
||||
from .albums import albums
|
||||
from .batch_edit import batch_edit
|
||||
from .cli_params import DB_OPTION, DEBUG_OPTIONS, JSON_OPTION, VERSION_OPTION
|
||||
from .common import OSXPHOTOS_HIDDEN
|
||||
from .debug_dump import debug_dump
|
||||
@ -24,7 +23,6 @@ from .export import export
|
||||
from .exportdb import exportdb
|
||||
from .grep import grep
|
||||
from .help import help
|
||||
from .import_cli import import_cli
|
||||
from .info import info
|
||||
from .install_uninstall_run import install, run, uninstall
|
||||
from .keywords import keywords
|
||||
@ -32,19 +30,24 @@ from .labels import labels
|
||||
from .list import list_libraries
|
||||
from .orphans import orphans
|
||||
from .persons import persons
|
||||
from .photo_inspect import photo_inspect
|
||||
from .places import places
|
||||
from .query import query
|
||||
from .repl import repl
|
||||
from .show_command import show
|
||||
from .snap_diff import diff, snap
|
||||
from .sync import sync
|
||||
from .theme import theme
|
||||
from .timewarp import timewarp
|
||||
from .tutorial import tutorial
|
||||
from .uuid import uuid
|
||||
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
|
||||
class CLI_Obj:
|
||||
@ -106,11 +109,9 @@ def cli_main(ctx, db, json_, profile, profile_sort, **kwargs):
|
||||
|
||||
|
||||
# install CLI commands
|
||||
for command in [
|
||||
commands = [
|
||||
about,
|
||||
add_locations,
|
||||
albums,
|
||||
batch_edit,
|
||||
debug_dump,
|
||||
diff,
|
||||
docs_command,
|
||||
@ -120,7 +121,6 @@ for command in [
|
||||
exportdb,
|
||||
grep,
|
||||
help,
|
||||
import_cli,
|
||||
info,
|
||||
install,
|
||||
keywords,
|
||||
@ -128,19 +128,28 @@ for command in [
|
||||
list_libraries,
|
||||
orphans,
|
||||
persons,
|
||||
photo_inspect,
|
||||
places,
|
||||
query,
|
||||
repl,
|
||||
run,
|
||||
show,
|
||||
snap,
|
||||
sync,
|
||||
theme,
|
||||
timewarp,
|
||||
tutorial,
|
||||
uninstall,
|
||||
uuid,
|
||||
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)
|
||||
|
||||
@ -8,6 +8,8 @@ from typing import Any, Callable
|
||||
import click
|
||||
import contextlib
|
||||
from textwrap import dedent
|
||||
|
||||
from ..utils import is_macos
|
||||
from .common import OSXPHOTOS_HIDDEN, print_version
|
||||
from .param_types import *
|
||||
|
||||
@ -642,6 +644,9 @@ _QUERY_PARAMETERS_DICT = {
|
||||
),
|
||||
}
|
||||
|
||||
if not is_macos:
|
||||
del _QUERY_PARAMETERS_DICT["--selected"]
|
||||
|
||||
|
||||
def QUERY_OPTIONS(
|
||||
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
|
||||
import Foundation
|
||||
from osxphotos.utils import is_macos
|
||||
|
||||
|
||||
def theme():
|
||||
if is_macos:
|
||||
import objc
|
||||
import Foundation
|
||||
|
||||
|
||||
def theme():
|
||||
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_dark_mode():
|
||||
def is_dark_mode():
|
||||
return theme() == "dark"
|
||||
|
||||
|
||||
def is_light_mode():
|
||||
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
|
||||
|
||||
import click
|
||||
from osxmetadata import (
|
||||
MDITEM_ATTRIBUTE_DATA,
|
||||
MDITEM_ATTRIBUTE_SHORT_NAMES,
|
||||
OSXMetaData,
|
||||
Tag,
|
||||
)
|
||||
from osxmetadata.constants import _TAGS_NAMES
|
||||
|
||||
import osxphotos
|
||||
from osxphotos._constants import (
|
||||
@ -47,26 +40,37 @@ from osxphotos.datetime_formatter import DateTimeFormatter
|
||||
from osxphotos.debug import is_debug
|
||||
from osxphotos.exiftool import get_exiftool_path
|
||||
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.photoexporter import ExportOptions, ExportResults, PhotoExporter
|
||||
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.queryoptions import load_uuid_from_file, query_options_from_kwargs
|
||||
from osxphotos.uti import get_preferred_uti_extension
|
||||
from osxphotos.utils import (
|
||||
format_sec_to_hhmmss,
|
||||
get_macos_version,
|
||||
is_macos,
|
||||
normalize_fs_path,
|
||||
pluralize,
|
||||
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_params import (
|
||||
DB_ARGUMENT,
|
||||
@ -851,7 +855,6 @@ def export(
|
||||
retry,
|
||||
save_config,
|
||||
screenshot,
|
||||
selected,
|
||||
selfie,
|
||||
shared,
|
||||
sidecar,
|
||||
@ -883,6 +886,7 @@ def export(
|
||||
verbose_flag,
|
||||
xattr_template,
|
||||
year,
|
||||
selected=False, # Isn't provided on unsupported platforms
|
||||
# debug, # debug, watch, breakpoint handled in cli/__init__.py
|
||||
# watch,
|
||||
# breakpoint,
|
||||
@ -1111,7 +1115,10 @@ def export(
|
||||
|
||||
verbose(f"osxphotos version: {__version__}")
|
||||
verbose(f"Python version: {sys.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}")
|
||||
|
||||
# validate options
|
||||
@ -1325,7 +1332,7 @@ def export(
|
||||
if ramdb
|
||||
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 export_db.was_created:
|
||||
@ -1713,7 +1720,7 @@ def export_photo(
|
||||
keyword_template=None,
|
||||
description_template=None,
|
||||
export_db=None,
|
||||
fileutil=FileUtil,
|
||||
fileutil=FileUtilShUtil,
|
||||
dry_run=None,
|
||||
touch_file=None,
|
||||
edited_suffix="_edited",
|
||||
|
||||
@ -5,7 +5,6 @@ import re
|
||||
import typing as t
|
||||
|
||||
import click
|
||||
from osxmetadata import MDITEM_ATTRIBUTE_DATA, MDITEM_ATTRIBUTE_SHORT_NAMES
|
||||
from rich.console import Console
|
||||
from rich.markdown import Markdown
|
||||
|
||||
@ -21,6 +20,10 @@ from osxphotos.phototemplate import (
|
||||
TEMPLATE_SUBSTITUTIONS_PATHLIB,
|
||||
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 .color_themes import get_theme
|
||||
@ -249,26 +252,28 @@ class ExportCommand(click.Command):
|
||||
+ f"rebuilding the '{OSXPHOTOS_EXPORT_DB}' database."
|
||||
)
|
||||
formatter.write("\n")
|
||||
|
||||
if is_macos:
|
||||
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.
|
||||
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':
|
||||
The following attributes may be used with '--xattr-template':
|
||||
|
||||
"""
|
||||
)
|
||||
@ -311,6 +316,7 @@ The following attributes may be used with '--xattr-template':
|
||||
formatter = click.HelpFormatter(width=HELP_WIDTH)
|
||||
|
||||
formatter.write("\n")
|
||||
|
||||
formatter.write_text(
|
||||
"With the --directory and --filename options you may specify a template for the "
|
||||
+ "export directory or filename, respectively. "
|
||||
|
||||
@ -20,7 +20,6 @@ from textwrap import dedent
|
||||
from typing import Callable, Dict, List, Optional, Tuple, Union
|
||||
|
||||
import click
|
||||
from photoscript import Photo, PhotosLibrary
|
||||
from rich.console import Console
|
||||
from rich.markdown import Markdown
|
||||
from strpdatetime import strpdatetime
|
||||
@ -43,7 +42,11 @@ from osxphotos.photoinfo import PhotoInfoNone
|
||||
from osxphotos.photosalbum import PhotosAlbumPhotoScript
|
||||
from osxphotos.phototemplate import PhotoTemplate, RenderOptions
|
||||
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 .click_rich_echo import rich_click_echo, rich_echo_error
|
||||
|
||||
@ -13,8 +13,6 @@ from typing import Generator, List, Optional, Tuple
|
||||
|
||||
import bitmath
|
||||
import click
|
||||
from applescript import ScriptError
|
||||
from photoscript import PhotosLibrary
|
||||
from rich.console import Console
|
||||
from rich.layout import Layout
|
||||
from rich.live import Live
|
||||
@ -23,8 +21,14 @@ from rich.panel import Panel
|
||||
from osxphotos import PhotoInfo, PhotosDB
|
||||
from osxphotos._constants import _UNKNOWN_PERSON, search_category_factory
|
||||
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.utils import dd_to_dms_str
|
||||
|
||||
from .cli_params import DB_OPTION, THEME_OPTION
|
||||
from .color_themes import get_theme
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
"""query command for osxphotos CLI"""
|
||||
|
||||
import sys
|
||||
import click
|
||||
|
||||
import osxphotos
|
||||
@ -9,9 +10,12 @@ from osxphotos.cli.click_rich_echo import (
|
||||
set_rich_theme,
|
||||
)
|
||||
from osxphotos.debug import set_debug
|
||||
from osxphotos.photosalbum import PhotosAlbum
|
||||
from osxphotos.phototemplate import RenderOptions
|
||||
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 (
|
||||
DB_ARGUMENT,
|
||||
@ -20,6 +24,7 @@ from .cli_params import (
|
||||
FIELD_OPTION,
|
||||
JSON_OPTION,
|
||||
QUERY_OPTIONS,
|
||||
make_click_option_decorator,
|
||||
)
|
||||
from .color_themes import get_default_theme
|
||||
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 .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()
|
||||
@DB_OPTION
|
||||
@JSON_OPTION
|
||||
@QUERY_OPTIONS
|
||||
@DELETED_OPTIONS
|
||||
@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.",
|
||||
)
|
||||
@MACOS_OPTIONS
|
||||
@click.option(
|
||||
"--quiet",
|
||||
is_flag=True,
|
||||
@ -70,8 +78,8 @@ def query(
|
||||
json_,
|
||||
print_template,
|
||||
quiet,
|
||||
add_to_album,
|
||||
photos_library,
|
||||
add_to_album=False,
|
||||
**kwargs,
|
||||
):
|
||||
"""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
|
||||
|
||||
if add_to_album and photos:
|
||||
assert_macos()
|
||||
|
||||
album_query = PhotosAlbum(add_to_album, verbose=None)
|
||||
photo_len = len(photos)
|
||||
photo_word = "photos" if photo_len > 1 else "photo"
|
||||
|
||||
@ -10,8 +10,6 @@ import time
|
||||
from typing import List
|
||||
|
||||
import click
|
||||
import photoscript
|
||||
from applescript import ScriptError
|
||||
from rich import pretty, print
|
||||
|
||||
import osxphotos
|
||||
@ -25,6 +23,11 @@ from osxphotos.queryoptions import (
|
||||
QueryOptions,
|
||||
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 .common import get_photos_db
|
||||
@ -55,6 +58,7 @@ def repl(ctx, cli_obj, db, emacs, beta, **kwargs):
|
||||
import logging
|
||||
|
||||
from objexplore import explore
|
||||
if is_macos:
|
||||
from photoscript import Album, Photo, PhotosLibrary
|
||||
from rich import inspect as _inspect
|
||||
|
||||
@ -194,6 +198,7 @@ def _get_selected(photosdb):
|
||||
"""get list of PhotoInfo objects for photos selected in Photos"""
|
||||
|
||||
def get_selected():
|
||||
assert_macos()
|
||||
try:
|
||||
selected = photoscript.PhotosLibrary().selection
|
||||
except ScriptError as e:
|
||||
@ -209,6 +214,7 @@ def _get_selected(photosdb):
|
||||
|
||||
|
||||
def _spotlight_photo(photo: PhotoInfo):
|
||||
assert_macos()
|
||||
photo_ = photoscript.Photo(photo.uuid)
|
||||
photo_.spotlight()
|
||||
|
||||
|
||||
@ -7,12 +7,15 @@ import click
|
||||
|
||||
from osxphotos._constants import UUID_PATTERN
|
||||
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 (
|
||||
photoscript_object_from_name,
|
||||
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_params import DB_OPTION
|
||||
|
||||
@ -9,7 +9,6 @@ import pathlib
|
||||
from typing import Any, Callable, Literal
|
||||
|
||||
import click
|
||||
import photoscript
|
||||
|
||||
from osxphotos import PhotoInfo, PhotosDB, __version__
|
||||
from osxphotos.photoinfo import PhotoInfoNone
|
||||
@ -22,7 +21,11 @@ from osxphotos.queryoptions import (
|
||||
query_options_from_kwargs,
|
||||
)
|
||||
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 (
|
||||
DB_OPTION,
|
||||
|
||||
@ -7,7 +7,6 @@ from functools import partial
|
||||
from textwrap import dedent
|
||||
|
||||
import click
|
||||
from photoscript import PhotosLibrary
|
||||
from rich.console import Console
|
||||
|
||||
from osxphotos._constants import APP_NAME
|
||||
@ -25,9 +24,13 @@ from osxphotos.photodates import (
|
||||
update_photo_from_function,
|
||||
update_photo_time_for_new_timezone,
|
||||
)
|
||||
from osxphotos.photosalbum import PhotosAlbumPhotoScript
|
||||
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 .click_rich_echo import rich_click_echo as echo
|
||||
|
||||
@ -1,6 +1,11 @@
|
||||
"""uuid command for osxphotos CLI"""
|
||||
|
||||
import click
|
||||
|
||||
from osxphotos.utils import assert_macos
|
||||
|
||||
assert_macos()
|
||||
|
||||
import photoscript
|
||||
|
||||
|
||||
|
||||
@ -5,12 +5,15 @@ from typing import Callable, List, Optional, Tuple
|
||||
|
||||
from osxphotos import PhotosDB
|
||||
from osxphotos.exiftool import ExifTool
|
||||
from photoscript import Photo
|
||||
|
||||
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 .phototz import PhotoTimeZone
|
||||
from .utils import noop
|
||||
|
||||
ExifDiff = namedtuple(
|
||||
"ExifDiff",
|
||||
|
||||
@ -9,9 +9,11 @@ import typing as t
|
||||
from abc import ABC, abstractmethod
|
||||
from tempfile import TemporaryDirectory
|
||||
|
||||
import Foundation
|
||||
|
||||
from .imageconverter import ImageConverter
|
||||
from .utils import is_macos, normalize_fs_path
|
||||
|
||||
if is_macos:
|
||||
import Foundation
|
||||
|
||||
__all__ = ["FileUtilABC", "FileUtilMacOS", "FileUtilShUtil", "FileUtil", "FileUtilNoOp"]
|
||||
|
||||
@ -90,6 +92,9 @@ class FileUtilMacOS(FileUtilABC):
|
||||
if src is None or dest is None:
|
||||
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):
|
||||
raise FileNotFoundError("src file does not appear to exist", src)
|
||||
|
||||
@ -115,6 +120,9 @@ class FileUtilMacOS(FileUtilABC):
|
||||
OSError if copy fails
|
||||
TypeError if either path is None
|
||||
"""
|
||||
src = normalize_fs_path(src)
|
||||
dest = normalize_fs_path(dest)
|
||||
|
||||
if not isinstance(src, pathlib.Path):
|
||||
src = pathlib.Path(src)
|
||||
|
||||
@ -135,6 +143,7 @@ class FileUtilMacOS(FileUtilABC):
|
||||
@classmethod
|
||||
def unlink(cls, filepath):
|
||||
"""unlink filepath; if it's pathlib.Path, use Path.unlink, otherwise use os.unlink"""
|
||||
filepath = normalize_fs_path(filepath)
|
||||
if isinstance(filepath, pathlib.Path):
|
||||
filepath.unlink()
|
||||
else:
|
||||
@ -143,6 +152,7 @@ class FileUtilMacOS(FileUtilABC):
|
||||
@classmethod
|
||||
def rmdir(cls, dirpath):
|
||||
"""remove directory filepath; dirpath must be empty"""
|
||||
dirpath = normalize_fs_path(dirpath)
|
||||
if isinstance(dirpath, pathlib.Path):
|
||||
dirpath.rmdir()
|
||||
else:
|
||||
@ -151,6 +161,7 @@ class FileUtilMacOS(FileUtilABC):
|
||||
@classmethod
|
||||
def utime(cls, path, times):
|
||||
"""Set the access and modified time of path."""
|
||||
path = normalize_fs_path(path)
|
||||
os.utime(path, times=times)
|
||||
|
||||
@classmethod
|
||||
@ -166,6 +177,9 @@ class FileUtilMacOS(FileUtilABC):
|
||||
Does not do a byte-by-byte comparison.
|
||||
"""
|
||||
|
||||
f1 = normalize_fs_path(f1)
|
||||
f2 = normalize_fs_path(f2)
|
||||
|
||||
s1 = cls._sig(os.stat(f1))
|
||||
if mtime1 is not None:
|
||||
s1 = (s1[0], s1[1], int(mtime1))
|
||||
@ -188,6 +202,7 @@ class FileUtilMacOS(FileUtilABC):
|
||||
if not s2:
|
||||
return False
|
||||
|
||||
f1 = normalize_fs_path(f1)
|
||||
s1 = cls._sig(os.stat(f1))
|
||||
if s1[0] != stat.S_IFREG or s2[0] != stat.S_IFREG:
|
||||
return False
|
||||
@ -196,6 +211,7 @@ class FileUtilMacOS(FileUtilABC):
|
||||
@classmethod
|
||||
def file_sig(cls, f1):
|
||||
"""return os.stat signature for file f1 as tuple of (mode, size, mtime)"""
|
||||
f1 = normalize_fs_path(f1)
|
||||
return cls._sig(os.stat(f1))
|
||||
|
||||
@classmethod
|
||||
@ -210,6 +226,8 @@ class FileUtilMacOS(FileUtilABC):
|
||||
Returns:
|
||||
True if success, otherwise False
|
||||
"""
|
||||
src_file = normalize_fs_path(src_file)
|
||||
dest_file = normalize_fs_path(dest_file)
|
||||
converter = ImageConverter()
|
||||
return converter.write_jpeg(
|
||||
src_file, dest_file, compression_quality=compression_quality
|
||||
@ -227,6 +245,8 @@ class FileUtilMacOS(FileUtilABC):
|
||||
Name of renamed file (dest)
|
||||
|
||||
"""
|
||||
src = normalize_fs_path(src)
|
||||
dest = normalize_fs_path(dest)
|
||||
os.rename(str(src), str(dest))
|
||||
return dest
|
||||
|
||||
@ -271,6 +291,9 @@ class FileUtilShUtil(FileUtilMacOS):
|
||||
OSError if copy fails
|
||||
TypeError if either path is None
|
||||
"""
|
||||
src = normalize_fs_path(src)
|
||||
dest = normalize_fs_path(dest)
|
||||
|
||||
if not isinstance(src, pathlib.Path):
|
||||
src = pathlib.Path(src)
|
||||
|
||||
@ -288,7 +311,7 @@ class FileUtilShUtil(FileUtilMacOS):
|
||||
return True
|
||||
|
||||
|
||||
class FileUtil(FileUtilMacOS):
|
||||
class FileUtil(FileUtilShUtil):
|
||||
"""Various file utilities"""
|
||||
|
||||
pass
|
||||
|
||||
@ -5,16 +5,20 @@
|
||||
# reference: https://stackoverflow.com/questions/59330149/coreimage-ciimage-write-jpg-is-shifting-colors-macos/59334308#59334308
|
||||
|
||||
import pathlib
|
||||
|
||||
import objc
|
||||
import Metal
|
||||
import Quartz
|
||||
from Cocoa import NSURL
|
||||
from Foundation import NSDictionary
|
||||
import sys
|
||||
|
||||
# needed to capture system-level stderr
|
||||
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"]
|
||||
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
from osxphotos.utils import normalize_unicode
|
||||
|
||||
from ._constants import MAX_DIRNAME_LEN, MAX_FILENAME_LEN
|
||||
|
||||
__all__ = [
|
||||
@ -24,7 +37,7 @@ def is_valid_filepath(filepath):
|
||||
|
||||
|
||||
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:
|
||||
filename: str, filename to sanitze
|
||||
@ -35,6 +48,7 @@ def sanitize_filename(filename, replacement=":"):
|
||||
"""
|
||||
|
||||
if filename:
|
||||
filename = normalize_unicode(filename)
|
||||
filename = filename.replace("/", replacement)
|
||||
if len(filename) > MAX_FILENAME_LEN:
|
||||
parts = filename.split(".")
|
||||
@ -54,7 +68,7 @@ def sanitize_filename(filename, 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:
|
||||
dirname: str, directory name to sanitize
|
||||
@ -69,7 +83,7 @@ def sanitize_dirname(dirname, 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:
|
||||
pathpart: str, path part to sanitize
|
||||
@ -82,6 +96,7 @@ def sanitize_pathpart(pathpart, replacement=":"):
|
||||
pathpart = (
|
||||
pathpart.replace("/", replacement) if replacement is not None else pathpart
|
||||
)
|
||||
pathpart = normalize_unicode(pathpart)
|
||||
if len(pathpart) > MAX_DIRNAME_LEN:
|
||||
drop = len(pathpart) - MAX_DIRNAME_LEN
|
||||
pathpart = pathpart[:-drop]
|
||||
|
||||
@ -7,6 +7,7 @@ import logging
|
||||
import os
|
||||
import pathlib
|
||||
import re
|
||||
import sys
|
||||
import typing as t
|
||||
from collections import namedtuple # pylint: disable=syntax-error
|
||||
from dataclasses import asdict, dataclass
|
||||
@ -14,7 +15,6 @@ from datetime import datetime
|
||||
from enum import Enum
|
||||
from types import SimpleNamespace
|
||||
|
||||
import photoscript
|
||||
from mako.template import Template
|
||||
|
||||
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 .export_db import ExportDB, ExportDBTemp
|
||||
from .fileutil import FileUtil
|
||||
from .photokit import (
|
||||
PHOTOS_VERSION_CURRENT,
|
||||
PHOTOS_VERSION_ORIGINAL,
|
||||
PHOTOS_VERSION_UNADJUSTED,
|
||||
PhotoKitFetchFailed,
|
||||
PhotoLibrary,
|
||||
)
|
||||
from .phototemplate import RenderOptions
|
||||
from .rich_utils import add_rich_markup_tag
|
||||
from .uti import get_preferred_uti_extension
|
||||
from .utils import (
|
||||
is_macos,
|
||||
hexdigest,
|
||||
increment_filename,
|
||||
increment_filename_with_count,
|
||||
lineno,
|
||||
list_directory,
|
||||
lock_filename,
|
||||
normalize_fs_path,
|
||||
unlock_filename,
|
||||
)
|
||||
|
||||
if is_macos:
|
||||
import photoscript
|
||||
|
||||
from .photokit import (
|
||||
PHOTOS_VERSION_CURRENT,
|
||||
PHOTOS_VERSION_ORIGINAL,
|
||||
PHOTOS_VERSION_UNADJUSTED,
|
||||
PhotoKitFetchFailed,
|
||||
PhotoLibrary,
|
||||
)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"ExportError",
|
||||
"ExportOptions",
|
||||
@ -721,7 +728,6 @@ class PhotoExporter:
|
||||
self, src: pathlib.Path, dest: pathlib.Path, options: ExportOptions
|
||||
) -> t.Literal[True, False]:
|
||||
"""Return True if photo should be updated, else False"""
|
||||
|
||||
# NOTE: The order of certain checks is important
|
||||
# read the comments below to understand why before changing
|
||||
|
||||
@ -1181,7 +1187,7 @@ class PhotoExporter:
|
||||
try:
|
||||
fileutil.copy(src, dest_str)
|
||||
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:
|
||||
raise ExportError(
|
||||
|
||||
@ -20,7 +20,6 @@ from types import SimpleNamespace
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
import yaml
|
||||
from osxmetadata import OSXMetaData
|
||||
|
||||
import osxphotos
|
||||
|
||||
@ -65,9 +64,12 @@ from .placeinfo import PlaceInfo4, PlaceInfo5
|
||||
from .query_builder import get_query
|
||||
from .scoreinfo import ScoreInfo
|
||||
from .searchinfo import SearchInfo
|
||||
from .text_detection import detect_text
|
||||
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"]
|
||||
|
||||
@ -1461,6 +1463,8 @@ class PhotoInfo:
|
||||
|
||||
def _detected_text(self):
|
||||
"""detect text in photo, either from cached extended attribute or by attempting text detection"""
|
||||
assert_macos()
|
||||
|
||||
path = (
|
||||
self.path_edited if self.hasadjustments and self.path_edited else self.path
|
||||
)
|
||||
|
||||
@ -19,9 +19,12 @@
|
||||
|
||||
import copy
|
||||
import pathlib
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
|
||||
assert(sys.platform == "darwin")
|
||||
|
||||
import AVFoundation
|
||||
import CoreServices
|
||||
import Foundation
|
||||
|
||||
@ -2,12 +2,15 @@
|
||||
|
||||
from typing import List, Optional
|
||||
|
||||
import photoscript
|
||||
from more_itertools import chunked
|
||||
from photoscript import Album, Folder, Photo, PhotosLibrary
|
||||
|
||||
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"]
|
||||
|
||||
|
||||
@ -4,6 +4,10 @@ from __future__ import annotations
|
||||
|
||||
import sqlite3
|
||||
|
||||
from .utils import assert_macos
|
||||
|
||||
assert_macos()
|
||||
|
||||
import photoscript
|
||||
|
||||
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
|
||||
|
||||
import bitmath
|
||||
import photoscript
|
||||
from rich import print
|
||||
|
||||
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 ..utils import (
|
||||
_check_file_exists,
|
||||
is_macos,
|
||||
get_macos_version,
|
||||
get_last_library_path,
|
||||
noop,
|
||||
@ -69,6 +69,9 @@ from ..utils import (
|
||||
)
|
||||
from .photosdb_utils import get_db_model_version, get_db_version
|
||||
|
||||
if is_macos:
|
||||
import photoscript
|
||||
|
||||
logger = logging.getLogger("osxphotos")
|
||||
|
||||
__all__ = ["PhotosDB"]
|
||||
@ -118,8 +121,8 @@ class PhotosDB:
|
||||
|
||||
# Check OS version
|
||||
system = platform.system()
|
||||
(ver, major, _) = get_macos_version()
|
||||
if system != "Darwin" or ((ver, major) not in _TESTED_OS_VERSIONS):
|
||||
(ver, major, _) = get_macos_version() if is_macos else (None, None, None)
|
||||
if system == "Darwin" and ((ver, major) not in _TESTED_OS_VERSIONS):
|
||||
logging.warning(
|
||||
f"WARNING: This module has only been tested with macOS 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 .exiftool import ExifToolCaching
|
||||
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
|
||||
|
||||
__all__ = [
|
||||
|
||||
@ -1,8 +1,13 @@
|
||||
""" Use Apple's Vision Framework via PyObjC to perform text detection on images (macOS 10.15+ only) """
|
||||
|
||||
import logging
|
||||
import sys
|
||||
from typing import List, Optional
|
||||
|
||||
from .utils import assert_macos, get_macos_version
|
||||
|
||||
assert_macos()
|
||||
|
||||
import objc
|
||||
import Quartz
|
||||
from Cocoa import NSURL
|
||||
@ -11,8 +16,6 @@ from Foundation import NSDictionary
|
||||
# needed to capture system-level stderr
|
||||
from wurlitzer import pipes
|
||||
|
||||
from .utils import get_macos_version
|
||||
|
||||
__all__ = ["detect_text", "make_request_handler"]
|
||||
|
||||
ver, major, minor = get_macos_version()
|
||||
|
||||
@ -2,13 +2,7 @@
|
||||
|
||||
from typing import Union
|
||||
|
||||
import Foundation
|
||||
import objc
|
||||
|
||||
|
||||
def known_timezone_names():
|
||||
"""Get list of valid timezones on macOS"""
|
||||
return Foundation.NSTimeZone.knownTimeZoneNames()
|
||||
from .utils import is_macos
|
||||
|
||||
|
||||
def format_offset_time(offset: int) -> str:
|
||||
@ -19,7 +13,17 @@ def format_offset_time(offset: int) -> str:
|
||||
return f"{sign}{hours:02d}:{minutes:02d}"
|
||||
|
||||
|
||||
class Timezone:
|
||||
if is_macos:
|
||||
import Foundation
|
||||
import objc
|
||||
|
||||
|
||||
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]):
|
||||
@ -59,3 +63,57 @@ class Timezone:
|
||||
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):
|
||||
self.timezone = zoneinfo.ZoneInfo(tz)
|
||||
self._name = tz
|
||||
elif isinstance(tz, int):
|
||||
if tz > 0:
|
||||
name = f"Etc/GMT+{tz // 3600}"
|
||||
else:
|
||||
name = f"Etc/GMT-{-tz // 3600}"
|
||||
self.timezone = zoneinfo.ZoneInfo(name)
|
||||
self._name = self.timezone.key
|
||||
else:
|
||||
raise TypeError("Timezone must be a string or an int")
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def offset(self) -> int:
|
||||
td = self.timezone.utcoffset(datetime.now())
|
||||
assert td
|
||||
return int(td.total_seconds())
|
||||
|
||||
@property
|
||||
def offset_str(self) -> str:
|
||||
return format_offset_time(self.offset)
|
||||
|
||||
@property
|
||||
def abbreviation(self) -> str:
|
||||
return self.timezone.key
|
||||
|
||||
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
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
""" 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
|
||||
UTTypeCopyPreferredTagWithClass and UTTypeCreatePreferredIdentifierForTag to retrieve the
|
||||
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,
|
||||
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.
|
||||
"""
|
||||
|
||||
@ -21,12 +21,14 @@ from __future__ import annotations
|
||||
import csv
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
import CoreServices
|
||||
import objc
|
||||
from .utils import assert_macos, is_macos, get_macos_version
|
||||
|
||||
from .utils import get_macos_version
|
||||
if is_macos:
|
||||
import CoreServices
|
||||
import objc
|
||||
|
||||
__all__ = ["get_preferred_uti_extension", "get_uti_for_extension"]
|
||||
|
||||
@ -518,11 +520,10 @@ def _load_uti_dict():
|
||||
EXT_UTI_DICT[row["extension"]] = row["UTI"]
|
||||
UTI_EXT_DICT[row["UTI"]] = row["preferred_extension"]
|
||||
|
||||
|
||||
_load_uti_dict()
|
||||
|
||||
# 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):
|
||||
@ -532,6 +533,9 @@ def _get_uti_from_mdls(extension):
|
||||
# mdls -name kMDItemContentType foo.3fr
|
||||
# kMDItemContentType = "com.hasselblad.3fr-raw-image"
|
||||
|
||||
if not is_macos:
|
||||
return None
|
||||
|
||||
try:
|
||||
with tempfile.NamedTemporaryFile(suffix="." + extension) as temp:
|
||||
output = subprocess.check_output(
|
||||
@ -573,7 +577,7 @@ def get_preferred_uti_extension(uti: str) -> str | None:
|
||||
uti: UTI str, e.g. 'public.jpeg'
|
||||
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
|
||||
# deprecated in Catalina+, likely won't work at all on macOS 12
|
||||
with objc.autorelease_pool():
|
||||
@ -602,7 +606,7 @@ def get_uti_for_extension(extension):
|
||||
if extension[0] == ".":
|
||||
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
|
||||
with objc.autorelease_pool():
|
||||
uti = CoreServices.UTTypeCreatePreferredIdentifierForTag(
|
||||
|
||||
@ -16,10 +16,9 @@ import sys
|
||||
import unicodedata
|
||||
import urllib.parse
|
||||
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
|
||||
|
||||
import CoreFoundation
|
||||
import requests
|
||||
import shortuuid
|
||||
|
||||
@ -28,6 +27,8 @@ from ._constants import UNICODE_FORMAT
|
||||
logger = logging.getLogger("osxphotos")
|
||||
|
||||
__all__ = [
|
||||
"is_macos",
|
||||
"assert_macos",
|
||||
"dd_to_dms_str",
|
||||
"expand_and_validate_filepath",
|
||||
"get_last_library_path",
|
||||
@ -53,6 +54,16 @@ __all__ = [
|
||||
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):
|
||||
"""do nothing (no operation)"""
|
||||
pass
|
||||
@ -67,6 +78,7 @@ def lineno(filename):
|
||||
|
||||
|
||||
def get_macos_version():
|
||||
assert_macos()
|
||||
# returns tuple of str containing OS version
|
||||
# e.g. 10.13.6 = ("10", "13", "6")
|
||||
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"""
|
||||
""" only works on MacOS 10.15 """
|
||||
""" on earlier versions, returns None """
|
||||
if not is_macos:
|
||||
return None
|
||||
_, major, _ = get_macos_version()
|
||||
if int(major) < 15:
|
||||
logger.debug(
|
||||
@ -241,6 +255,8 @@ def get_last_library_path():
|
||||
def list_photo_libraries():
|
||||
"""returns list of Photos libraries found on the system"""
|
||||
""" on MacOS < 10.15, this may omit some libraries """
|
||||
if not is_macos:
|
||||
return []
|
||||
|
||||
# On 10.15, mdfind appears to find all libraries
|
||||
# On older MacOS versions, mdfind appears to ignore some libraries
|
||||
@ -263,11 +279,14 @@ def list_photo_libraries():
|
||||
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"""
|
||||
# macOS HFS+ uses NFD, APFS doesn't normalize but stick with NFD
|
||||
# ref: https://eclecticlight.co/2021/05/08/explainer-unicode-normalization-and-apfs/
|
||||
return unicodedata.normalize("NFD", path)
|
||||
form = "NFD" if is_macos else "NFC"
|
||||
if isinstance(path, pathlib.Path):
|
||||
return pathlib.Path(unicodedata.normalize(form, str(path)))
|
||||
else:
|
||||
return unicodedata.normalize(form, path)
|
||||
|
||||
|
||||
# def findfiles(pattern, path):
|
||||
@ -356,7 +375,7 @@ def list_directory(
|
||||
return files
|
||||
|
||||
|
||||
def normalize_unicode(value):
|
||||
def normalize_unicode(value) -> Any:
|
||||
"""normalize unicode data"""
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
@ -5,14 +5,19 @@ import shutil
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
import photoscript
|
||||
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 .test_catalina_10_15_7 import UUID_DICT_LOCAL
|
||||
|
||||
# run timewarp tests (configured with --timewarp)
|
||||
TEST_TIMEWARP = False
|
||||
@ -34,6 +39,9 @@ NO_CLEANUP = False
|
||||
|
||||
|
||||
def get_os_version():
|
||||
if not is_macos:
|
||||
return (None, None, None)
|
||||
|
||||
import platform
|
||||
|
||||
# returns tuple containing OS version
|
||||
@ -53,7 +61,7 @@ def get_os_version():
|
||||
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":
|
||||
# Catalina
|
||||
TEST_LIBRARY = "tests/Test-10.15.7.photoslibrary"
|
||||
@ -77,28 +85,28 @@ else:
|
||||
TEST_LIBRARY_ADD_LOCATIONS = None
|
||||
|
||||
|
||||
@pytest.fixture(scope="session", autouse=True)
|
||||
@pytest.fixture(scope="session", autouse=is_macos)
|
||||
def setup_photos_timewarp():
|
||||
if not TEST_TIMEWARP:
|
||||
return
|
||||
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():
|
||||
if not TEST_IMPORT:
|
||||
return
|
||||
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():
|
||||
if not TEST_SYNC:
|
||||
return
|
||||
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():
|
||||
if not TEST_ADD_LOCATIONS:
|
||||
return
|
||||
@ -312,7 +320,10 @@ def addalbum_library():
|
||||
|
||||
def copy_photos_library_to_path(photos_library_path: str, dest_path: str) -> str:
|
||||
"""Copy a photos library to a folder"""
|
||||
if is_macos:
|
||||
ditto(photos_library_path, dest_path)
|
||||
else:
|
||||
shutil.copytree(photos_library_path, 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
|
||||
from osxphotos._constants import _UNKNOWN_PERSON
|
||||
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"
|
||||
PHOTOS_DB_LOCAL = os.path.expanduser("~/Pictures/Photos Library.photoslibrary")
|
||||
|
||||
@ -1448,6 +1448,7 @@ def test_multi_uuid(photosdb):
|
||||
assert len(photos) == 2
|
||||
|
||||
|
||||
@pytest.mark.skipif(not is_macos, reason="Only works on macOS")
|
||||
def test_detected_text(photosdb):
|
||||
"""test PhotoInfo.detected_text"""
|
||||
for uuid, expected_text in UUID_DETECTED_TEXT.items():
|
||||
|
||||
@ -14,10 +14,10 @@ import subprocess
|
||||
import tempfile
|
||||
import time
|
||||
from tempfile import TemporaryDirectory
|
||||
from bitmath import contextlib
|
||||
|
||||
import pytest
|
||||
from click.testing import CliRunner
|
||||
from osxmetadata import OSXMetaData, Tag
|
||||
|
||||
import osxphotos
|
||||
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.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 .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"
|
||||
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_EXPORT_FILENAMES = [
|
||||
CLI_EXPORT_FILENAMES = _normalize_fs_paths([
|
||||
"[2020-08-29] AAF035 (1).jpg",
|
||||
"[2020-08-29] AAF035 (2).jpg",
|
||||
"[2020-08-29] AAF035 (3).jpg",
|
||||
@ -100,10 +107,10 @@ CLI_EXPORT_FILENAMES = [
|
||||
"wedding.jpg",
|
||||
"winebottle (1).jpeg",
|
||||
"winebottle.jpeg",
|
||||
]
|
||||
])
|
||||
|
||||
|
||||
CLI_EXPORT_FILENAMES_DRY_RUN = [
|
||||
CLI_EXPORT_FILENAMES_DRY_RUN = _normalize_fs_paths([
|
||||
"[2020-08-29] AAF035.jpg",
|
||||
"DSC03584.dng",
|
||||
"Frítest_edited.jpeg",
|
||||
@ -130,15 +137,25 @@ CLI_EXPORT_FILENAMES_DRY_RUN = [
|
||||
"wedding.jpg",
|
||||
"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_TEMPLATE = "{edited?_edited,}"
|
||||
@ -146,7 +163,7 @@ CLI_EXPORT_ORIGINAL_SUFFIX = "_original"
|
||||
CLI_EXPORT_ORIGINAL_SUFFIX_TEMPLATE = "{edited?_original,}"
|
||||
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 (2).jpg",
|
||||
"[2020-08-29] AAF035 (3).jpg",
|
||||
@ -180,9 +197,9 @@ CLI_EXPORT_FILENAMES_EDITED_SUFFIX = [
|
||||
"wedding.jpg",
|
||||
"winebottle (1).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 (2).jpg",
|
||||
"[2020-08-29] AAF035 (3).jpg",
|
||||
@ -216,9 +233,9 @@ CLI_EXPORT_FILENAMES_EDITED_SUFFIX_TEMPLATE = [
|
||||
"wedding.jpg",
|
||||
"winebottle (1).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 (2).jpg",
|
||||
"[2020-08-29] AAF035_original (3).jpg",
|
||||
@ -252,9 +269,9 @@ CLI_EXPORT_FILENAMES_ORIGINAL_SUFFIX = [
|
||||
"wedding_original.jpg",
|
||||
"winebottle_original (1).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 (2).jpg",
|
||||
"[2020-08-29] AAF035 (3).jpg",
|
||||
@ -288,7 +305,7 @@ CLI_EXPORT_FILENAMES_ORIGINAL_SUFFIX_TEMPLATE = [
|
||||
"wedding_original.jpg",
|
||||
"winebottle (1).jpeg",
|
||||
"winebottle.jpeg",
|
||||
]
|
||||
])
|
||||
|
||||
CLI_EXPORT_FILENAMES_CURRENT = [
|
||||
"1793FAAB-DE75-4E25-886C-2BD66C780D6A_edited.jpeg", # Frítest.jpg
|
||||
@ -326,7 +343,7 @@ CLI_EXPORT_FILENAMES_CURRENT = [
|
||||
"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 (2).jpg",
|
||||
"[2020-08-29] AAF035 (3).jpg",
|
||||
@ -360,9 +377,9 @@ CLI_EXPORT_FILENAMES_CONVERT_TO_JPEG = [
|
||||
"wedding.jpg",
|
||||
"winebottle (1).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 (2).jpg",
|
||||
"[2020-08-29] AAF035 (3).jpg",
|
||||
@ -394,26 +411,26 @@ CLI_EXPORT_FILENAMES_CONVERT_TO_JPEG_SKIP_RAW = [
|
||||
"wedding.jpg",
|
||||
"winebottle (1).jpeg",
|
||||
"winebottle.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/July/Tulips.jpg",
|
||||
"2018/October/St James Park.jpg",
|
||||
"2018/September/Pumpkins3.jpg",
|
||||
"2018/September/Pumkins2.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",
|
||||
"2020/Februar/IMG_1064.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",
|
||||
"_/Tulips.jpg",
|
||||
"_/St James Park.jpg",
|
||||
@ -421,9 +438,9 @@ CLI_EXPORTED_DIRECTORY_TEMPLATE_FILENAMES_ALBUM1 = [
|
||||
"Pumpkin Farm/Pumkins2.jpg",
|
||||
"Pumpkin Farm/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",
|
||||
"NOALBUM/Tulips.jpg",
|
||||
"NOALBUM/St James Park.jpg",
|
||||
@ -431,28 +448,28 @@ CLI_EXPORTED_DIRECTORY_TEMPLATE_FILENAMES_ALBUM2 = [
|
||||
"Pumpkin Farm/Pumkins2.jpg",
|
||||
"Pumpkin Farm/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",
|
||||
"_/Pumpkins3.jpg",
|
||||
"Omaha, Nebraska, United States/Pumkins2.jpg",
|
||||
"_/Pumkins1.jpg",
|
||||
"_/Tulips.jpg",
|
||||
"_/wedding.jpg",
|
||||
]
|
||||
])
|
||||
|
||||
CLI_EXPORTED_DIRECTORY_TEMPLATE_FILENAMES3 = [
|
||||
CLI_EXPORTED_DIRECTORY_TEMPLATE_FILENAMES3 = _normalize_fs_paths([
|
||||
"2019/{foo}/wedding.jpg",
|
||||
"2019/{foo}/Tulips.jpg",
|
||||
"2018/{foo}/St James Park.jpg",
|
||||
"2018/{foo}/Pumpkins3.jpg",
|
||||
"2018/{foo}/Pumkins2.jpg",
|
||||
"2018/{foo}/Pumkins1.jpg",
|
||||
]
|
||||
])
|
||||
|
||||
|
||||
CLI_EXPORTED_FILENAME_TEMPLATE_FILENAMES1 = [
|
||||
CLI_EXPORTED_FILENAME_TEMPLATE_FILENAMES1 = _normalize_fs_paths([
|
||||
"2019-wedding.jpg",
|
||||
"2019-wedding_edited.jpeg",
|
||||
"2019-Tulips.jpg",
|
||||
@ -461,9 +478,9 @@ CLI_EXPORTED_FILENAME_TEMPLATE_FILENAMES1 = [
|
||||
"2018-Pumpkins3.jpg",
|
||||
"2018-Pumkins2.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-wedding.jpg",
|
||||
"Folder1_SubFolder2_AlbumInFolder-wedding_edited.jpeg",
|
||||
@ -484,18 +501,18 @@ CLI_EXPORTED_FILENAME_TEMPLATE_FILENAMES2 = [
|
||||
"None-IMG_1693.tif",
|
||||
"I have a deleted twin-wedding.jpg",
|
||||
"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",
|
||||
"Folder1/SubFolder2/AlbumInFolder/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"
|
||||
]
|
||||
])
|
||||
|
||||
CLI_EXPORTED_FILENAME_TEMPLATE_LONG_DESCRIPTION = [
|
||||
"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
|
||||
]
|
||||
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/10/13/St James Park.jpg",
|
||||
]
|
||||
])
|
||||
CLI_EXPORT_BY_DATE_NEED_TOUCH_UUID = [
|
||||
"D79B8D77-BFFC-460B-9312-034F2877D35B",
|
||||
"DC99FBDD-7A52-4100-A5BB-344131646C30",
|
||||
]
|
||||
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_DROP_EXT_FILENAMES = [
|
||||
CLI_EXPORT_SIDECAR_FILENAMES = _normalize_fs_paths([
|
||||
"Pumkins2.jpg",
|
||||
"Pumkins2.jpg.json",
|
||||
"Pumkins2.jpg.xmp",
|
||||
])
|
||||
CLI_EXPORT_SIDECAR_DROP_EXT_FILENAMES = _normalize_fs_paths([
|
||||
"Pumkins2.jpg",
|
||||
"Pumkins2.json",
|
||||
"Pumkins2.xmp",
|
||||
]
|
||||
])
|
||||
|
||||
CLI_EXPORT_LIVE = [
|
||||
"51F2BEF7-431A-4D31-8AC1-3284A57826AE.jpeg",
|
||||
"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_ORIGINAL = ["IMG_0476_2.CR2"]
|
||||
CLI_EXPORT_RAW_ORIGINAL = _normalize_fs_paths(["IMG_0476_2.CR2"])
|
||||
CLI_EXPORT_RAW_EDITED = [
|
||||
"441DFE2A-A69B-4C79-A69B-3F51D1B9B29C.cr2",
|
||||
"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 = {
|
||||
"intrash": "71E3E212-00EB-430D-8A63-5E294B268554",
|
||||
"template": "F12384F6-CD17-4151-ACBA-AE0E3688539E",
|
||||
}
|
||||
|
||||
CLI_TEMPLATE_SIDECAR_FILENAME = "Pumkins1.jpg.json"
|
||||
CLI_TEMPLATE_FILENAME = "Pumkins1.jpg"
|
||||
CLI_TEMPLATE_SIDECAR_FILENAME = normalize_fs_path("Pumkins1.jpg.json")
|
||||
CLI_TEMPLATE_FILENAME = normalize_fs_path("Pumkins1.jpg")
|
||||
|
||||
CLI_UUID_DICT_14_6 = {"intrash": "3tljdX43R8+k6peNHVrJNQ"}
|
||||
|
||||
@ -960,12 +990,12 @@ UUID_UNICODE_TITLE = [
|
||||
"D1D4040D-D141-44E8-93EA-E403D9F63E07", # Frítest.jpg
|
||||
]
|
||||
|
||||
EXPORT_UNICODE_TITLE_FILENAMES = [
|
||||
EXPORT_UNICODE_TITLE_FILENAMES = _normalize_fs_paths([
|
||||
"Frítest.jpg",
|
||||
"Frítest (1).jpg",
|
||||
"Frítest (2).jpg",
|
||||
"Frítest (3).jpg",
|
||||
]
|
||||
])
|
||||
|
||||
# data for --report
|
||||
UUID_REPORT = [
|
||||
@ -987,12 +1017,12 @@ QUERY_EXIF_DATA_CASE_INSENSITIVE = [
|
||||
EXPORT_EXIF_DATA = [("EXIF:Make", "FUJIFILM", ["Tulips.jpg", "Tulips_edited.jpeg"])]
|
||||
|
||||
UUID_LIVE_EDITED = "136A78FA-1B90-46CC-88A7-CCA3331F0353" # IMG_4813.HEIC
|
||||
CLI_EXPORT_LIVE_EDITED = [
|
||||
CLI_EXPORT_LIVE_EDITED = _normalize_fs_paths([
|
||||
"IMG_4813.HEIC",
|
||||
"IMG_4813.mov",
|
||||
"IMG_4813_edited.jpeg",
|
||||
"IMG_4813_edited.mov",
|
||||
]
|
||||
])
|
||||
|
||||
UUID_FAVORITE = "E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51"
|
||||
FILE_FAVORITE = "wedding.jpg"
|
||||
@ -1804,12 +1834,25 @@ def test_export_preview_update():
|
||||
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():
|
||||
|
||||
runner = CliRunner()
|
||||
cwd = os.getcwd()
|
||||
# pylint: disable=not-context-manager
|
||||
with runner.isolated_filesystem():
|
||||
with isolated_filesystem_here():
|
||||
result = runner.invoke(
|
||||
export,
|
||||
[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]
|
||||
|
||||
# pylint: disable=not-context-manager
|
||||
with runner.isolated_filesystem():
|
||||
with isolated_filesystem_here():
|
||||
result = runner.invoke(
|
||||
export,
|
||||
[
|
||||
@ -1854,7 +1897,7 @@ def test_export_using_hardlinks_incompat_options():
|
||||
photo = photosdb.photos(uuid=[CLI_EXPORT_UUID])[0]
|
||||
|
||||
# pylint: disable=not-context-manager
|
||||
with runner.isolated_filesystem():
|
||||
with isolated_filesystem_here():
|
||||
result = runner.invoke(
|
||||
export,
|
||||
[
|
||||
@ -3717,7 +3760,7 @@ def test_export_raw_edited_original():
|
||||
def test_export_directory_template_1():
|
||||
# test export using directory template
|
||||
|
||||
locale.setlocale(locale.LC_ALL, "en_US")
|
||||
setlocale(locale.LC_ALL, "en_US")
|
||||
|
||||
runner = CliRunner()
|
||||
cwd = os.getcwd()
|
||||
@ -3837,7 +3880,7 @@ def test_export_directory_template_locale():
|
||||
with runner.isolated_filesystem():
|
||||
# set locale environment
|
||||
os.environ["LC_ALL"] = "de_DE.UTF-8"
|
||||
locale.setlocale(locale.LC_ALL, "")
|
||||
setlocale(locale.LC_ALL, "")
|
||||
result = runner.invoke(
|
||||
export,
|
||||
[
|
||||
@ -3857,7 +3900,7 @@ def test_export_directory_template_locale():
|
||||
def test_export_filename_template_1():
|
||||
"""export photos using filename template"""
|
||||
|
||||
locale.setlocale(locale.LC_ALL, "en_US")
|
||||
setlocale(locale.LC_ALL, "en_US")
|
||||
|
||||
runner = CliRunner()
|
||||
cwd = os.getcwd()
|
||||
@ -3882,7 +3925,7 @@ def test_export_filename_template_1():
|
||||
def test_export_filename_template_2():
|
||||
"""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()
|
||||
cwd = os.getcwd()
|
||||
@ -3907,7 +3950,7 @@ def test_export_filename_template_2():
|
||||
def test_export_filename_template_strip():
|
||||
"""export photos using filename template with --strip"""
|
||||
|
||||
locale.setlocale(locale.LC_ALL, "en_US")
|
||||
setlocale(locale.LC_ALL, "en_US")
|
||||
|
||||
runner = CliRunner()
|
||||
cwd = os.getcwd()
|
||||
@ -3933,7 +3976,7 @@ def test_export_filename_template_strip():
|
||||
def test_export_filename_template_pathsep_in_name_1():
|
||||
"""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()
|
||||
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():
|
||||
"""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()
|
||||
cwd = os.getcwd()
|
||||
@ -3988,7 +4031,7 @@ def test_export_filename_template_pathsep_in_name_2():
|
||||
def test_export_filename_template_long_description():
|
||||
"""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()
|
||||
cwd = os.getcwd()
|
||||
@ -4664,7 +4707,6 @@ def test_export_force_update():
|
||||
export, [os.path.join(cwd, photos_db_path), ".", "--force-update"]
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
print(result.output)
|
||||
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"
|
||||
in result.output
|
||||
@ -4891,7 +4933,7 @@ def test_export_update_hardlink():
|
||||
runner = CliRunner()
|
||||
cwd = os.getcwd()
|
||||
# pylint: disable=not-context-manager
|
||||
with runner.isolated_filesystem():
|
||||
with isolated_filesystem_here():
|
||||
# basic export
|
||||
result = runner.invoke(
|
||||
export,
|
||||
@ -4924,7 +4966,7 @@ def test_export_update_hardlink_exiftool():
|
||||
runner = CliRunner()
|
||||
cwd = os.getcwd()
|
||||
# pylint: disable=not-context-manager
|
||||
with runner.isolated_filesystem():
|
||||
with isolated_filesystem_here():
|
||||
# basic export
|
||||
result = runner.invoke(
|
||||
export,
|
||||
@ -5069,7 +5111,7 @@ def test_export_then_hardlink():
|
||||
runner = CliRunner()
|
||||
cwd = os.getcwd()
|
||||
# pylint: disable=not-context-manager
|
||||
with runner.isolated_filesystem():
|
||||
with isolated_filesystem_here():
|
||||
# basic export
|
||||
result = runner.invoke(export, [os.path.join(cwd, CLI_PHOTOS_DB), ".", "-V"])
|
||||
assert result.exit_code == 0
|
||||
@ -5177,7 +5219,7 @@ def test_export_update_edits_dry_run():
|
||||
def test_export_directory_template_1_dry_run():
|
||||
"""test export using directory template with dry-run flag"""
|
||||
|
||||
locale.setlocale(locale.LC_ALL, "en_US")
|
||||
setlocale(locale.LC_ALL, "en_US")
|
||||
|
||||
runner = CliRunner()
|
||||
cwd = os.getcwd()
|
||||
@ -6052,7 +6094,7 @@ def test_export_as_hardlink_download_missing():
|
||||
runner = CliRunner()
|
||||
cwd = os.getcwd()
|
||||
# pylint: disable=not-context-manager
|
||||
with runner.isolated_filesystem():
|
||||
with isolated_filesystem_here():
|
||||
result = runner.invoke(
|
||||
export,
|
||||
[
|
||||
@ -6877,6 +6919,7 @@ def test_export_finder_tag_keywords_dry_run():
|
||||
assert result.exit_code == 0
|
||||
|
||||
|
||||
@pytest.mark.skipif(not is_macos, reason="Only works on macOS")
|
||||
def test_export_finder_tag_keywords():
|
||||
"""test --finder-tag-keywords"""
|
||||
|
||||
@ -6951,6 +6994,7 @@ def test_export_finder_tag_keywords():
|
||||
assert sorted(md.tags) == sorted(expected)
|
||||
|
||||
|
||||
@pytest.mark.skipif(not is_macos, reason="Only works on macOS")
|
||||
def test_export_finder_tag_template():
|
||||
"""test --finder-tag-template"""
|
||||
|
||||
@ -7028,6 +7072,7 @@ def test_export_finder_tag_template():
|
||||
assert sorted(md.tags) == sorted(expected)
|
||||
|
||||
|
||||
@pytest.mark.skipif(not is_macos, reason="Only works on macOS")
|
||||
def test_export_finder_tag_template_multiple():
|
||||
"""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)
|
||||
|
||||
|
||||
@pytest.mark.skipif(not is_macos, reason="Only works on macOS")
|
||||
def test_export_finder_tag_template_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)
|
||||
|
||||
|
||||
@pytest.mark.skipif(not is_macos, reason="Only works on macOS")
|
||||
def test_export_finder_tag_template_multi_field():
|
||||
"""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
|
||||
|
||||
|
||||
@pytest.mark.skipif(not is_macos, reason="Only works on macOS")
|
||||
def test_export_xattr_template():
|
||||
"""test --xattr template"""
|
||||
|
||||
@ -7655,14 +7703,14 @@ def test_query_name_unicode():
|
||||
cwd = os.getcwd()
|
||||
result = runner.invoke(
|
||||
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
|
||||
json_got = json.loads(result.output)
|
||||
|
||||
assert len(json_got) == 4
|
||||
assert normalize_unicode(json_got[0]["original_filename"]).startswith(
|
||||
normalize_unicode("Frítest.jpg")
|
||||
normalize_unicode("Frítest.jpg")
|
||||
)
|
||||
|
||||
|
||||
|
||||
@ -1,10 +1,14 @@
|
||||
"""Test osxphotos add-locations command"""
|
||||
|
||||
import photoscript
|
||||
import pytest
|
||||
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_LOCATION = "D79B8D77-BFFC-460B-9312-034F2877D35B" # Pumkins2.jpg
|
||||
|
||||
@ -2,10 +2,15 @@
|
||||
|
||||
import os
|
||||
|
||||
import photoscript
|
||||
import pytest
|
||||
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_MISSING = {
|
||||
"8E1D7BC9-9321-44F9-8CFB-4083F6B9232A": {"filename": "IMG_2000.JPGssss"}
|
||||
|
||||
@ -24,7 +24,6 @@ TEST_RUN_SCRIPT = "examples/cli_example_1.py"
|
||||
def runner() -> CliRunner:
|
||||
return CliRunner()
|
||||
|
||||
|
||||
from osxphotos.cli import (
|
||||
about,
|
||||
albums,
|
||||
@ -42,10 +41,14 @@ from osxphotos.cli import (
|
||||
places,
|
||||
theme,
|
||||
tutorial,
|
||||
uuid,
|
||||
version,
|
||||
)
|
||||
|
||||
from osxphotos.utils import is_macos
|
||||
|
||||
if is_macos:
|
||||
from osxphotos.cli import uuid
|
||||
|
||||
|
||||
def test_about(runner: CliRunner):
|
||||
with runner.isolated_filesystem():
|
||||
@ -68,9 +71,8 @@ def test_about(runner: CliRunner):
|
||||
persons,
|
||||
places,
|
||||
tutorial,
|
||||
uuid,
|
||||
version,
|
||||
],
|
||||
] + ([uuid] if is_macos else []),
|
||||
)
|
||||
def test_cli_comands(runner: CliRunner, command: Callable[..., Any]):
|
||||
with runner.isolated_filesystem():
|
||||
|
||||
@ -5,12 +5,17 @@ from __future__ import annotations
|
||||
import os
|
||||
import time
|
||||
|
||||
import photoscript
|
||||
import pytest
|
||||
from click.testing import CliRunner
|
||||
|
||||
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
|
||||
os.environ["TZ"] = "US/Pacific"
|
||||
|
||||
@ -14,17 +14,23 @@ from tempfile import TemporaryDirectory
|
||||
from typing import Dict
|
||||
|
||||
import pytest
|
||||
|
||||
from click.testing import CliRunner
|
||||
from photoscript import Photo
|
||||
from pytest import MonkeyPatch, approx
|
||||
|
||||
from osxphotos import PhotosDB, QueryOptions
|
||||
from osxphotos._constants import UUID_PATTERN
|
||||
from osxphotos.cli.import_cli import import_cli
|
||||
from osxphotos.datetime_utils import datetime_remove_tz
|
||||
from osxphotos.exiftool import get_exiftool_path
|
||||
from osxphotos.utils import is_macos
|
||||
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
|
||||
|
||||
TEST_IMAGES_DIR = "tests/test-images"
|
||||
|
||||
@ -2,11 +2,15 @@
|
||||
|
||||
import os
|
||||
import json
|
||||
import photoscript
|
||||
import pytest
|
||||
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_2 = "E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51" # wedding.jpg
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
""" test datetime_formatter.DateTimeFormatter """
|
||||
import pytest
|
||||
|
||||
from .locale_util import setlocale
|
||||
|
||||
|
||||
def test_datetime_formatter_1():
|
||||
"""Test DateTimeFormatter """
|
||||
@ -8,7 +10,7 @@ def test_datetime_formatter_1():
|
||||
import locale
|
||||
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)
|
||||
dtf = DateTimeFormatter(dt)
|
||||
@ -32,7 +34,7 @@ def test_datetime_formatter_2():
|
||||
import locale
|
||||
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)
|
||||
dtf = DateTimeFormatter(dt)
|
||||
@ -56,7 +58,7 @@ def test_datetime_formatter_3():
|
||||
import locale
|
||||
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)
|
||||
dtf = DateTimeFormatter(dt)
|
||||
|
||||
@ -538,13 +538,13 @@ def test_exiftool_terminate():
|
||||
|
||||
ps = subprocess.run(["ps"], capture_output=True)
|
||||
stdout = ps.stdout.decode("utf-8")
|
||||
assert "exiftool -stay_open" in stdout
|
||||
assert "exiftool" in stdout
|
||||
|
||||
osxphotos.exiftool.terminate_exiftool()
|
||||
|
||||
ps = subprocess.run(["ps"], capture_output=True)
|
||||
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
|
||||
exif2 = osxphotos.exiftool.ExifTool(TEST_FILE_ONE_KEYWORD)
|
||||
|
||||
@ -2,9 +2,9 @@ import os
|
||||
import pytest
|
||||
|
||||
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"
|
||||
PHOTOS_DB = os.path.expanduser("~/Pictures/Photos Library.photoslibrary")
|
||||
pytestmark = pytest.mark.skipif(
|
||||
|
||||
@ -74,10 +74,12 @@ def test_hardlink_file_valid():
|
||||
|
||||
temp_dir = tempfile.TemporaryDirectory(prefix="osxphotos_")
|
||||
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")
|
||||
FileUtil.hardlink(src, dest)
|
||||
FileUtil.copy(src, src2)
|
||||
FileUtil.hardlink(src2, dest)
|
||||
assert os.path.isfile(dest)
|
||||
assert os.path.samefile(src, dest)
|
||||
assert os.path.samefile(src2, dest)
|
||||
|
||||
|
||||
def test_unlink_file():
|
||||
|
||||
@ -14,9 +14,9 @@ import pytest
|
||||
import osxphotos
|
||||
from osxphotos._constants import _UNKNOWN_PERSON
|
||||
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 = True # don't run any of the local library tests
|
||||
PHOTOS_DB_LOCAL = os.path.expanduser("~/Pictures/Photos Library.photoslibrary")
|
||||
|
||||
@ -6,7 +6,9 @@ import tempfile
|
||||
|
||||
import pytest
|
||||
|
||||
from osxphotos.photokit import (
|
||||
from osxphotos.utils import is_macos
|
||||
if is_macos:
|
||||
from osxphotos.photokit import (
|
||||
LivePhotoAsset,
|
||||
PhotoAsset,
|
||||
PhotoLibrary,
|
||||
@ -14,7 +16,9 @@ from osxphotos.photokit import (
|
||||
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
|
||||
pytestmark = pytest.mark.skipif(
|
||||
|
||||
@ -15,7 +15,9 @@ from osxphotos.phototemplate import (
|
||||
RenderOptions,
|
||||
)
|
||||
from osxphotos.photoinfo import PhotoInfoNone
|
||||
from osxphotos.utils import is_macos
|
||||
from .photoinfo_mock import PhotoInfoMock
|
||||
from .locale_util import setlocale
|
||||
|
||||
try:
|
||||
exiftool = get_exiftool_path()
|
||||
@ -549,6 +551,8 @@ def test_lookup_multi(photosdb_places):
|
||||
lookup_str = re.match(r"\{([^\\,}]+)\}", subst).group(1)
|
||||
if subst in ["{exiftool}", "{photo}", "{function}", "{format}"]:
|
||||
continue
|
||||
if subst == "{detected_text}" and not is_macos:
|
||||
continue
|
||||
lookup = template.get_template_value_multi(
|
||||
lookup_str,
|
||||
path_sep=os.path.sep,
|
||||
@ -562,7 +566,7 @@ def test_subst(photosdb_places):
|
||||
"""Test that substitutions are correct"""
|
||||
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]
|
||||
|
||||
for template in TEMPLATE_VALUES:
|
||||
@ -574,7 +578,7 @@ def test_subst_date_modified(photosdb_places):
|
||||
"""Test that substitutions are correct for date modified"""
|
||||
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]
|
||||
|
||||
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"""
|
||||
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]
|
||||
|
||||
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
|
||||
# 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]
|
||||
|
||||
@ -614,6 +618,9 @@ def test_subst_locale_2(photosdb_places):
|
||||
import locale
|
||||
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
|
||||
os.environ["LANG"] = "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"""
|
||||
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]
|
||||
|
||||
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"""
|
||||
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]
|
||||
|
||||
template = "{place.name.area_of_interest,}"
|
||||
@ -658,7 +665,7 @@ def test_subst_unknown_val(photosdb_places):
|
||||
"""Test substitution with unknown value specified"""
|
||||
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]
|
||||
|
||||
template = "{created.year}/{foo}"
|
||||
@ -682,7 +689,7 @@ def test_subst_unknown_val_with_default(photosdb_places):
|
||||
"""Test substitution with unknown value specified"""
|
||||
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]
|
||||
|
||||
template = "{created.year}/{foo,bar}"
|
||||
@ -929,7 +936,7 @@ def test_subst_strftime(photosdb_places):
|
||||
"""Test that strftime substitutions are correct"""
|
||||
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]
|
||||
|
||||
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
|
||||
|
||||
|
||||
@pytest.mark.skipif(not is_macos, reason="Only works on macOS")
|
||||
def test_detected_text(photosdb):
|
||||
"""Test {detected_text} template"""
|
||||
photo = photosdb.get_photo(UUID_DETECTED_TEXT)
|
||||
|
||||
@ -5,6 +5,8 @@ import pytest
|
||||
import osxphotos
|
||||
from osxphotos.phototemplate import RenderOptions
|
||||
|
||||
from .locale_util import setlocale
|
||||
|
||||
PHOTOS_DB_PLACES = (
|
||||
"./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}"""
|
||||
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_template = osxphotos.PhotoTemplate(photo)
|
||||
@ -64,7 +66,7 @@ def test_subst_strftime_today(photosdb):
|
||||
"""Test that strftime substitutions are correct for {today.strftime}"""
|
||||
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_template = osxphotos.PhotoTemplate(photo)
|
||||
|
||||
@ -8,6 +8,7 @@ from osxphotos.uti import (
|
||||
get_preferred_uti_extension,
|
||||
get_uti_for_extension,
|
||||
)
|
||||
from osxphotos.utils import is_macos
|
||||
|
||||
EXT_DICT = {"heic": "public.heic", "jpg": "public.jpeg", ".jpg": "public.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
|
||||
|
||||
|
||||
@pytest.mark.skipif(not is_macos, reason="Only works on macOS")
|
||||
def test_get_uti_from_mdls():
|
||||
"""get _get_uti_from_mdls"""
|
||||
for ext in EXT_DICT:
|
||||
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():
|
||||
"""get UTI when objc is not available and it's not in the EXT_UTI_DICT"""
|
||||
# monkey patch the EXT_UTI_DICT
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user