Refactor verbose 931 (#960)
* Began refactoring verbose code for #931 * Fixed tests for timewarp due to verbose refactor * Updated test data * More refactoring for #931 * Refactored queryoptions.py * Refactored queryoptions.py * Refactored queryoptions.py * Refactored echo_error * Refactored debug * Refactored debug * Refactored use of verbose in export * Refactored use of verbose in export * Refactred --verbose in add-locations and debug-dump * Refactored --verbose for * Refactored --verbose for osxphotos exportdb * Refactored --verbose for osxphotos import * Refactored --verbose for osxphotos orphans * Refactored --verbose for osxphotos snap-diff * Refactored --verbose for osxphotos sync * Refactored --verbose for osxphotos timewarp * Added default verbose() function to verbose
This commit is contained in:
parent
770d85759d
commit
0c293d0bf5
@ -1,3 +1,7 @@
|
||||
"""__init__.py for osxphotos"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from ._constants import AlbumSortOrder
|
||||
@ -20,7 +24,13 @@ from .placeinfo import PlaceInfo
|
||||
from .queryoptions import QueryOptions
|
||||
from .scoreinfo import ScoreInfo
|
||||
from .searchinfo import SearchInfo
|
||||
from .utils import _get_logger
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.DEBUG,
|
||||
format="%(asctime)s - %(name)s - %(levelname)s - %(filename)s - %(lineno)d - %(message)s",
|
||||
)
|
||||
|
||||
logger: logging.Logger = logging.getLogger("osxphotos")
|
||||
|
||||
if not is_debug():
|
||||
logging.disable(logging.DEBUG)
|
||||
@ -37,6 +47,7 @@ __all__ = [
|
||||
"ExportResults",
|
||||
"FileUtil",
|
||||
"FileUtilNoOp",
|
||||
"FolderInfo",
|
||||
"ImportInfo",
|
||||
"LikeInfo",
|
||||
"MomentInfo",
|
||||
@ -53,8 +64,7 @@ __all__ = [
|
||||
"ScoreInfo",
|
||||
"SearchInfo",
|
||||
"__version__",
|
||||
"_get_logger",
|
||||
"is_debug",
|
||||
"logger",
|
||||
"set_debug",
|
||||
"FolderInfo",
|
||||
]
|
||||
|
||||
@ -47,7 +47,7 @@ from .about import about
|
||||
from .add_locations import add_locations
|
||||
from .albums import albums
|
||||
from .cli import cli_main
|
||||
from .common import get_photos_db, load_uuid_from_file
|
||||
from .common import get_photos_db
|
||||
from .debug_dump import debug_dump
|
||||
from .dump import dump
|
||||
from .exiftool_cli import exiftool
|
||||
@ -91,7 +91,6 @@ __all__ = [
|
||||
"labels",
|
||||
"list_libraries",
|
||||
"list_libraries",
|
||||
"load_uuid_from_file",
|
||||
"orphans",
|
||||
"persons",
|
||||
"photo_inspect",
|
||||
|
||||
@ -8,17 +8,12 @@ import click
|
||||
import photoscript
|
||||
|
||||
import osxphotos
|
||||
from osxphotos.queryoptions import IncompatibleQueryOptions, query_options_from_kwargs
|
||||
from osxphotos.utils import pluralize
|
||||
|
||||
from .click_rich_echo import (
|
||||
rich_click_echo,
|
||||
rich_echo_error,
|
||||
set_rich_console,
|
||||
set_rich_theme,
|
||||
set_rich_timestamp,
|
||||
)
|
||||
from .color_themes import get_theme
|
||||
from .common import QUERY_OPTIONS, THEME_OPTION, query_options_from_kwargs
|
||||
from .click_rich_echo import rich_click_echo as echo
|
||||
from .click_rich_echo import rich_echo_error as echo_error
|
||||
from .common import QUERY_OPTIONS, THEME_OPTION, TIMESTAMP_OPTION, VERBOSE_OPTION
|
||||
from .param_types import TimeOffset
|
||||
from .rich_progress import rich_progress
|
||||
from .verbose import get_verbose_console, verbose_print
|
||||
@ -94,15 +89,15 @@ def get_location(
|
||||
help="Don't actually add location, just print what would be done. "
|
||||
"Most useful with --verbose.",
|
||||
)
|
||||
@click.option("--verbose", "-V", "verbose_", is_flag=True, help="Print verbose output.")
|
||||
@click.option(
|
||||
"--timestamp", "-T", is_flag=True, help="Add time stamp to verbose output."
|
||||
)
|
||||
@VERBOSE_OPTION
|
||||
@TIMESTAMP_OPTION
|
||||
@QUERY_OPTIONS
|
||||
@THEME_OPTION
|
||||
@click.pass_obj
|
||||
@click.pass_context
|
||||
def add_locations(ctx, cli_ob, window, dry_run, verbose_, timestamp, theme, **kwargs):
|
||||
def add_locations(
|
||||
ctx, cli_ob, window, dry_run, verbose_flag, timestamp, theme, **kwargs
|
||||
):
|
||||
"""Add missing location data to photos in Photos.app using nearest neighbor.
|
||||
|
||||
This command will search for photos that are missing location data and look
|
||||
@ -136,20 +131,19 @@ def add_locations(ctx, cli_ob, window, dry_run, verbose_, timestamp, theme, **kw
|
||||
use `osxphotos add-locations` to add location information.
|
||||
See `osxphotos help timewarp` for more information.
|
||||
"""
|
||||
color_theme = get_theme(theme)
|
||||
verbose = verbose_print(
|
||||
verbose_, timestamp, rich=True, theme=color_theme, highlight=False
|
||||
)
|
||||
# set console for rich_echo to be same as for verbose_
|
||||
set_rich_console(get_verbose_console())
|
||||
set_rich_theme(color_theme)
|
||||
set_rich_timestamp(timestamp)
|
||||
verbose = verbose_print(verbose_flag, timestamp, theme=theme)
|
||||
|
||||
verbose("Searching for photos with missing location data...")
|
||||
|
||||
# load photos database
|
||||
photosdb = osxphotos.PhotosDB(verbose=verbose)
|
||||
try:
|
||||
query_options = query_options_from_kwargs(**kwargs)
|
||||
except IncompatibleQueryOptions as e:
|
||||
echo_error("Incompatible query options")
|
||||
echo_error(ctx.obj.group.commands["repl"].get_help(ctx))
|
||||
ctx.exit(1)
|
||||
|
||||
photos = photosdb.query(query_options)
|
||||
|
||||
# sort photos by date
|
||||
@ -159,7 +153,7 @@ def add_locations(ctx, cli_ob, window, dry_run, verbose_, timestamp, theme, **kw
|
||||
missing_location = 0
|
||||
found_location = 0
|
||||
verbose(f"Processing {len(photos)} photos, window = ±{window}...")
|
||||
with rich_progress(console=get_verbose_console(), mock=verbose_) as progress:
|
||||
with rich_progress(console=get_verbose_console(), mock=verbose_flag) as progress:
|
||||
task = progress.add_task(
|
||||
f"Processing [num]{num_photos}[/] {pluralize(len(photos), 'photo', 'photos')}, window = ±{window}",
|
||||
total=num_photos,
|
||||
@ -183,7 +177,7 @@ def add_locations(ctx, cli_ob, window, dry_run, verbose_, timestamp, theme, **kw
|
||||
f"No location found for [filename]{photo.original_filename}[/] ([uuid]{photo.uuid}[/])"
|
||||
)
|
||||
progress.advance(task)
|
||||
rich_click_echo(
|
||||
echo(
|
||||
f"Done. Processed: [num]{num_photos}[/] photos, "
|
||||
f"missing location: [num]{missing_location}[/], "
|
||||
f"found location: [num]{found_location}[/] "
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
"""Globals and constants use by the CLI commands"""
|
||||
|
||||
|
||||
import dataclasses
|
||||
import os
|
||||
import pathlib
|
||||
from datetime import datetime
|
||||
@ -11,7 +10,6 @@ from packaging import version
|
||||
from xdg import xdg_config_home, xdg_data_home
|
||||
|
||||
import osxphotos
|
||||
from osxphotos import QueryOptions
|
||||
from osxphotos._constants import APP_NAME
|
||||
from osxphotos._version import __version__
|
||||
from osxphotos.utils import get_latest_version
|
||||
@ -41,20 +39,14 @@ __all__ = [
|
||||
"JSON_OPTION",
|
||||
"QUERY_OPTIONS",
|
||||
"THEME_OPTION",
|
||||
"VERBOSE_OPTION",
|
||||
"TIMESTAMP_OPTION",
|
||||
"get_photos_db",
|
||||
"load_uuid_from_file",
|
||||
"noop",
|
||||
"query_options_from_kwargs",
|
||||
"time_stamp",
|
||||
]
|
||||
|
||||
|
||||
class IncompatibleQueryOptions(Exception):
|
||||
"""Incompatible query options"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
def noop(*args, **kwargs):
|
||||
"""no-op function"""
|
||||
pass
|
||||
@ -289,7 +281,11 @@ def QUERY_OPTIONS(f):
|
||||
help="Case insensitive search for title, description, place, keyword, person, or album.",
|
||||
),
|
||||
o("--edited", is_flag=True, help="Search for photos that have been edited."),
|
||||
o("--not-edited", is_flag=True, help="Search for photos that have not been edited."),
|
||||
o(
|
||||
"--not-edited",
|
||||
is_flag=True,
|
||||
help="Search for photos that have not been edited.",
|
||||
),
|
||||
o(
|
||||
"--external-edit",
|
||||
is_flag=True,
|
||||
@ -610,37 +606,22 @@ THEME_OPTION = click.option(
|
||||
"--theme",
|
||||
metavar="THEME",
|
||||
type=click.Choice(["dark", "light", "mono", "plain"], case_sensitive=False),
|
||||
help="Specify the color theme to use for --verbose output. "
|
||||
help="Specify the color theme to use for output. "
|
||||
"Valid themes are 'dark', 'light', 'mono', and 'plain'. "
|
||||
"Defaults to 'dark' or 'light' depending on system dark mode setting.",
|
||||
)
|
||||
|
||||
VERBOSE_OPTION = click.option(
|
||||
"--verbose",
|
||||
"-V",
|
||||
"verbose_flag",
|
||||
count=True,
|
||||
help="Print verbose output; may be specified multiple times for more verbose output.",
|
||||
)
|
||||
|
||||
def load_uuid_from_file(filename):
|
||||
"""Load UUIDs from file. Does not validate UUIDs.
|
||||
Format is 1 UUID per line, any line beginning with # is ignored.
|
||||
Whitespace is stripped.
|
||||
|
||||
Arguments:
|
||||
filename: file name of the file containing UUIDs
|
||||
|
||||
Returns:
|
||||
list of UUIDs or empty list of no UUIDs in file
|
||||
|
||||
Raises:
|
||||
FileNotFoundError if file does not exist
|
||||
"""
|
||||
|
||||
if not pathlib.Path(filename).is_file():
|
||||
raise FileNotFoundError(f"Could not find file {filename}")
|
||||
|
||||
uuid = []
|
||||
with open(filename, "r") as uuid_file:
|
||||
for line in uuid_file:
|
||||
line = line.strip()
|
||||
if len(line) and line[0] != "#":
|
||||
uuid.append(line)
|
||||
return uuid
|
||||
TIMESTAMP_OPTION = click.option(
|
||||
"--timestamp", is_flag=True, help="Add time stamp to verbose output"
|
||||
)
|
||||
|
||||
|
||||
def get_config_dir() -> pathlib.Path:
|
||||
@ -670,103 +651,3 @@ def check_version():
|
||||
"to suppress this message and prevent osxphotos from checking for latest version.",
|
||||
err=True,
|
||||
)
|
||||
|
||||
|
||||
def query_options_from_kwargs(**kwargs) -> QueryOptions:
|
||||
"""Validate query options and create a QueryOptions instance"""
|
||||
# sanity check input args
|
||||
nonexclusive = [
|
||||
"added_after",
|
||||
"added_before",
|
||||
"added_in_last",
|
||||
"album",
|
||||
"duplicate",
|
||||
"edited",
|
||||
"exif",
|
||||
"external_edit",
|
||||
"folder",
|
||||
"from_date",
|
||||
"from_time",
|
||||
"has_raw",
|
||||
"keyword",
|
||||
"label",
|
||||
"max_size",
|
||||
"min_size",
|
||||
"name",
|
||||
"person",
|
||||
"query_eval",
|
||||
"query_function",
|
||||
"regex",
|
||||
"selected",
|
||||
"to_date",
|
||||
"to_time",
|
||||
"uti",
|
||||
"uuid_from_file",
|
||||
"uuid",
|
||||
"year",
|
||||
]
|
||||
exclusive = [
|
||||
("burst", "not_burst"),
|
||||
("cloudasset", "not_cloudasset"),
|
||||
("favorite", "not_favorite"),
|
||||
("has_comment", "no_comment"),
|
||||
("has_likes", "no_likes"),
|
||||
("hdr", "not_hdr"),
|
||||
("hidden", "not_hidden"),
|
||||
("in_album", "not_in_album"),
|
||||
("incloud", "not_incloud"),
|
||||
("live", "not_live"),
|
||||
("location", "no_location"),
|
||||
("keyword", "no_keyword"),
|
||||
("missing", "not_missing"),
|
||||
("only_photos", "only_movies"),
|
||||
("panorama", "not_panorama"),
|
||||
("portrait", "not_portrait"),
|
||||
("screenshot", "not_screenshot"),
|
||||
("selfie", "not_selfie"),
|
||||
("shared", "not_shared"),
|
||||
("slow_mo", "not_slow_mo"),
|
||||
("time_lapse", "not_time_lapse"),
|
||||
("is_reference", "not_reference"),
|
||||
]
|
||||
# print help if no non-exclusive term or a double exclusive term is given
|
||||
# TODO: add option to validate requiring at least one query arg
|
||||
if any(all([kwargs[b], kwargs[n]]) for b, n in exclusive) or any(
|
||||
[
|
||||
all([any(kwargs["title"]), kwargs["no_title"]]),
|
||||
all([any(kwargs["description"]), kwargs["no_description"]]),
|
||||
all([any(kwargs["place"]), kwargs["no_place"]]),
|
||||
all([any(kwargs["keyword"]), kwargs["no_keyword"]]),
|
||||
]
|
||||
):
|
||||
raise IncompatibleQueryOptions
|
||||
|
||||
# can also be used with --deleted/--not-deleted which are not part of
|
||||
# standard query options
|
||||
try:
|
||||
if kwargs["deleted"] and kwargs["not_deleted"]:
|
||||
raise IncompatibleQueryOptions
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
# actually have something to query
|
||||
include_photos = True
|
||||
include_movies = True # default searches for everything
|
||||
if kwargs["only_movies"]:
|
||||
include_photos = False
|
||||
if kwargs["only_photos"]:
|
||||
include_movies = False
|
||||
|
||||
# load UUIDs if necessary and append to any uuids passed with --uuid
|
||||
uuid = None
|
||||
if kwargs["uuid_from_file"]:
|
||||
uuid_list = list(kwargs["uuid"]) # Click option is a tuple
|
||||
uuid_list.extend(load_uuid_from_file(kwargs["uuid_from_file"]))
|
||||
uuid = tuple(uuid_list)
|
||||
|
||||
query_fields = [field.name for field in dataclasses.fields(QueryOptions)]
|
||||
query_dict = {field: kwargs.get(field) for field in query_fields}
|
||||
query_dict["photos"] = include_photos
|
||||
query_dict["movies"] = include_movies
|
||||
query_dict["uuid"] = uuid
|
||||
return QueryOptions(**query_dict)
|
||||
|
||||
@ -9,7 +9,15 @@ from rich import print
|
||||
import osxphotos
|
||||
from osxphotos._constants import _PHOTOS_4_VERSION, _UNKNOWN_PLACE
|
||||
|
||||
from .common import DB_ARGUMENT, DB_OPTION, JSON_OPTION, OSXPHOTOS_HIDDEN, get_photos_db
|
||||
from .common import (
|
||||
DB_ARGUMENT,
|
||||
DB_OPTION,
|
||||
JSON_OPTION,
|
||||
OSXPHOTOS_HIDDEN,
|
||||
TIMESTAMP_OPTION,
|
||||
VERBOSE_OPTION,
|
||||
get_photos_db,
|
||||
)
|
||||
from .list import _list_libraries
|
||||
from .verbose import verbose_print
|
||||
|
||||
@ -31,13 +39,14 @@ from .verbose import verbose_print
|
||||
"May be repeated to include multiple UUIDs.",
|
||||
multiple=True,
|
||||
)
|
||||
@click.option("--verbose", "-V", "verbose", is_flag=True, help="Print verbose output.")
|
||||
@VERBOSE_OPTION
|
||||
@TIMESTAMP_OPTION
|
||||
@click.pass_obj
|
||||
@click.pass_context
|
||||
def debug_dump(ctx, cli_obj, db, photos_library, dump, uuid, verbose):
|
||||
def debug_dump(ctx, cli_obj, db, photos_library, dump, uuid, verbose_flag, timestamp):
|
||||
"""Print out debug info"""
|
||||
|
||||
verbose_ = verbose_print(verbose, rich=True)
|
||||
verbose = verbose_print(verbose_flag, timestamp)
|
||||
db = get_photos_db(*photos_library, db, cli_obj.db)
|
||||
if db is None:
|
||||
click.echo(ctx.obj.group.commands["debug-dump"].get_help(ctx), err=True)
|
||||
@ -47,7 +56,7 @@ def debug_dump(ctx, cli_obj, db, photos_library, dump, uuid, verbose):
|
||||
|
||||
start_t = time.perf_counter()
|
||||
print(f"Opening database: {db}")
|
||||
photosdb = osxphotos.PhotosDB(dbfile=db, verbose=verbose_)
|
||||
photosdb = osxphotos.PhotosDB(dbfile=db, verbose=verbose)
|
||||
stop_t = time.perf_counter()
|
||||
print(f"Done; took {(stop_t-start_t):.2f} seconds")
|
||||
|
||||
|
||||
@ -17,15 +17,14 @@ from osxphotos.fileutil import FileUtil, FileUtilNoOp
|
||||
from osxphotos.photoexporter import ExportOptions, ExportResults, PhotoExporter
|
||||
from osxphotos.utils import pluralize
|
||||
|
||||
from .click_rich_echo import (
|
||||
rich_click_echo,
|
||||
rich_echo_error,
|
||||
set_rich_console,
|
||||
set_rich_theme,
|
||||
set_rich_timestamp,
|
||||
from .click_rich_echo import rich_click_echo, rich_echo_error
|
||||
from .common import (
|
||||
DB_OPTION,
|
||||
THEME_OPTION,
|
||||
TIMESTAMP_OPTION,
|
||||
VERBOSE_OPTION,
|
||||
get_photos_db,
|
||||
)
|
||||
from .color_themes import get_theme
|
||||
from .common import DB_OPTION, THEME_OPTION, get_photos_db
|
||||
from .export import export, render_and_validate_report
|
||||
from .param_types import ExportDBType, TemplateString
|
||||
from .report_writer import ReportWriterNoOp, export_report_writer_factory
|
||||
@ -166,8 +165,8 @@ from .verbose import get_verbose_console, verbose_print
|
||||
help="If used with --report, add data to existing report file instead of overwriting it. "
|
||||
"See also --report.",
|
||||
)
|
||||
@click.option("--verbose", "-V", is_flag=True, help="Print verbose output.")
|
||||
@click.option("--timestamp", is_flag=True, help="Add time stamp to verbose output")
|
||||
@VERBOSE_OPTION
|
||||
@TIMESTAMP_OPTION
|
||||
@click.option(
|
||||
"--dry-run",
|
||||
is_flag=True,
|
||||
@ -203,7 +202,7 @@ def exiftool(
|
||||
save_config,
|
||||
theme,
|
||||
timestamp,
|
||||
verbose,
|
||||
verbose_flag,
|
||||
):
|
||||
"""Run exiftool on previously exported files to update metadata.
|
||||
|
||||
@ -235,6 +234,7 @@ def exiftool(
|
||||
|
||||
# need to ensure --exiftool is true in the config options
|
||||
locals_["exiftool"] = True
|
||||
locals_["verbose"] = verbose_flag
|
||||
config = ConfigOptions(
|
||||
"export",
|
||||
locals_,
|
||||
@ -249,14 +249,7 @@ def exiftool(
|
||||
"save_config",
|
||||
],
|
||||
)
|
||||
color_theme = get_theme(theme)
|
||||
verbose_ = verbose_print(
|
||||
verbose, timestamp, rich=True, theme=color_theme, highlight=False
|
||||
)
|
||||
# set console for rich_echo to be same as for verbose_
|
||||
set_rich_console(get_verbose_console())
|
||||
set_rich_theme(color_theme)
|
||||
set_rich_timestamp(timestamp)
|
||||
verbose = verbose_print(verbose_flag, timestamp, theme=theme)
|
||||
|
||||
# load config options from either file or export database
|
||||
# values already set in config will take precedence over any values
|
||||
@ -269,26 +262,16 @@ def exiftool(
|
||||
f"[error]Error parsing {load_config} config file: {e.message}", err=True
|
||||
)
|
||||
sys.exit(1)
|
||||
verbose_(f"Loaded options from file [filepath]{load_config}")
|
||||
verbose(f"Loaded options from file [filepath]{load_config}")
|
||||
elif db_config:
|
||||
config = export_db_get_config(exportdb, config)
|
||||
verbose_("Loaded options from export database")
|
||||
verbose("Loaded options from export database")
|
||||
|
||||
# from here on out, use config.param_name instead of using the params passed into the function
|
||||
# as the values may have been updated from config file or database
|
||||
if load_config or db_config:
|
||||
# config file might have changed verbose
|
||||
color_theme = get_theme(config.theme)
|
||||
verbose_ = verbose_print(
|
||||
config.verbose,
|
||||
config.timestamp,
|
||||
rich=True,
|
||||
theme=color_theme,
|
||||
highlight=False,
|
||||
)
|
||||
# set console for rich_echo to be same as for verbose_
|
||||
set_rich_console(get_verbose_console())
|
||||
set_rich_timestamp(config.timestamp)
|
||||
verbose = verbose_print(config.verbose, config.timestamp, theme=theme)
|
||||
|
||||
# validate options
|
||||
if append and not report:
|
||||
@ -298,14 +281,14 @@ def exiftool(
|
||||
config.db = get_photos_db(config.db)
|
||||
|
||||
if save_config:
|
||||
verbose_(f"Saving options to config file '[filepath]{save_config}'")
|
||||
verbose(f"Saving options to config file '[filepath]{save_config}'")
|
||||
config.write_to_file(save_config)
|
||||
|
||||
process_files(exportdb, export_dir, verbose=verbose_, options=config)
|
||||
process_files(exportdb, export_dir, verbose=verbose, options=config)
|
||||
|
||||
|
||||
def process_files(
|
||||
exportdb: str, export_dir: str, verbose: Callable, options: ConfigOptions
|
||||
exportdb: str, export_dir: str, verbose: Callable[..., None], options: ConfigOptions
|
||||
):
|
||||
"""Process files in the export database.
|
||||
|
||||
@ -362,6 +345,12 @@ def process_files(
|
||||
hardlink_ok = True
|
||||
verbose(f"Processing file [filepath]{file}[/] ([num]{count}/{total}[/num])")
|
||||
photo = photosdb.get_photo(uuid)
|
||||
if not photo:
|
||||
verbose(
|
||||
f"Could not find photo for [filepath]{file}[/] ([uuid]{uuid}[/])"
|
||||
)
|
||||
report_writer.write(ExportResults(missing=[file]))
|
||||
continue
|
||||
export_options = ExportOptions(
|
||||
description_template=options.description_template,
|
||||
dry_run=options.dry_run,
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
import os
|
||||
import pathlib
|
||||
import platform
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
@ -54,19 +55,16 @@ from osxphotos.photokit import (
|
||||
)
|
||||
from osxphotos.photosalbum import PhotosAlbum
|
||||
from osxphotos.phototemplate import PhotoTemplate, RenderOptions
|
||||
from osxphotos.queryoptions import QueryOptions
|
||||
from osxphotos.queryoptions import QueryOptions, load_uuid_from_file
|
||||
from osxphotos.uti import get_preferred_uti_extension
|
||||
from osxphotos.utils import format_sec_to_hhmmss, normalize_fs_path, pluralize
|
||||
|
||||
from .click_rich_echo import (
|
||||
rich_click_echo,
|
||||
rich_echo,
|
||||
rich_echo_error,
|
||||
set_rich_console,
|
||||
set_rich_theme,
|
||||
set_rich_timestamp,
|
||||
from osxphotos.utils import (
|
||||
get_macos_version,
|
||||
format_sec_to_hhmmss,
|
||||
normalize_fs_path,
|
||||
pluralize,
|
||||
)
|
||||
from .color_themes import get_theme
|
||||
|
||||
from .click_rich_echo import rich_click_echo, rich_echo, rich_echo_error
|
||||
from .common import (
|
||||
CLI_COLOR_ERROR,
|
||||
CLI_COLOR_WARNING,
|
||||
@ -78,8 +76,9 @@ from .common import (
|
||||
OSXPHOTOS_HIDDEN,
|
||||
QUERY_OPTIONS,
|
||||
THEME_OPTION,
|
||||
TIMESTAMP_OPTION,
|
||||
VERBOSE_OPTION,
|
||||
get_photos_db,
|
||||
load_uuid_from_file,
|
||||
noop,
|
||||
)
|
||||
from .help import ExportCommand, get_help_msg
|
||||
@ -87,13 +86,13 @@ from .list import _list_libraries
|
||||
from .param_types import ExportDBType, FunctionCall, TemplateString
|
||||
from .report_writer import ReportWriterNoOp, export_report_writer_factory
|
||||
from .rich_progress import rich_progress
|
||||
from .verbose import get_verbose_console, time_stamp, verbose_print
|
||||
from .verbose import get_verbose_console, verbose_print
|
||||
|
||||
|
||||
@click.command(cls=ExportCommand)
|
||||
@DB_OPTION
|
||||
@click.option("--verbose", "-V", "verbose", is_flag=True, help="Print verbose output.")
|
||||
@click.option("--timestamp", is_flag=True, help="Add time stamp to verbose output")
|
||||
@VERBOSE_OPTION
|
||||
@TIMESTAMP_OPTION
|
||||
@click.option(
|
||||
"--no-progress", is_flag=True, help="Do not display progress bar during export."
|
||||
)
|
||||
@ -875,7 +874,7 @@ def export(
|
||||
uti,
|
||||
uuid,
|
||||
uuid_from_file,
|
||||
verbose,
|
||||
verbose_flag,
|
||||
xattr_template,
|
||||
year,
|
||||
# debug, # debug, watch, breakpoint handled in cli/__init__.py
|
||||
@ -908,6 +907,10 @@ def export(
|
||||
locals_ = locals()
|
||||
set_crash_data("locals", locals_)
|
||||
|
||||
# config expects --verbose to be named "verbose" not "verbose_flag"
|
||||
locals_["verbose"] = verbose_flag
|
||||
del locals_["verbose_flag"]
|
||||
|
||||
# NOTE: because of the way ConfigOptions works, Click options must not
|
||||
# set defaults which are not None or False. If defaults need to be set
|
||||
# do so below after load_config and save_config are handled.
|
||||
@ -917,14 +920,7 @@ def export(
|
||||
ignore=["ctx", "cli_obj", "dest", "load_config", "save_config", "config_only"],
|
||||
)
|
||||
|
||||
color_theme = get_theme(theme)
|
||||
verbose_ = verbose_print(
|
||||
verbose, timestamp, rich=True, theme=color_theme, highlight=False
|
||||
)
|
||||
# set console for rich_echo to be same as for verbose_
|
||||
set_rich_console(get_verbose_console())
|
||||
set_rich_theme(color_theme)
|
||||
set_rich_timestamp(timestamp)
|
||||
verbose = verbose_print(verbose=verbose_flag, timestamp=timestamp, theme=theme)
|
||||
|
||||
if load_config:
|
||||
try:
|
||||
@ -1095,23 +1091,20 @@ def export(
|
||||
uti = cfg.uti
|
||||
uuid = cfg.uuid
|
||||
uuid_from_file = cfg.uuid_from_file
|
||||
verbose = cfg.verbose
|
||||
# this is the one option that is named differently in the config file than the variable passed by --verbose (verbose_flag)
|
||||
verbose_flag = cfg.verbose
|
||||
xattr_template = cfg.xattr_template
|
||||
year = cfg.year
|
||||
# config file might have changed verbose
|
||||
color_theme = get_theme(theme)
|
||||
verbose_ = verbose_print(
|
||||
verbose, timestamp, rich=True, theme=color_theme, highlight=False
|
||||
)
|
||||
# set console for rich_echo to be same as for verbose_
|
||||
set_rich_console(get_verbose_console())
|
||||
set_rich_timestamp(timestamp)
|
||||
|
||||
verbose_(f"Loaded options from file [filepath]{load_config}")
|
||||
verbose = verbose_print(verbose=verbose_flag, timestamp=timestamp, theme=theme)
|
||||
verbose(f"Loaded options from file [filepath]{load_config}")
|
||||
|
||||
set_crash_data("cfg", cfg.asdict())
|
||||
|
||||
verbose_(f"osxphotos version {__version__}")
|
||||
verbose(f"osxphotos version: {__version__}")
|
||||
verbose(f"Python version: {sys.version}")
|
||||
verbose(f"Platform: {platform.platform()}, {'.'.join(get_macos_version())}")
|
||||
verbose(f"Verbose level: {verbose_flag}")
|
||||
|
||||
# validate options
|
||||
exclusive_options = [
|
||||
@ -1195,7 +1188,7 @@ def export(
|
||||
sys.exit(1)
|
||||
|
||||
if save_config:
|
||||
verbose_(f"Saving options to config file '[filepath]{save_config}'")
|
||||
verbose(f"Saving options to config file '[filepath]{save_config}'")
|
||||
cfg.write_to_file(save_config)
|
||||
if config_only:
|
||||
rich_echo(f"Saved config file to '[filepath]{save_config}'")
|
||||
@ -1260,7 +1253,7 @@ def export(
|
||||
ctx.exit(1)
|
||||
|
||||
if any([exiftool, exiftool_merge_keywords, exiftool_merge_persons]):
|
||||
verbose_(f"exiftool path: [filepath]{exiftool_path}")
|
||||
verbose(f"exiftool path: [filepath]{exiftool_path}")
|
||||
|
||||
# default searches for everything
|
||||
photos = True
|
||||
@ -1331,14 +1324,14 @@ def export(
|
||||
)
|
||||
fileutil = FileUtilShUtil if alt_copy else FileUtil
|
||||
|
||||
if verbose_:
|
||||
if verbose:
|
||||
if export_db.was_created:
|
||||
verbose_(f"Created export database [filepath]{export_db_path}")
|
||||
verbose(f"Created export database [filepath]{export_db_path}")
|
||||
else:
|
||||
verbose_(f"Using export database [filepath]{export_db_path}")
|
||||
verbose(f"Using export database [filepath]{export_db_path}")
|
||||
upgraded = export_db.was_upgraded
|
||||
if upgraded:
|
||||
verbose_(
|
||||
verbose(
|
||||
f"Upgraded export database [filepath]{export_db_path}[/] from version [num]{upgraded[0]}[/] to [num]{upgraded[1]}[/]"
|
||||
)
|
||||
|
||||
@ -1346,7 +1339,7 @@ def export(
|
||||
export_db.set_config(cfg.write_to_str())
|
||||
|
||||
photosdb = osxphotos.PhotosDB(
|
||||
dbfile=db, verbose=verbose_, exiftool=exiftool_path, rich=True
|
||||
dbfile=db, verbose=verbose, exiftool=exiftool_path, rich=True
|
||||
)
|
||||
|
||||
# enable beta features if requested
|
||||
@ -1478,17 +1471,17 @@ def export(
|
||||
|
||||
# set up for --add-export-to-album if needed
|
||||
album_export = (
|
||||
PhotosAlbum(add_exported_to_album, verbose=verbose_)
|
||||
PhotosAlbum(add_exported_to_album, verbose=verbose)
|
||||
if add_exported_to_album
|
||||
else None
|
||||
)
|
||||
album_skipped = (
|
||||
PhotosAlbum(add_skipped_to_album, verbose=verbose_)
|
||||
PhotosAlbum(add_skipped_to_album, verbose=verbose)
|
||||
if add_skipped_to_album
|
||||
else None
|
||||
)
|
||||
album_missing = (
|
||||
PhotosAlbum(add_missing_to_album, verbose=verbose_)
|
||||
PhotosAlbum(add_missing_to_album, verbose=verbose)
|
||||
if add_missing_to_album
|
||||
else None
|
||||
)
|
||||
@ -1552,17 +1545,17 @@ def export(
|
||||
update_errors=update_errors,
|
||||
use_photokit=use_photokit,
|
||||
use_photos_export=use_photos_export,
|
||||
verbose_=verbose_,
|
||||
verbose=verbose,
|
||||
tmpdir=tmpdir,
|
||||
)
|
||||
|
||||
if post_function:
|
||||
for function in post_function:
|
||||
# post function is tuple of (function, filename.py::function_name)
|
||||
verbose_(f"Calling post-function [bold]{function[1]}")
|
||||
verbose(f"Calling post-function [bold]{function[1]}")
|
||||
if not dry_run:
|
||||
try:
|
||||
function[0](p, export_results, verbose_)
|
||||
function[0](p, export_results, verbose)
|
||||
except Exception as e:
|
||||
rich_echo_error(
|
||||
f"[error]Error running post-function [italic]{function[1]}[/italic]: {e}"
|
||||
@ -1576,7 +1569,7 @@ def export(
|
||||
dry_run=dry_run,
|
||||
exiftool_path=exiftool_path,
|
||||
export_db=export_db,
|
||||
verbose_=verbose_,
|
||||
verbose=verbose,
|
||||
)
|
||||
|
||||
if album_export and export_results.exported:
|
||||
@ -1646,7 +1639,7 @@ def export(
|
||||
finder_tag_template=finder_tag_template,
|
||||
strip=strip,
|
||||
export_dir=dest,
|
||||
verbose_=verbose_,
|
||||
verbose=verbose,
|
||||
)
|
||||
export_results.xattr_written.extend(tags_written)
|
||||
export_results.xattr_skipped.extend(tags_skipped)
|
||||
@ -1660,7 +1653,7 @@ def export(
|
||||
xattr_template,
|
||||
strip=strip,
|
||||
export_dir=dest,
|
||||
verbose_=verbose_,
|
||||
verbose=verbose,
|
||||
)
|
||||
export_results.xattr_written.extend(xattr_written)
|
||||
export_results.xattr_skipped.extend(xattr_skipped)
|
||||
@ -1758,7 +1751,7 @@ def export(
|
||||
all_files += files_to_keep
|
||||
rich_echo(f"Cleaning up [filepath]{dest}")
|
||||
cleaned_files, cleaned_dirs = cleanup_files(
|
||||
dest, all_files, dirs_to_keep, fileutil, verbose_=verbose_
|
||||
dest, all_files, dirs_to_keep, fileutil, verbose=verbose
|
||||
)
|
||||
file_str = "files" if len(cleaned_files) != 1 else "file"
|
||||
dir_str = "directories" if len(cleaned_dirs) != 1 else "directory"
|
||||
@ -1775,12 +1768,12 @@ def export(
|
||||
export_db.set_export_results(results)
|
||||
|
||||
if report:
|
||||
verbose_(f"Wrote export report to [filepath]{report}")
|
||||
verbose(f"Wrote export report to [filepath]{report}")
|
||||
report_writer.close()
|
||||
|
||||
# close export_db and write changes if needed
|
||||
if ramdb and not dry_run:
|
||||
verbose_(f"Writing export database changes back to [filepath]{export_db.path}")
|
||||
verbose(f"Writing export database changes back to [filepath]{export_db.path}")
|
||||
export_db.write_to_disk()
|
||||
export_db.close()
|
||||
|
||||
@ -1788,7 +1781,7 @@ def export(
|
||||
def export_photo(
|
||||
photo=None,
|
||||
dest=None,
|
||||
verbose_=None,
|
||||
verbose=None,
|
||||
export_by_date=None,
|
||||
sidecar=None,
|
||||
sidecar_drop_ext=False,
|
||||
@ -1885,7 +1878,7 @@ def export_photo(
|
||||
update: bool, only export updated photos
|
||||
update_errors: bool, attempt to re-export photos that previously produced errors even if they otherwise would not be exported
|
||||
use_photos_export: bool; if True forces the use of AppleScript to export even if photo not missing
|
||||
verbose_: callable for verbose output
|
||||
verbose: callable for verbose output
|
||||
tmpdir: optional str; temporary directory to use for export
|
||||
Returns:
|
||||
list of path(s) of exported photo or None if photo was missing
|
||||
@ -1908,7 +1901,7 @@ def export_photo(
|
||||
# requested edited version but it's missing, download original
|
||||
export_original = True
|
||||
export_edited = False
|
||||
verbose_(
|
||||
verbose(
|
||||
f"Edited file for [filename]{photo.original_filename}[/] is missing, exporting original"
|
||||
)
|
||||
|
||||
@ -2004,7 +1997,7 @@ def export_photo(
|
||||
)
|
||||
original_filename = str(original_filename)
|
||||
|
||||
verbose_(
|
||||
verbose(
|
||||
f"Exporting [filename]{photo.original_filename}[/] ([filename]{photo.filename}[/]) ([count]{photo_num}/{num_photos}[/])"
|
||||
)
|
||||
|
||||
@ -2052,7 +2045,7 @@ def export_photo(
|
||||
update_errors=update_errors,
|
||||
use_photos_export=use_photos_export,
|
||||
use_photokit=use_photokit,
|
||||
verbose_=verbose_,
|
||||
verbose=verbose,
|
||||
tmpdir=tmpdir,
|
||||
)
|
||||
|
||||
@ -2120,7 +2113,7 @@ def export_photo(
|
||||
f"{edited_filename.stem}{rendered_edited_suffix}{edited_ext}"
|
||||
)
|
||||
|
||||
verbose_(
|
||||
verbose(
|
||||
f"Exporting edited version of [filename]{photo.original_filename}[/filename] ([filename]{photo.filename}[/filename])"
|
||||
)
|
||||
|
||||
@ -2168,7 +2161,7 @@ def export_photo(
|
||||
update_errors=update_errors,
|
||||
use_photos_export=use_photos_export,
|
||||
use_photokit=use_photokit,
|
||||
verbose_=verbose_,
|
||||
verbose=verbose,
|
||||
tmpdir=tmpdir,
|
||||
)
|
||||
|
||||
@ -2255,7 +2248,7 @@ def export_photo_to_directory(
|
||||
update_errors,
|
||||
use_photos_export,
|
||||
use_photokit,
|
||||
verbose_,
|
||||
verbose,
|
||||
tmpdir,
|
||||
):
|
||||
"""Export photo to directory dest_path"""
|
||||
@ -2267,7 +2260,7 @@ def export_photo_to_directory(
|
||||
if photo.intrash and not photo_path and not preview_if_missing:
|
||||
# skip deleted files if they're missing
|
||||
# as AppleScript/PhotoKit cannot export deleted photos
|
||||
verbose_(
|
||||
verbose(
|
||||
f"Skipping missing deleted photo {photo.original_filename} ({photo.uuid})"
|
||||
)
|
||||
results.missing.append(str(pathlib.Path(dest_path) / filename))
|
||||
@ -2276,7 +2269,7 @@ def export_photo_to_directory(
|
||||
render_options = RenderOptions(export_dir=export_dir, dest_path=dest_path)
|
||||
|
||||
if not export_original and not edited:
|
||||
verbose_(f"Skipping original version of [filename]{photo.original_filename}")
|
||||
verbose(f"Skipping original version of [filename]{photo.original_filename}")
|
||||
return results
|
||||
|
||||
tries = 0
|
||||
@ -2322,14 +2315,14 @@ def export_photo_to_directory(
|
||||
use_persons_as_keywords=person_keyword,
|
||||
use_photokit=use_photokit,
|
||||
use_photos_export=use_photos_export,
|
||||
verbose=verbose_,
|
||||
verbose=verbose,
|
||||
)
|
||||
exporter = PhotoExporter(photo)
|
||||
export_results = exporter.export(
|
||||
dest=dest_path, filename=filename, options=export_options
|
||||
)
|
||||
for warning_ in export_results.exiftool_warning:
|
||||
verbose_(
|
||||
verbose(
|
||||
f"[warning]exiftool warning for file {warning_[0]}: {warning_[1]}"
|
||||
)
|
||||
for error_ in export_results.exiftool_error:
|
||||
@ -2364,19 +2357,19 @@ def export_photo_to_directory(
|
||||
f"Retrying export for photo ([uuid]{photo.uuid}[/uuid]: [filename]{photo.original_filename}[/filename])"
|
||||
)
|
||||
|
||||
if verbose_:
|
||||
if verbose:
|
||||
if update or force_update:
|
||||
for new in results.new:
|
||||
verbose_(f"Exported new file [filepath]{new}")
|
||||
verbose(f"Exported new file [filepath]{new}")
|
||||
for updated in results.updated:
|
||||
verbose_(f"Exported updated file [filepath]{updated}")
|
||||
verbose(f"Exported updated file [filepath]{updated}")
|
||||
for skipped in results.skipped:
|
||||
verbose_(f"Skipped up to date file [filepath]{skipped}")
|
||||
verbose(f"Skipped up to date file [filepath]{skipped}")
|
||||
else:
|
||||
for exported in results.exported:
|
||||
verbose_(f"Exported [filepath]{exported}")
|
||||
verbose(f"Exported [filepath]{exported}")
|
||||
for touched in results.touched:
|
||||
verbose_(f"Touched date on file [filepath]{touched}")
|
||||
verbose(f"Touched date on file [filepath]{touched}")
|
||||
|
||||
return results
|
||||
|
||||
@ -2572,7 +2565,7 @@ def collect_files_to_keep(
|
||||
return files_to_keep, dirs_to_keep
|
||||
|
||||
|
||||
def cleanup_files(dest_path, files_to_keep, dirs_to_keep, fileutil, verbose_):
|
||||
def cleanup_files(dest_path, files_to_keep, dirs_to_keep, fileutil, verbose):
|
||||
"""cleanup dest_path by deleting and files and empty directories
|
||||
not in files_to_keep
|
||||
|
||||
@ -2581,7 +2574,7 @@ def cleanup_files(dest_path, files_to_keep, dirs_to_keep, fileutil, verbose_):
|
||||
files_to_keep: list of full file paths to keep (not delete)
|
||||
dirs_to_keep: list of full dir paths to keep (not delete if they are empty)
|
||||
fileutil: FileUtil object
|
||||
verbose_: verbose callable for printing verbose output
|
||||
verbose: verbose callable for printing verbose output
|
||||
|
||||
Returns:
|
||||
tuple of (list of files deleted, list of directories deleted)
|
||||
@ -2593,7 +2586,7 @@ def cleanup_files(dest_path, files_to_keep, dirs_to_keep, fileutil, verbose_):
|
||||
deleted_files = []
|
||||
for p in pathlib.Path(dest_path).rglob("*"):
|
||||
if p.is_file() and normalize_fs_path(str(p).lower()) not in keepers:
|
||||
verbose_(f"Deleting [filepath]{p}")
|
||||
verbose(f"Deleting [filepath]{p}")
|
||||
fileutil.unlink(p)
|
||||
deleted_files.append(str(p))
|
||||
|
||||
@ -2605,7 +2598,7 @@ def cleanup_files(dest_path, files_to_keep, dirs_to_keep, fileutil, verbose_):
|
||||
continue
|
||||
if not list(pathlib.Path(dirpath).glob("*")):
|
||||
# directory and directory is empty
|
||||
verbose_(f"Deleting empty directory {dirpath}")
|
||||
verbose(f"Deleting empty directory {dirpath}")
|
||||
fileutil.rmdir(dirpath)
|
||||
deleted_dirs.append(str(dirpath))
|
||||
|
||||
@ -2623,7 +2616,7 @@ def write_finder_tags(
|
||||
finder_tag_template=None,
|
||||
strip=False,
|
||||
export_dir=None,
|
||||
verbose_=noop,
|
||||
verbose=noop,
|
||||
):
|
||||
"""Write Finder tags (extended attributes) to files; only writes attributes if attributes on file differ from what would be written
|
||||
|
||||
@ -2637,7 +2630,7 @@ def write_finder_tags(
|
||||
exiftool_merge_keywords: if True, include any keywords in the exif data of the source image as keywords
|
||||
finder_tag_template: list of templates to evaluate for determining Finder tags
|
||||
export_dir: value to use for {export_dir} template
|
||||
verbose_: function to call to print verbose messages
|
||||
verbose: function to call to print verbose messages
|
||||
|
||||
Returns:
|
||||
(list of file paths that were updated with new Finder tags, list of file paths skipped because Finder tags didn't need updating)
|
||||
@ -2697,11 +2690,11 @@ def write_finder_tags(
|
||||
for f in files:
|
||||
md = OSXMetaData(f)
|
||||
if sorted(md.tags) != sorted(tags):
|
||||
verbose_(f"Writing Finder tags to {f}")
|
||||
verbose(f"Writing Finder tags to {f}")
|
||||
md.tags = tags
|
||||
written.append(f)
|
||||
else:
|
||||
verbose_(f"Skipping Finder tags for {f}: nothing to do")
|
||||
verbose(f"Skipping Finder tags for {f}: nothing to do")
|
||||
skipped.append(f)
|
||||
|
||||
return (written, skipped)
|
||||
@ -2713,14 +2706,17 @@ def write_extended_attributes(
|
||||
xattr_template,
|
||||
strip=False,
|
||||
export_dir=None,
|
||||
verbose_=noop,
|
||||
verbose=noop,
|
||||
):
|
||||
"""Writes extended attributes to exported files
|
||||
|
||||
Args:
|
||||
photo: a PhotoInfo object
|
||||
files: list of file paths to write extended attributes to
|
||||
xattr_template: list of tuples: (attribute name, attribute template)
|
||||
strip: xattr_template: list of tuples: (attribute name, attribute template)
|
||||
export_dir: value to use for {export_dir} template
|
||||
verbose: function to call to print verbose messages
|
||||
|
||||
Returns:
|
||||
tuple(list of file paths that were updated with new attributes, list of file paths skipped because attributes didn't need updating)
|
||||
@ -2770,10 +2766,10 @@ def write_extended_attributes(
|
||||
if (not file_value and not value) or file_value == value:
|
||||
# if both not set or both equal, nothing to do
|
||||
# get returns None if not set and value will be [] if not set so can't directly compare
|
||||
verbose_(f"Skipping extended attribute {attr} for {f}: nothing to do")
|
||||
verbose(f"Skipping extended attribute {attr} for {f}: nothing to do")
|
||||
skipped.add(f)
|
||||
else:
|
||||
verbose_(f"Writing extended attribute {attr} to {f}")
|
||||
verbose(f"Writing extended attribute {attr} to {f}")
|
||||
md.set(attr, value)
|
||||
written.add(f)
|
||||
|
||||
@ -2788,7 +2784,7 @@ def run_post_command(
|
||||
dry_run,
|
||||
exiftool_path,
|
||||
export_db,
|
||||
verbose_,
|
||||
verbose,
|
||||
):
|
||||
# todo: pass in RenderOptions from export? (e.g. so it contains strip, etc?)
|
||||
# todo: need a shell_quote template type:
|
||||
@ -2805,7 +2801,7 @@ def run_post_command(
|
||||
command, _ = template.render(command_template, options=render_options)
|
||||
command = command[0] if command else None
|
||||
if command:
|
||||
verbose_(f'Running command: "{command}"')
|
||||
verbose(f'Running command: "{command}"')
|
||||
if not dry_run:
|
||||
args = shlex.split(command)
|
||||
cwd = pathlib.Path(f).parent
|
||||
|
||||
@ -34,6 +34,7 @@ from .click_rich_echo import (
|
||||
set_rich_theme,
|
||||
)
|
||||
from .color_themes import get_theme
|
||||
from .common import TIMESTAMP_OPTION, VERBOSE_OPTION
|
||||
from .export import render_and_validate_report
|
||||
from .param_types import TemplateString
|
||||
from .report_writer import export_report_writer_factory
|
||||
@ -150,7 +151,8 @@ from .verbose import get_verbose_console, verbose_print
|
||||
help="If used with --report, add data to existing report file instead of overwriting it. "
|
||||
"See also --report.",
|
||||
)
|
||||
@click.option("--verbose", "-V", is_flag=True, help="Print verbose output.")
|
||||
@VERBOSE_OPTION
|
||||
@TIMESTAMP_OPTION
|
||||
@click.option(
|
||||
"--dry-run",
|
||||
is_flag=True,
|
||||
@ -171,6 +173,7 @@ def exportdb(
|
||||
report,
|
||||
save_config,
|
||||
sql,
|
||||
timestamp,
|
||||
touch_file,
|
||||
update_signatures,
|
||||
uuid_files,
|
||||
@ -178,17 +181,11 @@ def exportdb(
|
||||
delete_uuid,
|
||||
delete_file,
|
||||
vacuum,
|
||||
verbose,
|
||||
verbose_flag,
|
||||
version,
|
||||
):
|
||||
"""Utilities for working with the osxphotos export database"""
|
||||
color_theme = get_theme()
|
||||
verbose_ = verbose_print(
|
||||
verbose, timestamp=False, rich=True, theme=color_theme, highlight=False
|
||||
)
|
||||
# set console for rich_echo to be same as for verbose_
|
||||
set_rich_console(get_verbose_console(theme=color_theme))
|
||||
set_rich_theme(color_theme)
|
||||
verbose = verbose_print(verbose_flag, timestamp=timestamp)
|
||||
|
||||
# validate options and args
|
||||
if append and not report:
|
||||
@ -264,7 +261,7 @@ def exportdb(
|
||||
if update_signatures:
|
||||
try:
|
||||
updated, skipped = export_db_update_signatures(
|
||||
export_db, export_dir, verbose_, dry_run
|
||||
export_db, export_dir, verbose, dry_run
|
||||
)
|
||||
except Exception as e:
|
||||
rich_echo(f"[error]Error: {e}[/error]")
|
||||
@ -299,7 +296,7 @@ def exportdb(
|
||||
if check_signatures:
|
||||
try:
|
||||
matched, notmatched, skipped = export_db_check_signatures(
|
||||
export_db, export_dir, verbose_=verbose_
|
||||
export_db, export_dir, verbose_=verbose
|
||||
)
|
||||
except Exception as e:
|
||||
rich_echo(f"[error]Error: {e}[/error]")
|
||||
@ -314,7 +311,7 @@ def exportdb(
|
||||
if touch_file:
|
||||
try:
|
||||
touched, not_touched, skipped = export_db_touch_files(
|
||||
export_db, export_dir, verbose_=verbose_, dry_run=dry_run
|
||||
export_db, export_dir, verbose_=verbose, dry_run=dry_run
|
||||
)
|
||||
except Exception as e:
|
||||
rich_echo(f"[error]Error: {e}[/error]")
|
||||
|
||||
@ -27,7 +27,7 @@ from strpdatetime import strpdatetime
|
||||
|
||||
from osxphotos._constants import _OSXPHOTOS_NONE_SENTINEL
|
||||
from osxphotos._version import __version__
|
||||
from osxphotos.cli.common import get_data_dir
|
||||
from osxphotos.cli.common import TIMESTAMP_OPTION, VERBOSE_OPTION, get_data_dir
|
||||
from osxphotos.cli.help import HELP_WIDTH
|
||||
from osxphotos.cli.param_types import FunctionCall, StrpDateTimePattern, TemplateString
|
||||
from osxphotos.datetime_utils import (
|
||||
@ -44,14 +44,7 @@ from osxphotos.phototemplate import PhotoTemplate, RenderOptions
|
||||
from osxphotos.sqlitekvstore import SQLiteKVStore
|
||||
from osxphotos.utils import pluralize
|
||||
|
||||
from .click_rich_echo import (
|
||||
rich_click_echo,
|
||||
rich_echo_error,
|
||||
set_rich_console,
|
||||
set_rich_theme,
|
||||
set_rich_timestamp,
|
||||
)
|
||||
from .color_themes import get_theme
|
||||
from .click_rich_echo import rich_click_echo, rich_echo_error
|
||||
from .common import THEME_OPTION
|
||||
from .rich_progress import rich_progress
|
||||
from .verbose import get_verbose_console, verbose_print
|
||||
@ -1362,10 +1355,8 @@ class ImportCommand(click.Command):
|
||||
help="If used with --report, add data to existing report file instead of overwriting it. "
|
||||
"See also --report.",
|
||||
)
|
||||
@click.option("--verbose", "-V", "verbose_", is_flag=True, help="Print verbose output.")
|
||||
@click.option(
|
||||
"--timestamp", "-T", is_flag=True, help="Add time stamp to verbose output"
|
||||
)
|
||||
@VERBOSE_OPTION
|
||||
@TIMESTAMP_OPTION
|
||||
@click.option(
|
||||
"--no-progress", is_flag=True, help="Do not display progress bar during import."
|
||||
)
|
||||
@ -1419,19 +1410,12 @@ def import_cli(
|
||||
theme,
|
||||
timestamp,
|
||||
title,
|
||||
verbose_,
|
||||
verbose_flag,
|
||||
walk,
|
||||
):
|
||||
"""Import photos and videos into Photos."""
|
||||
|
||||
color_theme = get_theme(theme)
|
||||
verbose = verbose_print(
|
||||
verbose_, timestamp, rich=True, theme=color_theme, highlight=False
|
||||
)
|
||||
# set console for rich_echo to be same as for verbose_
|
||||
set_rich_console(get_verbose_console())
|
||||
set_rich_theme(color_theme)
|
||||
set_rich_timestamp(timestamp)
|
||||
verbose = verbose_print(verbose=verbose_flag, timestamp=timestamp, theme=theme)
|
||||
|
||||
if not files:
|
||||
echo("Nothing to import", err=True)
|
||||
|
||||
@ -19,12 +19,16 @@ from osxphotos.fileutil import FileUtil
|
||||
from osxphotos.utils import increment_filename, pluralize
|
||||
|
||||
from .click_rich_echo import rich_click_echo as echo
|
||||
from .click_rich_echo import set_rich_console, set_rich_theme, set_rich_timestamp
|
||||
from .color_themes import get_theme
|
||||
from .common import DB_OPTION, THEME_OPTION, get_photos_db
|
||||
from .common import (
|
||||
DB_OPTION,
|
||||
THEME_OPTION,
|
||||
TIMESTAMP_OPTION,
|
||||
VERBOSE_OPTION,
|
||||
get_photos_db,
|
||||
)
|
||||
from .help import get_help_msg
|
||||
from .list import _list_libraries
|
||||
from .verbose import get_verbose_console, verbose_print
|
||||
from .verbose import verbose_print
|
||||
|
||||
|
||||
@click.command(name="orphans")
|
||||
@ -36,22 +40,15 @@ from .verbose import get_verbose_console, verbose_print
|
||||
help="Export orphans to directory EXPORT_PATH. If --export not specified, orphans are listed but not exported.",
|
||||
)
|
||||
@DB_OPTION
|
||||
@click.option("--verbose", "-V", "verbose", is_flag=True, help="Print verbose output.")
|
||||
@click.option("--timestamp", is_flag=True, help="Add time stamp to verbose output")
|
||||
@VERBOSE_OPTION
|
||||
@TIMESTAMP_OPTION
|
||||
@THEME_OPTION
|
||||
@click.pass_obj
|
||||
@click.pass_context
|
||||
def orphans(ctx, cli_obj, export, db, verbose, timestamp, theme):
|
||||
def orphans(ctx, cli_obj, export, db, verbose_flag, timestamp, theme):
|
||||
"""Find orphaned photos in a Photos library"""
|
||||
|
||||
color_theme = get_theme(theme)
|
||||
verbose_ = verbose_print(
|
||||
verbose, timestamp, rich=True, theme=color_theme, highlight=False
|
||||
)
|
||||
# set console for rich_echo to be same as for verbose_
|
||||
set_rich_console(get_verbose_console())
|
||||
set_rich_theme(color_theme)
|
||||
set_rich_timestamp(timestamp)
|
||||
verbose_ = verbose_print(verbose=verbose_flag, timestamp=timestamp, theme=theme)
|
||||
|
||||
# below needed for to make CliRunner work for testing
|
||||
cli_db = cli_obj.db if cli_obj is not None else None
|
||||
|
||||
@ -11,7 +11,7 @@ from osxphotos.cli.click_rich_echo import (
|
||||
from osxphotos.debug import set_debug
|
||||
from osxphotos.photosalbum import PhotosAlbum
|
||||
from osxphotos.phototemplate import RenderOptions
|
||||
from osxphotos.queryoptions import QueryOptions
|
||||
from osxphotos.queryoptions import QueryOptions, load_uuid_from_file
|
||||
|
||||
from .color_themes import get_default_theme
|
||||
from .common import (
|
||||
@ -25,7 +25,6 @@ from .common import (
|
||||
OSXPHOTOS_HIDDEN,
|
||||
QUERY_OPTIONS,
|
||||
get_photos_db,
|
||||
load_uuid_from_file,
|
||||
)
|
||||
from .list import _list_libraries
|
||||
from .print_photo_info import print_photo_fields, print_photo_info
|
||||
|
||||
@ -13,19 +13,22 @@ from rich import pretty, print
|
||||
|
||||
import osxphotos
|
||||
from osxphotos._constants import _PHOTOS_4_VERSION
|
||||
from osxphotos.cli.click_rich_echo import rich_echo_error as echo_error
|
||||
from osxphotos.photoinfo import PhotoInfo
|
||||
from osxphotos.photosdb import PhotosDB
|
||||
from osxphotos.pyrepl import embed_repl
|
||||
from osxphotos.queryoptions import QueryOptions
|
||||
from osxphotos.queryoptions import (
|
||||
IncompatibleQueryOptions,
|
||||
QueryOptions,
|
||||
query_options_from_kwargs,
|
||||
)
|
||||
|
||||
from .common import (
|
||||
DB_ARGUMENT,
|
||||
DB_OPTION,
|
||||
DELETED_OPTIONS,
|
||||
IncompatibleQueryOptions,
|
||||
QUERY_OPTIONS,
|
||||
get_photos_db,
|
||||
query_options_from_kwargs,
|
||||
)
|
||||
|
||||
|
||||
@ -70,6 +73,11 @@ def repl(ctx, cli_obj, db, emacs, beta, **kwargs):
|
||||
logger.disabled = True
|
||||
|
||||
pretty.install()
|
||||
try:
|
||||
query_options = query_options_from_kwargs(**kwargs)
|
||||
except IncompatibleQueryOptions as e:
|
||||
echo_error(f"Incompatible query options: {e}")
|
||||
ctx.exit(1)
|
||||
print(f"python version: {sys.version}")
|
||||
print(f"osxphotos version: {osxphotos._version.__version__}")
|
||||
db = db or get_photos_db()
|
||||
@ -80,12 +88,6 @@ def repl(ctx, cli_obj, db, emacs, beta, **kwargs):
|
||||
print("Beta mode enabled")
|
||||
print("Getting photos")
|
||||
tic = time.perf_counter()
|
||||
try:
|
||||
query_options = query_options_from_kwargs(**kwargs)
|
||||
except IncompatibleQueryOptions:
|
||||
click.echo("Incompatible query options", err=True)
|
||||
click.echo(ctx.obj.group.commands["repl"].get_help(ctx), err=True)
|
||||
sys.exit(1)
|
||||
photos = _query_photos(photosdb, query_options)
|
||||
all_photos = _get_all_photos(photosdb)
|
||||
toc = time.perf_counter()
|
||||
|
||||
@ -12,7 +12,13 @@ from rich.syntax import Syntax
|
||||
|
||||
import osxphotos
|
||||
|
||||
from .common import DB_OPTION, OSXPHOTOS_SNAPSHOT_DIR, get_photos_db
|
||||
from .common import (
|
||||
DB_OPTION,
|
||||
OSXPHOTOS_SNAPSHOT_DIR,
|
||||
TIMESTAMP_OPTION,
|
||||
VERBOSE_OPTION,
|
||||
get_photos_db,
|
||||
)
|
||||
from .verbose import verbose_print
|
||||
|
||||
|
||||
@ -80,8 +86,9 @@ def snap(ctx, cli_obj, db):
|
||||
"Default is 'monokai'.",
|
||||
)
|
||||
@click.argument("db2", nargs=-1, type=click.Path(exists=True))
|
||||
@click.option("--verbose", "-V", "verbose", is_flag=True, help="Print verbose output.")
|
||||
def diff(ctx, cli_obj, db, raw_output, style, db2, verbose):
|
||||
@VERBOSE_OPTION
|
||||
@TIMESTAMP_OPTION
|
||||
def diff(ctx, cli_obj, db, raw_output, style, db2, verbose_flag, timestamp):
|
||||
"""Compare two Photos databases and print out differences
|
||||
|
||||
To use the diff command, you'll need to install sqldiff via homebrew:
|
||||
@ -110,7 +117,7 @@ def diff(ctx, cli_obj, db, raw_output, style, db2, verbose):
|
||||
Works only on Photos library versions since Catalina (10.15) or newer.
|
||||
"""
|
||||
|
||||
verbose_ = verbose_print(verbose, rich=True)
|
||||
verbose = verbose_print(verbose_flag, timestamp=timestamp)
|
||||
|
||||
sqldiff = shutil.which("sqldiff")
|
||||
if not sqldiff:
|
||||
@ -118,7 +125,7 @@ def diff(ctx, cli_obj, db, raw_output, style, db2, verbose):
|
||||
"sqldiff not found; install via homebrew (https://brew.sh/): `brew install sqldiff`"
|
||||
)
|
||||
ctx.exit(2)
|
||||
verbose_(f"sqldiff found at '{sqldiff}'")
|
||||
verbose(f"sqldiff found at '{sqldiff}'")
|
||||
|
||||
db = get_photos_db(db, cli_obj.db)
|
||||
db_path = pathlib.Path(db)
|
||||
@ -133,7 +140,7 @@ def diff(ctx, cli_obj, db, raw_output, style, db2, verbose):
|
||||
else:
|
||||
# get most recent snapshot
|
||||
db_folder = os.environ.get("OSXPHOTOS_SNAPSHOT", OSXPHOTOS_SNAPSHOT_DIR)
|
||||
verbose_(f"Using snapshot folder: '{db_folder}'")
|
||||
verbose(f"Using snapshot folder: '{db_folder}'")
|
||||
folders = sorted([f for f in pathlib.Path(db_folder).glob("*") if f.is_dir()])
|
||||
folder_2 = folders[-1]
|
||||
db_2 = folder_2 / "Photos.sqlite"
|
||||
@ -143,7 +150,7 @@ def diff(ctx, cli_obj, db, raw_output, style, db2, verbose):
|
||||
if not db_2.exists():
|
||||
print(f"database file {db_2} missing")
|
||||
|
||||
verbose_(f"Comparing databases {db_1} and {db_2}")
|
||||
verbose(f"Comparing databases {db_1} and {db_2}")
|
||||
|
||||
diff_proc = subprocess.Popen([sqldiff, db_2, db_1], stdout=subprocess.PIPE)
|
||||
console = Console()
|
||||
|
||||
@ -16,19 +16,23 @@ from osxphotos.photoinfo import PhotoInfoNone
|
||||
from osxphotos.photosalbum import PhotosAlbum
|
||||
from osxphotos.photosdb.photosdb_utils import get_db_version
|
||||
from osxphotos.phototemplate import PhotoTemplate, RenderOptions
|
||||
from osxphotos.queryoptions import QueryOptions
|
||||
from osxphotos.queryoptions import (
|
||||
IncompatibleQueryOptions,
|
||||
QueryOptions,
|
||||
query_options_from_kwargs,
|
||||
)
|
||||
from osxphotos.sqlitekvstore import SQLiteKVStore
|
||||
from osxphotos.utils import pluralize
|
||||
|
||||
from .click_rich_echo import (
|
||||
rich_click_echo,
|
||||
rich_echo_error,
|
||||
set_rich_console,
|
||||
set_rich_theme,
|
||||
set_rich_timestamp,
|
||||
from .click_rich_echo import rich_click_echo as echo
|
||||
from .click_rich_echo import rich_echo_error as echo_error
|
||||
from .common import (
|
||||
DB_OPTION,
|
||||
QUERY_OPTIONS,
|
||||
THEME_OPTION,
|
||||
TIMESTAMP_OPTION,
|
||||
VERBOSE_OPTION,
|
||||
)
|
||||
from .color_themes import get_theme
|
||||
from .common import DB_OPTION, QUERY_OPTIONS, THEME_OPTION, query_options_from_kwargs
|
||||
from .param_types import TemplateString
|
||||
from .report_writer import sync_report_writer_factory
|
||||
from .rich_progress import rich_progress
|
||||
@ -107,7 +111,7 @@ def render_and_validate_report(report: str) -> str:
|
||||
report = report_file[0]
|
||||
|
||||
if os.path.isdir(report):
|
||||
rich_click_echo(
|
||||
echo(
|
||||
f"[error]Report '{report}' is a directory, must be file name",
|
||||
err=True,
|
||||
)
|
||||
@ -183,7 +187,7 @@ def export_metadata(
|
||||
verbose(f"Analyzing [num]{num_photos}[/] {photo_word} to export")
|
||||
verbose(f"Exporting [num]{len(photos)}[/] {photo_word} to {output_path}")
|
||||
export_metadata_to_db(photos, metadata_db, progress=True)
|
||||
rich_click_echo(
|
||||
echo(
|
||||
f"Done: exported metadata for [num]{len(photos)}[/] {photo_word} to [filepath]{output_path}[/]"
|
||||
)
|
||||
metadata_db.close()
|
||||
@ -289,7 +293,7 @@ def import_metadata(
|
||||
elif import_type == "export":
|
||||
import_db = open_metadata_db(import_path)
|
||||
else:
|
||||
rich_echo_error(
|
||||
echo_error(
|
||||
f"Unable to determine type of import file: [filepath]{import_path}[/]"
|
||||
)
|
||||
raise click.Abort()
|
||||
@ -309,7 +313,7 @@ def import_metadata(
|
||||
elif unmatched:
|
||||
# unable to find metadata for photo in import_db
|
||||
for photo in key_photos:
|
||||
rich_click_echo(
|
||||
echo(
|
||||
f"Unable to find metadata for [filename]{photo.original_filename}[/] ([uuid]{photo.uuid}[/]) in [filepath]{import_path}[/]"
|
||||
)
|
||||
|
||||
@ -317,7 +321,7 @@ def import_metadata(
|
||||
# find any keys in import_db that don't match keys in photos
|
||||
for key in import_db.keys():
|
||||
if key not in key_to_photo:
|
||||
rich_click_echo(f"Unable to find [uuid]{key}[/] in selected photos.")
|
||||
echo(f"Unable to find [uuid]{key}[/] in selected photos.")
|
||||
|
||||
return results
|
||||
|
||||
@ -368,7 +372,7 @@ def _update_albums_for_photo(
|
||||
before = sorted(photo.albums)
|
||||
albums_to_add = set(value) - set(before)
|
||||
if not albums_to_add:
|
||||
verbose(f"\tNothing to do for albums")
|
||||
verbose(f"\tNothing to do for albums", level=2)
|
||||
results.add_result(
|
||||
photo.uuid,
|
||||
photo.original_filename,
|
||||
@ -426,7 +430,7 @@ def _set_metadata_for_photo(
|
||||
if not dry_run:
|
||||
set_photo_property(photo_, field, value)
|
||||
else:
|
||||
verbose(f"\tNothing to do for {field}")
|
||||
verbose(f"\tNothing to do for {field}", level=2)
|
||||
|
||||
results.add_result(
|
||||
photo.uuid,
|
||||
@ -464,7 +468,7 @@ def _merge_metadata_for_photo(
|
||||
before = sorted(before)
|
||||
|
||||
if value == before:
|
||||
verbose(f"\tNothing to do for {field}")
|
||||
verbose(f"\tNothing to do for {field}", level=2)
|
||||
results.add_result(
|
||||
photo.uuid,
|
||||
photo.original_filename,
|
||||
@ -486,7 +490,7 @@ def _merge_metadata_for_photo(
|
||||
elif before is None:
|
||||
new_value = value
|
||||
else:
|
||||
rich_echo_error(
|
||||
echo_error(
|
||||
f"Unable to merge {field} for [filename]{photo.original_filename}[filename]"
|
||||
)
|
||||
raise click.Abort()
|
||||
@ -498,7 +502,7 @@ def _merge_metadata_for_photo(
|
||||
else:
|
||||
# Merge'd value might still be the same as original value
|
||||
# (e.g. if value is str and has previously been merged)
|
||||
verbose(f"\tNothing to do for {field}")
|
||||
verbose(f"\tNothing to do for {field}", level=2)
|
||||
|
||||
results.add_result(
|
||||
photo.uuid,
|
||||
@ -534,7 +538,7 @@ def print_import_summary(results: SyncResults):
|
||||
f"updated {property}: [num]{summary.get(property,0)}[/]"
|
||||
for property in SYNC_PROPERTIES
|
||||
)
|
||||
rich_click_echo(
|
||||
echo(
|
||||
f"Processed [num]{summary['total']}[/] photos, updated: [num]{summary['updated']}[/], {property_summary}"
|
||||
)
|
||||
|
||||
@ -634,10 +638,8 @@ def print_import_summary(results: SyncResults):
|
||||
is_flag=True,
|
||||
help="Dry run; " "when used with --import, don't actually update metadata.",
|
||||
)
|
||||
@click.option("--verbose", "-V", "verbose_", is_flag=True, help="Print verbose output.")
|
||||
@click.option(
|
||||
"--timestamp", "-T", is_flag=True, help="Add time stamp to verbose output."
|
||||
)
|
||||
@VERBOSE_OPTION
|
||||
@TIMESTAMP_OPTION
|
||||
@QUERY_OPTIONS
|
||||
@DB_OPTION
|
||||
@THEME_OPTION
|
||||
@ -657,7 +659,7 @@ def sync(
|
||||
theme,
|
||||
timestamp,
|
||||
unmatched,
|
||||
verbose_,
|
||||
verbose_flag,
|
||||
**kwargs, # query options
|
||||
):
|
||||
"""Sync metadata and albums between Photos libraries.
|
||||
@ -711,23 +713,17 @@ def sync(
|
||||
osxphotos sync --export /path/to/export/folder/computer2.db --merge all --import /path/to/export/folder/computer1.db
|
||||
|
||||
"""
|
||||
color_theme = get_theme(theme)
|
||||
verbose = verbose_print(
|
||||
verbose_, timestamp, rich=True, theme=color_theme, highlight=False
|
||||
)
|
||||
# set console for rich_echo to be same as for verbose_
|
||||
set_rich_console(get_verbose_console())
|
||||
set_rich_theme(color_theme)
|
||||
set_rich_timestamp(timestamp)
|
||||
|
||||
verbose = verbose_print(verbose=verbose_flag, timestamp=timestamp, theme=theme)
|
||||
|
||||
if (set_ or merge) and not import_path:
|
||||
rich_echo_error("--set and --merge can only be used with --import")
|
||||
echo_error("--set and --merge can only be used with --import")
|
||||
ctx.exit(1)
|
||||
|
||||
# filter out photos in shared albums as these cannot be updated
|
||||
# Not elegant but works for now without completely refactoring QUERY_OPTIONS
|
||||
if kwargs.get("shared"):
|
||||
rich_echo_error(
|
||||
echo_error(
|
||||
"[warning]--shared cannot be used with --import/--export "
|
||||
"as photos in shared iCloud albums cannot be updated; "
|
||||
"--shared will be ignored[/]"
|
||||
@ -748,14 +744,19 @@ def sync(
|
||||
set_ = set(set_)
|
||||
merge = set(merge)
|
||||
if set_ & merge:
|
||||
rich_echo_error(
|
||||
echo_error(
|
||||
"--set and --merge cannot be used with the same fields: "
|
||||
f"set: {set_}, merge: {merge}"
|
||||
)
|
||||
ctx.exit(1)
|
||||
|
||||
if import_path:
|
||||
try:
|
||||
query_options = query_options_from_kwargs(**kwargs)
|
||||
except IncompatibleQueryOptions:
|
||||
echo_error("Incompatible query options")
|
||||
echo_error(ctx.obj.group.commands["repl"].get_help(ctx))
|
||||
ctx.exit(1)
|
||||
photosdb = PhotosDB(dbfile=db, verbose=verbose)
|
||||
photos = photosdb.query(query_options)
|
||||
results = import_metadata(
|
||||
|
||||
@ -25,16 +25,10 @@ from osxphotos.photosalbum import PhotosAlbumPhotoScript
|
||||
from osxphotos.phototz import PhotoTimeZone, PhotoTimeZoneUpdater
|
||||
from osxphotos.utils import noop, pluralize
|
||||
|
||||
from .click_rich_echo import (
|
||||
rich_click_echo,
|
||||
rich_echo,
|
||||
rich_echo_error,
|
||||
set_rich_console,
|
||||
set_rich_theme,
|
||||
set_rich_timestamp,
|
||||
)
|
||||
from .click_rich_echo import rich_click_echo as echo
|
||||
from .click_rich_echo import rich_echo_error as echo_error
|
||||
from .color_themes import get_theme
|
||||
from .common import THEME_OPTION
|
||||
from .common import THEME_OPTION, TIMESTAMP_OPTION, VERBOSE_OPTION
|
||||
from .darkmode import is_dark_mode
|
||||
from .help import HELP_WIDTH, rich_text
|
||||
from .param_types import (
|
||||
@ -310,7 +304,8 @@ command which can be used to change the time zone of photos after import.
|
||||
help="When used with --compare-exif, adds any photos with date/time/timezone differences "
|
||||
"between Photos/EXIF to album ALBUM. If ALBUM does not exist, it will be created.",
|
||||
)
|
||||
@click.option("--verbose", "-V", "verbose_", is_flag=True, help="Show verbose output.")
|
||||
@VERBOSE_OPTION
|
||||
@TIMESTAMP_OPTION
|
||||
@click.option(
|
||||
"--library",
|
||||
"-L",
|
||||
@ -326,19 +321,6 @@ command which can be used to change the time zone of photos after import.
|
||||
type=click.Path(exists=True),
|
||||
help="Optional path to exiftool executable (will look in $PATH if not specified) for those options which require exiftool.",
|
||||
)
|
||||
@click.option(
|
||||
"--output-file",
|
||||
"-o",
|
||||
type=click.File(mode="w", lazy=False),
|
||||
help="Output file. If not specified, output is written to stdout.",
|
||||
)
|
||||
@click.option(
|
||||
"--terminal-width",
|
||||
"-w",
|
||||
type=int,
|
||||
help="Terminal width in characters.",
|
||||
hidden=True,
|
||||
)
|
||||
@click.option("--timestamp", is_flag=True, help="Add time stamp to verbose output")
|
||||
@THEME_OPTION
|
||||
@click.option(
|
||||
@ -366,13 +348,11 @@ def timewarp(
|
||||
use_file_time,
|
||||
add_to_album,
|
||||
exiftool_path,
|
||||
verbose_,
|
||||
verbose_flag,
|
||||
library,
|
||||
theme,
|
||||
parse_date,
|
||||
plain,
|
||||
output_file,
|
||||
terminal_width,
|
||||
timestamp,
|
||||
force,
|
||||
):
|
||||
@ -419,33 +399,7 @@ def timewarp(
|
||||
if add_to_album and not compare_exif:
|
||||
raise click.UsageError("--add-to-album must be used with --compare-exif.")
|
||||
|
||||
# configure colored rich output
|
||||
# TODO: this is all a little hacky, find a better way to do this
|
||||
color_theme = get_theme(theme)
|
||||
verbose = verbose_print(
|
||||
verbose_,
|
||||
timestamp,
|
||||
rich=True,
|
||||
theme=color_theme,
|
||||
highlight=False,
|
||||
file=output_file,
|
||||
)
|
||||
# set console for rich_echo to be same as for verbose_
|
||||
terminal_width = terminal_width or (1000 if output_file else None)
|
||||
if output_file:
|
||||
set_rich_console(Console(file=output_file, width=terminal_width))
|
||||
elif terminal_width:
|
||||
set_rich_console(
|
||||
Console(
|
||||
file=sys.stdout,
|
||||
theme=color_theme,
|
||||
force_terminal=True,
|
||||
width=terminal_width,
|
||||
)
|
||||
)
|
||||
else:
|
||||
set_rich_console(get_verbose_console(theme=color_theme))
|
||||
set_rich_theme(color_theme)
|
||||
verbose = verbose_print(verbose=verbose_flag, timestamp=timestamp, theme=theme)
|
||||
|
||||
if any([compare_exif, push_exif, pull_exif]):
|
||||
exiftool_path = exiftool_path or get_exiftool_path()
|
||||
@ -454,19 +408,19 @@ def timewarp(
|
||||
try:
|
||||
photos = PhotosLibrary().selection
|
||||
if not photos:
|
||||
rich_echo_error("[warning]No photos selected[/]")
|
||||
echo_error("[warning]No photos selected[/]")
|
||||
sys.exit(0)
|
||||
except Exception as e:
|
||||
# AppleScript error -1728 occurs if user attempts to get selected photos in a Smart Album
|
||||
if "(-1728)" in str(e):
|
||||
rich_echo_error(
|
||||
echo_error(
|
||||
"[error]Could not get selected photos. Ensure photos is open and photos are selected. "
|
||||
"If you have selected photos and you see this message, it may be because the selected photos are in a Photos Smart Album. "
|
||||
f"{APP_NAME} cannot access photos in a Smart Album. Select the photos in a regular album or in 'All Photos' view. "
|
||||
"Another option is to create a new album using 'File | New Album With Selection' then select the photos in the new album.[/]",
|
||||
)
|
||||
else:
|
||||
rich_echo_error(
|
||||
echo_error(
|
||||
f"[error]Could not get selected photos. Ensure Photos is open and photos to process are selected. {e}[/]",
|
||||
)
|
||||
sys.exit(1)
|
||||
@ -535,7 +489,7 @@ def timewarp(
|
||||
if inspect:
|
||||
tzinfo = PhotoTimeZone(library_path=library)
|
||||
if photos:
|
||||
rich_echo(
|
||||
echo(
|
||||
"[filename]filename[/filename], [uuid]uuid[/uuid], "
|
||||
"[time]photo time (local)[/time], "
|
||||
"[time]photo time[/time], "
|
||||
@ -545,7 +499,7 @@ def timewarp(
|
||||
tz_seconds, tz_str, tz_name = tzinfo.get_timezone(photo)
|
||||
photo_date_local = datetime_naive_to_local(photo.date)
|
||||
photo_date_tz = datetime_to_new_tz(photo_date_local, tz_seconds)
|
||||
rich_echo(
|
||||
echo(
|
||||
f"[filename]{photo.filename}[/filename], [uuid]{photo.uuid}[/uuid], "
|
||||
f"[time]{photo_date_local.strftime(DATETIME_FORMAT)}[/time], "
|
||||
f"[time]{photo_date_tz.strftime(DATETIME_FORMAT)}[/time], "
|
||||
@ -563,7 +517,7 @@ def timewarp(
|
||||
exiftool_path=exiftool_path,
|
||||
)
|
||||
if not album:
|
||||
rich_echo(
|
||||
echo(
|
||||
"filename, uuid, photo time (Photos), photo time (EXIF), timezone offset (Photos), timezone offset (EXIF)"
|
||||
)
|
||||
for photo in photos:
|
||||
@ -592,13 +546,13 @@ def timewarp(
|
||||
else:
|
||||
verbose(f"Photo {filename} ({uuid}) has same date/time/timezone")
|
||||
else:
|
||||
rich_echo(
|
||||
echo(
|
||||
f"{filename}, {uuid}, "
|
||||
f"{diff_results.photos_date} {diff_results.photos_time}, {diff_results.exif_date} {diff_results.exif_time}, "
|
||||
f"{diff_results.photos_tz}, {diff_results.exif_tz}"
|
||||
)
|
||||
if album:
|
||||
rich_echo(
|
||||
echo(
|
||||
f"Compared {len(photos)} photos, found {different_photos} "
|
||||
f"that {pluralize(different_photos, 'is', 'are')} different and "
|
||||
f"added {pluralize(different_photos, 'it', 'them')} to album '{album.name}'."
|
||||
@ -649,12 +603,10 @@ def timewarp(
|
||||
# before exiftool is run
|
||||
exif_warn, exif_error = exif_updater.update_exif_from_photos(p)
|
||||
if exif_warn:
|
||||
rich_echo_error(
|
||||
f"[warning]Warning running exiftool: {exif_warn}[/]"
|
||||
)
|
||||
echo_error(f"[warning]Warning running exiftool: {exif_warn}[/]")
|
||||
if exif_error:
|
||||
rich_echo_error(f"[error]Error running exiftool: {exif_error}[/]")
|
||||
echo_error(f"[error]Error running exiftool: {exif_error}[/]")
|
||||
|
||||
progress.advance(task)
|
||||
|
||||
rich_echo("Done.")
|
||||
echo("Done.")
|
||||
|
||||
@ -1,14 +1,22 @@
|
||||
"""helper functions for printing verbose output"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import typing as t
|
||||
from datetime import datetime
|
||||
from typing import IO, Any, Callable, Optional
|
||||
|
||||
import click
|
||||
from rich.console import Console
|
||||
from rich.theme import Theme
|
||||
|
||||
from .click_rich_echo import rich_click_echo
|
||||
from .click_rich_echo import (
|
||||
rich_click_echo,
|
||||
set_rich_console,
|
||||
set_rich_theme,
|
||||
set_rich_timestamp,
|
||||
)
|
||||
from .color_themes import get_theme
|
||||
from .common import CLI_COLOR_ERROR, CLI_COLOR_WARNING, time_stamp
|
||||
|
||||
# set to 1 if running tests
|
||||
@ -17,14 +25,79 @@ OSXPHOTOS_IS_TESTING = bool(os.getenv("OSXPHOTOS_IS_TESTING", default=False))
|
||||
# include error/warning emoji's in verbose output
|
||||
ERROR_EMOJI = True
|
||||
|
||||
__all__ = ["get_verbose_console", "verbose_print"]
|
||||
# global to store verbose level
|
||||
__verbose_level = 1
|
||||
|
||||
# global verbose function
|
||||
__verbose_function: Callable[..., None] | None = None
|
||||
|
||||
|
||||
__all__ = [
|
||||
"get_verbose_console",
|
||||
"get_verbose_level",
|
||||
"set_verbose_level",
|
||||
"verbose_print",
|
||||
"verbose",
|
||||
]
|
||||
|
||||
|
||||
def _reset_verbose_globals():
|
||||
"""Reset globals for testing"""
|
||||
global __verbose_level
|
||||
global __verbose_function
|
||||
global _console
|
||||
__verbose_level = 1
|
||||
__verbose_function = None
|
||||
_console = _Console()
|
||||
|
||||
|
||||
def noop(*args, **kwargs):
|
||||
"""no-op function"""
|
||||
pass
|
||||
|
||||
|
||||
def verbose(*args, level: int = 1, **kwargs):
|
||||
"""Print verbose output
|
||||
|
||||
Args:
|
||||
*args: arguments to pass to verbose function for printing
|
||||
level: verbose level; if level > get_verbose_level(), output is suppressed
|
||||
|
||||
Notes:
|
||||
Normally you should use verbose_print() to get the verbose function instead of calling this directly
|
||||
|
||||
"""
|
||||
global __verbose_function
|
||||
if __verbose_function is None:
|
||||
return
|
||||
__verbose_function(*args, level=level, **kwargs)
|
||||
|
||||
|
||||
def set_verbose_level(level: int):
|
||||
"""Set verbose level"""
|
||||
global __verbose_level
|
||||
global __verbose_function
|
||||
__verbose_level = level
|
||||
if level > 0 and __verbose_function is None:
|
||||
# if verbose level set but verbose function not set, set it to default
|
||||
# verbose_print sets the global __verbose_function
|
||||
__verbose_function = _verbose_print_function(level)
|
||||
elif level == 0 and __verbose_function is not None:
|
||||
# if verbose level set to 0 but verbose function is set, set it to no-op
|
||||
__verbose_function = noop
|
||||
|
||||
|
||||
def get_verbose_level() -> int:
|
||||
"""Get verbose level"""
|
||||
global __verbose_level
|
||||
return __verbose_level
|
||||
|
||||
|
||||
class _Console:
|
||||
"""Store console object for verbose output"""
|
||||
|
||||
def __init__(self):
|
||||
self._console: t.Optional[Console] = None
|
||||
self._console: Optional[Console] = None
|
||||
|
||||
@property
|
||||
def console(self):
|
||||
@ -38,12 +111,7 @@ class _Console:
|
||||
_console = _Console()
|
||||
|
||||
|
||||
def noop(*args, **kwargs):
|
||||
"""no-op function"""
|
||||
pass
|
||||
|
||||
|
||||
def get_verbose_console(theme: t.Optional[Theme] = None) -> Console:
|
||||
def get_verbose_console(theme: Optional[Theme] = None) -> Console:
|
||||
"""Get console object or create one if not already created
|
||||
|
||||
Args:
|
||||
@ -59,18 +127,68 @@ def get_verbose_console(theme: t.Optional[Theme] = None) -> Console:
|
||||
|
||||
|
||||
def verbose_print(
|
||||
verbose: int = 1,
|
||||
timestamp: bool = False,
|
||||
rich: bool = True,
|
||||
theme: str | None = None,
|
||||
highlight: bool = False,
|
||||
file: Optional[IO] = None,
|
||||
**kwargs: Any,
|
||||
) -> Callable[..., None]:
|
||||
"""Configure verbose printing and create verbose function to print output
|
||||
|
||||
Args:
|
||||
verbose: if > 0, returns verbose print function otherwise returns no-op function; the value of verbose is the verbose level
|
||||
timestamp: if True, includes timestamp in verbose output
|
||||
rich: use rich.print instead of click.echo
|
||||
highlight: if True, use automatic rich.print highlighting
|
||||
theme: optional name of theme to use for formatting (will be loaded by get_theme())
|
||||
file: optional file handle to write to instead of stdout
|
||||
kwargs: any extra arguments to pass to click.echo or rich.print depending on whether rich==True
|
||||
|
||||
Returns:
|
||||
function to print output
|
||||
|
||||
Note: sets the console for rich_echo to be the same as the console used for verbose output
|
||||
"""
|
||||
|
||||
set_verbose_level(verbose)
|
||||
color_theme = get_theme(theme)
|
||||
verbose_function = _verbose_print_function(
|
||||
verbose=verbose,
|
||||
timestamp=timestamp,
|
||||
rich=rich,
|
||||
theme=color_theme,
|
||||
highlight=highlight,
|
||||
file=file,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
# set console for rich_echo to be same as for verbose
|
||||
set_rich_console(get_verbose_console())
|
||||
set_rich_theme(color_theme)
|
||||
set_rich_timestamp(timestamp)
|
||||
|
||||
# set global verbose function to match
|
||||
global __verbose_function
|
||||
__verbose_function = verbose_function
|
||||
|
||||
return verbose_function
|
||||
|
||||
|
||||
def _verbose_print_function(
|
||||
verbose: bool = True,
|
||||
timestamp: bool = False,
|
||||
rich: bool = False,
|
||||
highlight: bool = False,
|
||||
theme: t.Optional[Theme] = None,
|
||||
file: t.Optional[t.IO] = None,
|
||||
**kwargs: t.Any,
|
||||
) -> t.Callable:
|
||||
theme: Optional[Theme] = None,
|
||||
file: Optional[IO] = None,
|
||||
**kwargs: Any,
|
||||
) -> Callable[..., None]:
|
||||
"""Create verbose function to print output
|
||||
|
||||
Args:
|
||||
verbose: if True, returns verbose print function otherwise returns no-op function
|
||||
verbose: if > 0, returns verbose print function otherwise returns no-op function; the value of verbose is the verbose level
|
||||
timestamp: if True, includes timestamp in verbose output
|
||||
rich: use rich.print instead of click.echo
|
||||
highlight: if True, use automatic rich.print highlighting
|
||||
@ -91,8 +209,10 @@ def verbose_print(
|
||||
_console.console = Console(theme=theme, width=10_000)
|
||||
|
||||
# closure to capture timestamp
|
||||
def verbose_(*args):
|
||||
def verbose_(*args, level: int = 1):
|
||||
"""print output if verbose flag set"""
|
||||
if get_verbose_level() < level:
|
||||
return
|
||||
styled_args = []
|
||||
timestamp_str = f"{str(datetime.now())} -- " if timestamp else ""
|
||||
for arg in args:
|
||||
@ -103,10 +223,12 @@ def verbose_print(
|
||||
elif "warning" in arg.lower():
|
||||
arg = click.style(arg, fg=CLI_COLOR_WARNING)
|
||||
styled_args.append(arg)
|
||||
click.echo(*styled_args, **kwargs)
|
||||
click.echo(*styled_args, **kwargs, file=file or None)
|
||||
|
||||
def rich_verbose_(*args):
|
||||
def rich_verbose_(*args, level: int = 1):
|
||||
"""rich.print output if verbose flag set"""
|
||||
if get_verbose_level() < level:
|
||||
return
|
||||
global ERROR_EMOJI
|
||||
timestamp_str = time_stamp() if timestamp else ""
|
||||
new_args = []
|
||||
@ -124,8 +246,10 @@ def verbose_print(
|
||||
new_args.append(arg)
|
||||
_console.console.print(*new_args, highlight=highlight, **kwargs)
|
||||
|
||||
def rich_verbose_testing_(*args):
|
||||
def rich_verbose_testing_(*args, level: int = 1):
|
||||
"""print output if verbose flag set using rich.print"""
|
||||
if get_verbose_level() < level:
|
||||
return
|
||||
global ERROR_EMOJI
|
||||
timestamp_str = time_stamp() if timestamp else ""
|
||||
new_args = []
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
"""Utilities for debugging"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import sys
|
||||
import time
|
||||
@ -9,21 +11,33 @@ from typing import Dict, List
|
||||
import wrapt
|
||||
from rich import print
|
||||
|
||||
__all__ = [
|
||||
"debug_breakpoint",
|
||||
"debug_watch",
|
||||
"get_debug_flags",
|
||||
"get_debug_options",
|
||||
"is_debug",
|
||||
"set_debug",
|
||||
"wrap_function",
|
||||
]
|
||||
|
||||
|
||||
# global variable to control debug output
|
||||
# set via --debug
|
||||
DEBUG = False
|
||||
__osxphotos_debug = False
|
||||
|
||||
|
||||
def set_debug(debug: bool):
|
||||
"""set debug flag"""
|
||||
global DEBUG
|
||||
DEBUG = debug
|
||||
global __osxphotos_debug
|
||||
__osxphotos_debug = debug
|
||||
logging.disable(logging.NOTSET if debug else logging.DEBUG)
|
||||
|
||||
|
||||
def is_debug():
|
||||
"""return debug flag"""
|
||||
return DEBUG
|
||||
global __osxphotos_debug
|
||||
return __osxphotos_debug
|
||||
|
||||
|
||||
def debug_watch(wrapped, instance, args, kwargs):
|
||||
|
||||
@ -8,7 +8,6 @@ import contextlib
|
||||
import dataclasses
|
||||
import datetime
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import os.path
|
||||
import pathlib
|
||||
@ -16,6 +15,7 @@ import plistlib
|
||||
from datetime import timedelta, timezone
|
||||
from functools import cached_property
|
||||
from typing import Any, Dict, Optional
|
||||
import logging
|
||||
|
||||
import yaml
|
||||
from osxmetadata import OSXMetaData
|
||||
@ -68,6 +68,7 @@ from .utils import _get_resource_loc, hexdigest, list_directory
|
||||
|
||||
__all__ = ["PhotoInfo", "PhotoInfoNone"]
|
||||
|
||||
logger = logging.getLogger("osxphotos")
|
||||
|
||||
class PhotoInfo:
|
||||
"""
|
||||
@ -245,7 +246,7 @@ class PhotoInfo:
|
||||
photopath = self._path_edited_5()
|
||||
|
||||
if photopath is not None and not os.path.isfile(photopath):
|
||||
logging.debug(
|
||||
logger.debug(
|
||||
f"edited file for UUID {self._uuid} should be at {photopath} but does not appear to exist"
|
||||
)
|
||||
photopath = None
|
||||
@ -285,7 +286,7 @@ class PhotoInfo:
|
||||
filename = f"{self._uuid}_2_0_a.mov"
|
||||
else:
|
||||
# don't know what it is!
|
||||
logging.debug(f"WARNING: unknown type {self._info['type']}")
|
||||
logger.debug(f"WARNING: unknown type {self._info['type']}")
|
||||
return None
|
||||
|
||||
return os.path.join(library, "resources", "renders", directory, filename)
|
||||
@ -332,7 +333,7 @@ class PhotoInfo:
|
||||
try:
|
||||
photopath = self._get_predicted_path_edited_4()
|
||||
except ValueError as e:
|
||||
logging.debug(f"ERROR: {e}")
|
||||
logger.debug(f"ERROR: {e}")
|
||||
photopath = None
|
||||
|
||||
if photopath is not None and not os.path.isfile(photopath):
|
||||
@ -346,12 +347,12 @@ class PhotoInfo:
|
||||
|
||||
# check again to see if we found a valid file
|
||||
if photopath is not None and not os.path.isfile(photopath):
|
||||
logging.debug(
|
||||
logger.debug(
|
||||
f"MISSING PATH: edited file for UUID {self._uuid} should be at {photopath} but does not appear to exist"
|
||||
)
|
||||
photopath = None
|
||||
else:
|
||||
logging.debug(f"{self.uuid} hasAdjustments but edit_resource_id is None")
|
||||
logger.debug(f"{self.uuid} hasAdjustments but edit_resource_id is None")
|
||||
photopath = None
|
||||
|
||||
return photopath
|
||||
@ -400,7 +401,7 @@ class PhotoInfo:
|
||||
None,
|
||||
)
|
||||
if photopath is None:
|
||||
logging.debug(
|
||||
logger.debug(
|
||||
f"MISSING PATH: edited live photo file for UUID {self._uuid} does not appear to exist"
|
||||
)
|
||||
return photopath
|
||||
@ -496,7 +497,7 @@ class PhotoInfo:
|
||||
self._db._masters_path, self._info["raw_info"]["imagePath"]
|
||||
)
|
||||
if not os.path.isfile(photopath):
|
||||
logging.debug(
|
||||
logger.debug(
|
||||
f"MISSING PATH: RAW photo for UUID {self._uuid} should be at {photopath} but does not appear to exist"
|
||||
)
|
||||
photopath = None
|
||||
@ -912,7 +913,7 @@ class PhotoInfo:
|
||||
if self.live_photo and not self.ismissing:
|
||||
live_model_id = self._info["live_model_id"]
|
||||
if live_model_id is None:
|
||||
logging.debug(f"missing live_model_id: {self._uuid}")
|
||||
logger.debug(f"missing live_model_id: {self._uuid}")
|
||||
photopath = None
|
||||
else:
|
||||
folder_id, file_id, nn_id = _get_resource_loc(live_model_id)
|
||||
@ -1196,7 +1197,7 @@ class PhotoInfo:
|
||||
"""
|
||||
|
||||
if self._db._db_version <= _PHOTOS_4_VERSION:
|
||||
logging.debug(f"score not implemented for this database version")
|
||||
logger.debug(f"score not implemented for this database version")
|
||||
return None
|
||||
|
||||
try:
|
||||
@ -1342,7 +1343,7 @@ class PhotoInfo:
|
||||
"""
|
||||
|
||||
if self._db._db_version <= _PHOTOS_4_VERSION:
|
||||
logging.debug(f"exif_info not implemented for this database version")
|
||||
logger.debug(f"exif_info not implemented for this database version")
|
||||
return None
|
||||
|
||||
try:
|
||||
@ -1369,7 +1370,7 @@ class PhotoInfo:
|
||||
lens_model=exif["ZLENSMODEL"],
|
||||
)
|
||||
except KeyError:
|
||||
logging.debug(f"Could not find exif record for uuid {self.uuid}")
|
||||
logger.debug(f"Could not find exif record for uuid {self.uuid}")
|
||||
exif_info = ExifInfo(
|
||||
iso=None,
|
||||
flash_fired=None,
|
||||
@ -1441,7 +1442,7 @@ class PhotoInfo:
|
||||
"""
|
||||
|
||||
if self._db._db_version <= _PHOTOS_4_VERSION:
|
||||
logging.debug(f"cloud_metadata not implemented for this database version")
|
||||
logger.debug(f"cloud_metadata not implemented for this database version")
|
||||
return {}
|
||||
|
||||
_, cursor = self._db.get_db_connection()
|
||||
|
||||
@ -34,7 +34,7 @@ from wurlitzer import pipes
|
||||
|
||||
from .fileutil import FileUtil
|
||||
from .uti import get_preferred_uti_extension
|
||||
from .utils import _get_os_version, increment_filename
|
||||
from .utils import get_macos_version, increment_filename
|
||||
|
||||
__all__ = [
|
||||
"NSURL_to_path",
|
||||
@ -124,7 +124,7 @@ def request_photokit_authorization():
|
||||
will do the actual request.
|
||||
"""
|
||||
|
||||
(_, major, _) = _get_os_version()
|
||||
(_, major, _) = get_macos_version()
|
||||
|
||||
def handler(status):
|
||||
pass
|
||||
|
||||
@ -34,6 +34,8 @@ from ..utils import normalize_unicode
|
||||
These methods only work on Photos 5 databases. Will print warning on earlier library versions.
|
||||
"""
|
||||
|
||||
logger = logging.getLogger("osxphotos")
|
||||
|
||||
|
||||
def _process_searchinfo(self):
|
||||
"""load machine learning/search term label info from a Photos library
|
||||
@ -55,12 +57,12 @@ def _process_searchinfo(self):
|
||||
self._db_searchinfo_labels_normalized = _db_searchinfo_labels_normalized = {}
|
||||
|
||||
if self._skip_searchinfo:
|
||||
logging.debug("Skipping search info processing")
|
||||
logger.debug("Skipping search info processing")
|
||||
return
|
||||
|
||||
if self._db_version <= _PHOTOS_4_VERSION:
|
||||
raise NotImplementedError(
|
||||
f"search info not implemented for this database version"
|
||||
"search info not implemented for this database version"
|
||||
)
|
||||
|
||||
search_db_path = pathlib.Path(self._dbfile).parent / "search" / "psi.sqlite"
|
||||
|
||||
@ -53,7 +53,6 @@ from .._constants import (
|
||||
from .._version import __version__
|
||||
from ..albuminfo import AlbumInfo, FolderInfo, ImportInfo, ProjectInfo
|
||||
from ..datetime_utils import datetime_has_tz, datetime_naive_to_local
|
||||
from ..debug import is_debug
|
||||
from ..fileutil import FileUtil
|
||||
from ..personinfo import PersonInfo
|
||||
from ..photoinfo import PhotoInfo
|
||||
@ -63,13 +62,15 @@ 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,
|
||||
_get_os_version,
|
||||
get_macos_version,
|
||||
get_last_library_path,
|
||||
noop,
|
||||
normalize_unicode,
|
||||
)
|
||||
from .photosdb_utils import get_db_model_version, get_db_version
|
||||
|
||||
logger = logging.getLogger("osxphotos")
|
||||
|
||||
__all__ = ["PhotosDB"]
|
||||
|
||||
# TODO: Add test for imageTimeZoneOffsetSeconds = None
|
||||
@ -117,7 +118,7 @@ class PhotosDB:
|
||||
|
||||
# Check OS version
|
||||
system = platform.system()
|
||||
(ver, major, _) = _get_os_version()
|
||||
(ver, major, _) = get_macos_version()
|
||||
if system != "Darwin" or ((ver, major) not in _TESTED_OS_VERSIONS):
|
||||
logging.warning(
|
||||
f"WARNING: This module has only been tested with macOS versions "
|
||||
@ -283,8 +284,7 @@ class PhotosDB:
|
||||
# key is Z_PK of ZMOMENT table and values are the moment info
|
||||
self._db_moment_pk = {}
|
||||
|
||||
if is_debug():
|
||||
logging.debug(f"dbfile = {dbfile}")
|
||||
logger.debug(f"dbfile = {dbfile}")
|
||||
|
||||
if dbfile is None:
|
||||
dbfile = get_last_library_path()
|
||||
@ -300,8 +300,7 @@ class PhotosDB:
|
||||
if not _check_file_exists(dbfile):
|
||||
raise FileNotFoundError(f"dbfile {dbfile} does not exist", dbfile)
|
||||
|
||||
if is_debug():
|
||||
logging.debug(f"dbfile = {dbfile}")
|
||||
logger.debug(f"dbfile = {dbfile}")
|
||||
|
||||
# init database names
|
||||
# _tmp_db is the file that will processed by _process_database4/5
|
||||
@ -352,8 +351,7 @@ class PhotosDB:
|
||||
# set the photos version to actual value based on Photos.sqlite
|
||||
self._photos_ver = get_db_model_version(self._tmp_db)
|
||||
|
||||
if is_debug():
|
||||
logging.debug(
|
||||
logger.debug(
|
||||
f"_dbfile = {self._dbfile}, _dbfile_actual = {self._dbfile_actual}"
|
||||
)
|
||||
|
||||
@ -367,8 +365,7 @@ class PhotosDB:
|
||||
masters_path = os.path.join(library_path, "originals")
|
||||
self._masters_path = masters_path
|
||||
|
||||
if is_debug():
|
||||
logging.debug(f"library = {library_path}, masters = {masters_path}")
|
||||
logger.debug(f"library = {library_path}, masters = {masters_path}")
|
||||
|
||||
if int(self._db_version) <= int(_PHOTOS_4_VERSION):
|
||||
self._process_database4()
|
||||
@ -628,38 +625,10 @@ class PhotosDB:
|
||||
print(f"Error copying{fname} to {dest_path}", file=sys.stderr)
|
||||
raise Exception
|
||||
|
||||
if is_debug():
|
||||
logging.debug(dest_path)
|
||||
logger.debug(dest_path)
|
||||
|
||||
return dest_path
|
||||
|
||||
# NOTE: This method seems to cause problems with applescript
|
||||
# Bummer...would'be been nice to avoid copying the DB
|
||||
# def _link_db_file(self, fname):
|
||||
# """ links the sqlite database file to a temp file """
|
||||
# """ returns the name of the temp file """
|
||||
# """ If sqlite shared memory and write-ahead log files exist, those are copied too """
|
||||
# # required because python's sqlite3 implementation can't read a locked file
|
||||
# # _, suffix = os.path.splitext(fname)
|
||||
# dest_name = dest_path = ""
|
||||
# try:
|
||||
# dest_name = pathlib.Path(fname).name
|
||||
# dest_path = os.path.join(self._tempdir_name, dest_name)
|
||||
# FileUtil.hardlink(fname, dest_path)
|
||||
# # link write-ahead log and shared memory files (-wal and -shm) files if they exist
|
||||
# if os.path.exists(f"{fname}-wal"):
|
||||
# FileUtil.hardlink(f"{fname}-wal", f"{dest_path}-wal")
|
||||
# if os.path.exists(f"{fname}-shm"):
|
||||
# FileUtil.hardlink(f"{fname}-shm", f"{dest_path}-shm")
|
||||
# except:
|
||||
# print("Error linking " + fname + " to " + dest_path, file=sys.stderr)
|
||||
# raise Exception
|
||||
|
||||
# if is_debug():
|
||||
# logging.debug(dest_path)
|
||||
|
||||
# return dest_path
|
||||
|
||||
def _process_database4(self):
|
||||
"""process the Photos database to extract info
|
||||
works on Photos version <= 4.0"""
|
||||
@ -741,7 +710,7 @@ class PhotosDB:
|
||||
self._dbpersons_pk[pk]["photo_uuid"] = person[2]
|
||||
self._dbpersons_pk[pk]["keyface_uuid"] = person[3]
|
||||
except KeyError:
|
||||
logging.debug(f"Unexpected KeyError _dbpersons_pk[{pk}]")
|
||||
logger.debug(f"Unexpected KeyError _dbpersons_pk[{pk}]")
|
||||
|
||||
# get information on detected faces
|
||||
verbose("Processing detected faces in photos.")
|
||||
@ -1118,8 +1087,7 @@ class PhotosDB:
|
||||
self._dbphotos[uuid]["type"] = _MOVIE_TYPE
|
||||
else:
|
||||
# unknown
|
||||
if is_debug():
|
||||
logging.debug(f"WARNING: {uuid} found unknown type {row[21]}")
|
||||
logger.debug(f"WARNING: {uuid} found unknown type {row[21]}")
|
||||
self._dbphotos[uuid]["type"] = None
|
||||
|
||||
self._dbphotos[uuid]["UTI"] = row[22]
|
||||
@ -1352,8 +1320,7 @@ class PhotosDB:
|
||||
if resource_type == 4:
|
||||
# photo
|
||||
if "edit_resource_id_photo" in self._dbphotos[uuid]:
|
||||
if is_debug():
|
||||
logging.debug(
|
||||
logger.debug(
|
||||
f"WARNING: found more than one edit_resource_id_photo for "
|
||||
f"UUID {row[0]},adjustmentUUID {row[1]}, modelID {row[2]}"
|
||||
)
|
||||
@ -1362,8 +1329,7 @@ class PhotosDB:
|
||||
elif resource_type == 8:
|
||||
# video
|
||||
if "edit_resource_id_video" in self._dbphotos[uuid]:
|
||||
if is_debug():
|
||||
logging.debug(
|
||||
logger.debug(
|
||||
f"WARNING: found more than one edit_resource_id_video for "
|
||||
f"UUID {row[0]},adjustmentUUID {row[1]}, modelID {row[2]}"
|
||||
)
|
||||
@ -1655,8 +1621,7 @@ class PhotosDB:
|
||||
but it works so don't touch it.
|
||||
"""
|
||||
|
||||
if is_debug():
|
||||
logging.debug(f"_process_database5")
|
||||
logger.debug(f"_process_database5")
|
||||
verbose = self._verbose
|
||||
verbose(f"Processing database.")
|
||||
(conn, c) = sqlite_open_ro(self._tmp_db)
|
||||
@ -1679,8 +1644,7 @@ class PhotosDB:
|
||||
hdr_type_column = _DB_TABLE_NAMES[photos_ver]["HDR_TYPE"]
|
||||
|
||||
# Look for all combinations of persons and pictures
|
||||
if is_debug():
|
||||
logging.debug(f"Getting information about persons")
|
||||
logger.debug(f"Getting information about persons")
|
||||
|
||||
# get info to associate persons with photos
|
||||
# then get detected faces in each photo and link to persons
|
||||
@ -1757,7 +1721,7 @@ class PhotosDB:
|
||||
self._dbpersons_pk[pk]["photo_uuid"] = person[2]
|
||||
self._dbpersons_pk[pk]["keyface_uuid"] = person[3]
|
||||
except KeyError:
|
||||
logging.debug(f"Unexpected KeyError _dbpersons_pk[{pk}]")
|
||||
logger.debug(f"Unexpected KeyError _dbpersons_pk[{pk}]")
|
||||
|
||||
# get information on detected faces
|
||||
verbose("Processing detected faces in photos.")
|
||||
@ -2095,8 +2059,7 @@ class PhotosDB:
|
||||
elif row[17] == 1:
|
||||
info["type"] = _MOVIE_TYPE
|
||||
else:
|
||||
if is_debug():
|
||||
logging.debug(f"WARNING: {uuid} found unknown type {row[17]}")
|
||||
logger.debug(f"WARNING: {uuid} found unknown type {row[17]}")
|
||||
info["type"] = None
|
||||
|
||||
info["UTI"] = row[18]
|
||||
@ -2283,7 +2246,7 @@ class PhotosDB:
|
||||
self._dbphotos[uuid]["fok_import_session"] = row[2]
|
||||
self._dbphotos[uuid]["import_uuid"] = row[3]
|
||||
except KeyError:
|
||||
logging.debug(f"No info record for uuid {uuid} for import session")
|
||||
logger.debug(f"No info record for uuid {uuid} for import session")
|
||||
|
||||
# Get extended description
|
||||
verbose("Processing additional photo details.")
|
||||
@ -2300,8 +2263,7 @@ class PhotosDB:
|
||||
if uuid in self._dbphotos:
|
||||
self._dbphotos[uuid]["extendedDescription"] = normalize_unicode(row[1])
|
||||
else:
|
||||
if is_debug():
|
||||
logging.debug(
|
||||
logger.debug(
|
||||
f"WARNING: found description {row[1]} but no photo for {uuid}"
|
||||
)
|
||||
|
||||
@ -2319,8 +2281,7 @@ class PhotosDB:
|
||||
if uuid in self._dbphotos:
|
||||
self._dbphotos[uuid]["adjustmentFormatID"] = row[2]
|
||||
else:
|
||||
if is_debug():
|
||||
logging.debug(
|
||||
logger.debug(
|
||||
f"WARNING: found adjustmentformatidentifier {row[2]} but no photo for uuid {row[0]}"
|
||||
)
|
||||
|
||||
@ -2696,7 +2657,7 @@ class PhotosDB:
|
||||
try:
|
||||
folders = self._dbalbum_folders[album_uuid]
|
||||
except KeyError:
|
||||
logging.debug(f"Caught _dbalbum_folders KeyError for album: {album_uuid}")
|
||||
logger.debug(f"Caught _dbalbum_folders KeyError for album: {album_uuid}")
|
||||
return []
|
||||
|
||||
def _recurse_folder_hierarchy(folders, hierarchy=[]):
|
||||
@ -2733,7 +2694,7 @@ class PhotosDB:
|
||||
try:
|
||||
folders = self._dbalbum_folders[album_uuid]
|
||||
except KeyError:
|
||||
logging.debug(f"Caught _dbalbum_folders KeyError for album: {album_uuid}")
|
||||
logger.debug(f"Caught _dbalbum_folders KeyError for album: {album_uuid}")
|
||||
return []
|
||||
|
||||
def _recurse_folder_hierarchy(folders, hierarchy=[]):
|
||||
|
||||
@ -1,12 +1,20 @@
|
||||
""" QueryOptions class for PhotosDB.query """
|
||||
|
||||
import dataclasses
|
||||
import datetime
|
||||
import pathlib
|
||||
from dataclasses import asdict, dataclass
|
||||
from typing import Iterable, List, Optional, Tuple
|
||||
|
||||
import bitmath
|
||||
|
||||
__all__ = ["QueryOptions"]
|
||||
__all__ = ["QueryOptions", "query_options_from_kwargs", "IncompatibleQueryOptions"]
|
||||
|
||||
|
||||
class IncompatibleQueryOptions(Exception):
|
||||
"""Incompatible query options"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
@ -182,3 +190,128 @@ class QueryOptions:
|
||||
|
||||
def asdict(self):
|
||||
return asdict(self)
|
||||
|
||||
|
||||
def query_options_from_kwargs(**kwargs) -> QueryOptions:
|
||||
"""Validate query options and create a QueryOptions instance"""
|
||||
# sanity check input args
|
||||
nonexclusive = [
|
||||
"added_after",
|
||||
"added_before",
|
||||
"added_in_last",
|
||||
"album",
|
||||
"duplicate",
|
||||
"exif",
|
||||
"external_edit",
|
||||
"folder",
|
||||
"from_date",
|
||||
"from_time",
|
||||
"has_raw",
|
||||
"keyword",
|
||||
"label",
|
||||
"max_size",
|
||||
"min_size",
|
||||
"name",
|
||||
"person",
|
||||
"query_eval",
|
||||
"query_function",
|
||||
"regex",
|
||||
"selected",
|
||||
"to_date",
|
||||
"to_time",
|
||||
"uti",
|
||||
"uuid",
|
||||
"uuid_from_file",
|
||||
"year",
|
||||
]
|
||||
exclusive = [
|
||||
("burst", "not_burst"),
|
||||
("cloudasset", "not_cloudasset"),
|
||||
("edited", "not_edited"),
|
||||
("favorite", "not_favorite"),
|
||||
("has_comment", "no_comment"),
|
||||
("has_likes", "no_likes"),
|
||||
("hdr", "not_hdr"),
|
||||
("hidden", "not_hidden"),
|
||||
("in_album", "not_in_album"),
|
||||
("incloud", "not_incloud"),
|
||||
("is_reference", "not_reference"),
|
||||
("keyword", "no_keyword"),
|
||||
("live", "not_live"),
|
||||
("location", "no_location"),
|
||||
("missing", "not_missing"),
|
||||
("only_photos", "only_movies"),
|
||||
("panorama", "not_panorama"),
|
||||
("portrait", "not_portrait"),
|
||||
("screenshot", "not_screenshot"),
|
||||
("selfie", "not_selfie"),
|
||||
("shared", "not_shared"),
|
||||
("slow_mo", "not_slow_mo"),
|
||||
("time_lapse", "not_time_lapse"),
|
||||
("deleted", "not_deleted"),
|
||||
]
|
||||
# TODO: add option to validate requiring at least one query arg
|
||||
for arg, not_arg in exclusive:
|
||||
if kwargs.get(arg) and kwargs.get(not_arg):
|
||||
arg = arg.replace("_", "-")
|
||||
not_arg = not_arg.replace("_", "-")
|
||||
raise IncompatibleQueryOptions(
|
||||
f"--{arg} and --{not_arg} are mutually exclusive"
|
||||
)
|
||||
|
||||
# some options like title can be specified multiple times
|
||||
# check if any of them are specified along with their no_ counterpart
|
||||
exclusive_multi_options = ["title", "description", "place", "keyword"]
|
||||
for option in exclusive_multi_options:
|
||||
if kwargs.get(option) and kwargs.get("no_{option}"):
|
||||
raise IncompatibleQueryOptions(
|
||||
f"--{option} and --no-{option} are mutually exclusive"
|
||||
)
|
||||
|
||||
include_photos = True
|
||||
include_movies = True # default searches for everything
|
||||
if kwargs.get("only_movies"):
|
||||
include_photos = False
|
||||
if kwargs.get("only_photos"):
|
||||
include_movies = False
|
||||
|
||||
# load UUIDs if necessary and append to any uuids passed with --uuid
|
||||
uuid = None
|
||||
if uuid_from_file := kwargs.get("uuid_from_file"):
|
||||
uuid_list = list(kwargs.get("uuid", [])) # Click option is a tuple
|
||||
uuid_list.extend(load_uuid_from_file(uuid_from_file))
|
||||
uuid = tuple(uuid_list)
|
||||
|
||||
query_fields = [field.name for field in dataclasses.fields(QueryOptions)]
|
||||
query_dict = {field: kwargs.get(field) for field in query_fields}
|
||||
query_dict["photos"] = include_photos
|
||||
query_dict["movies"] = include_movies
|
||||
query_dict["uuid"] = uuid
|
||||
return QueryOptions(**query_dict)
|
||||
|
||||
|
||||
def load_uuid_from_file(filename):
|
||||
"""Load UUIDs from file. Does not validate UUIDs.
|
||||
Format is 1 UUID per line, any line beginning with # is ignored.
|
||||
Whitespace is stripped.
|
||||
|
||||
Arguments:
|
||||
filename: file name of the file containing UUIDs
|
||||
|
||||
Returns:
|
||||
list of UUIDs or empty list of no UUIDs in file
|
||||
|
||||
Raises:
|
||||
FileNotFoundError if file does not exist
|
||||
"""
|
||||
|
||||
if not pathlib.Path(filename).is_file():
|
||||
raise FileNotFoundError(f"Could not find file {filename}")
|
||||
|
||||
uuid = []
|
||||
with open(filename, "r") as uuid_file:
|
||||
for line in uuid_file:
|
||||
line = line.strip()
|
||||
if len(line) and line[0] != "#":
|
||||
uuid.append(line)
|
||||
return uuid
|
||||
|
||||
@ -5,6 +5,7 @@ import pathlib
|
||||
import sqlite3
|
||||
from typing import List, Tuple
|
||||
|
||||
logger = logging.getLogger("osxphotos")
|
||||
|
||||
def sqlite_open_ro(dbname: str) -> Tuple[sqlite3.Connection, sqlite3.Cursor]:
|
||||
"""opens sqlite file dbname in read-only mode
|
||||
@ -32,7 +33,7 @@ def sqlite_db_is_locked(dbname):
|
||||
conn.close()
|
||||
locked = False
|
||||
except Exception as e:
|
||||
logging.debug(f"sqlite_db_is_locked: {e}")
|
||||
logger.debug(f"sqlite_db_is_locked: {e}")
|
||||
locked = True
|
||||
|
||||
return locked
|
||||
|
||||
@ -11,11 +11,11 @@ from Foundation import NSDictionary
|
||||
# needed to capture system-level stderr
|
||||
from wurlitzer import pipes
|
||||
|
||||
from .utils import _get_os_version
|
||||
from .utils import get_macos_version
|
||||
|
||||
__all__ = ["detect_text", "make_request_handler"]
|
||||
|
||||
ver, major, minor = _get_os_version()
|
||||
ver, major, minor = get_macos_version()
|
||||
if ver == "10" and int(major) < 15:
|
||||
vision = False
|
||||
else:
|
||||
|
||||
@ -26,7 +26,7 @@ import tempfile
|
||||
import CoreServices
|
||||
import objc
|
||||
|
||||
from .utils import _get_os_version
|
||||
from .utils import get_macos_version
|
||||
|
||||
__all__ = ["get_preferred_uti_extension", "get_uti_for_extension"]
|
||||
|
||||
@ -522,7 +522,7 @@ def _load_uti_dict():
|
||||
_load_uti_dict()
|
||||
|
||||
# OS version for determining which methods can be used
|
||||
OS_VER, OS_MAJOR, _ = (int(x) for x in _get_os_version())
|
||||
OS_VER, OS_MAJOR, _ = (int(x) for x in get_macos_version())
|
||||
|
||||
|
||||
def _get_uti_from_mdls(extension):
|
||||
|
||||
@ -25,14 +25,16 @@ import shortuuid
|
||||
|
||||
from ._constants import UNICODE_FORMAT
|
||||
|
||||
logger = logging.getLogger("osxphotos")
|
||||
|
||||
__all__ = [
|
||||
"dd_to_dms_str",
|
||||
"expand_and_validate_filepath",
|
||||
"get_last_library_path",
|
||||
"get_system_library_path",
|
||||
"hexdigest",
|
||||
"increment_filename_with_count",
|
||||
"increment_filename",
|
||||
"increment_filename_with_count",
|
||||
"lineno",
|
||||
"list_directory",
|
||||
"list_photo_libraries",
|
||||
@ -46,23 +48,9 @@ __all__ = [
|
||||
]
|
||||
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.DEBUG,
|
||||
format="%(asctime)s - %(levelname)s - %(filename)s - %(lineno)d - %(message)s",
|
||||
)
|
||||
|
||||
VERSION_INFO_URL = "https://pypi.org/pypi/osxphotos/json"
|
||||
|
||||
|
||||
def _get_logger():
|
||||
"""Used only for testing
|
||||
|
||||
Returns:
|
||||
logging.Logger object -- logging.Logger object for osxphotos
|
||||
"""
|
||||
return logging.Logger(__name__)
|
||||
|
||||
|
||||
def noop(*args, **kwargs):
|
||||
"""do nothing (no operation)"""
|
||||
pass
|
||||
@ -76,7 +64,7 @@ def lineno(filename):
|
||||
return f"{filename}: {line}"
|
||||
|
||||
|
||||
def _get_os_version():
|
||||
def get_macos_version():
|
||||
# returns tuple of str containing OS version
|
||||
# e.g. 10.13.6 = ("10", "13", "6")
|
||||
version = platform.mac_ver()[0].split(".")
|
||||
@ -176,9 +164,9 @@ 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 """
|
||||
_, major, _ = _get_os_version()
|
||||
_, major, _ = get_macos_version()
|
||||
if int(major) < 15:
|
||||
logging.debug(
|
||||
logger.debug(
|
||||
f"get_system_library_path not implemented for MacOS < 10.15: you have {major}"
|
||||
)
|
||||
return None
|
||||
@ -191,7 +179,7 @@ def get_system_library_path():
|
||||
with open(plist_file, "rb") as fp:
|
||||
pl = plistload(fp)
|
||||
else:
|
||||
logging.debug(f"could not find plist file: {str(plist_file)}")
|
||||
logger.debug(f"could not find plist file: {str(plist_file)}")
|
||||
return None
|
||||
|
||||
return pl.get("SystemLibraryPath")
|
||||
@ -208,7 +196,7 @@ def get_last_library_path():
|
||||
with open(plist_file, "rb") as fp:
|
||||
pl = plistload(fp)
|
||||
else:
|
||||
logging.debug(f"could not find plist file: {str(plist_file)}")
|
||||
logger.debug(f"could not find plist file: {str(plist_file)}")
|
||||
return None
|
||||
|
||||
# get the IPXDefaultLibraryURLBookmark from com.apple.Photos.plist
|
||||
@ -244,7 +232,7 @@ def get_last_library_path():
|
||||
|
||||
return photospath
|
||||
else:
|
||||
logging.debug("Could not get path to Photos database")
|
||||
logger.debug("Could not get path to Photos database")
|
||||
return None
|
||||
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@ -7,7 +7,7 @@
|
||||
<key>hostuuid</key>
|
||||
<string>585B80BF-8D1F-55EF-A9E8-6CF4E5523959</string>
|
||||
<key>pid</key>
|
||||
<integer>1961</integer>
|
||||
<integer>508</integer>
|
||||
<key>processname</key>
|
||||
<string>photolibraryd</string>
|
||||
<key>uid</key>
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 2.8 MiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 120 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -2,12 +2,18 @@
|
||||
|
||||
import datetime
|
||||
import pathlib
|
||||
import time
|
||||
|
||||
from tests.parse_timewarp_output import CompareValues, InspectValues
|
||||
|
||||
TEST_LIBRARY_TIMEWARP = "tests/TestTimeWarp-10.15.7.photoslibrary"
|
||||
|
||||
|
||||
def is_dst() -> bool:
|
||||
"""Return True if daylight savings time is in effect"""
|
||||
return bool(time.localtime().tm_isdst)
|
||||
|
||||
|
||||
def get_file_timestamp(file: str) -> str:
|
||||
"""Get timestamp of file"""
|
||||
return datetime.datetime.fromtimestamp(pathlib.Path(file).stat().st_mtime).strftime(
|
||||
@ -24,6 +30,7 @@ CATALINA_PHOTOS_5 = {
|
||||
"marigold flowers": "IMG_6517.jpeg",
|
||||
"multi-colored zinnia flowers": "IMG_6506.jpeg",
|
||||
"sunset": "IMG_6551.mov",
|
||||
"palm tree": "20230120_010203-0400.jpg",
|
||||
},
|
||||
"inspect": {
|
||||
# IMG_6501.jpeg
|
||||
@ -362,4 +369,28 @@ CATALINA_PHOTOS_5 = {
|
||||
"GMT-0700",
|
||||
),
|
||||
},
|
||||
"parse_date": {
|
||||
# 20230120_010203-0400.jpg
|
||||
"uuid": "5285C4E2-BB1A-49DF-AEF5-246AA337ACAB",
|
||||
"expected": InspectValues(
|
||||
"20230120_010203-0400.jpg",
|
||||
"5285C4E2-BB1A-49DF-AEF5-246AA337ACAB",
|
||||
"2023-01-20 01:02:03-0800" if not is_dst() else "2023-01-20 00:02:03-0700",
|
||||
"2023-01-20 01:02:03-0800" if not is_dst() else "2023-01-20 00:02:03-0700",
|
||||
"-0800",
|
||||
"GMT-0800",
|
||||
),
|
||||
},
|
||||
"parse_date_tz": {
|
||||
# 20230120_010203-0400.jpg
|
||||
"uuid": "5285C4E2-BB1A-49DF-AEF5-246AA337ACAB",
|
||||
"expected": InspectValues(
|
||||
"20230120_010203-0400.jpg",
|
||||
"5285C4E2-BB1A-49DF-AEF5-246AA337ACAB",
|
||||
"2023-01-19 21:02:03-0800" if not is_dst() else "2023-01-19 20:02:03-0700",
|
||||
"2023-01-20 01:02:03-0400",
|
||||
"-0400",
|
||||
"GMT-0400",
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
@ -66,19 +66,19 @@ elif OS_VER[0] == "13":
|
||||
TEST_LIBRARY_SYNC = TEST_LIBRARY
|
||||
from tests.config_timewarp_ventura import TEST_LIBRARY_TIMEWARP
|
||||
|
||||
TEST_LIBRARY_ADD_LOCATIONS = None
|
||||
TEST_LIBRARY_ADD_LOCATIONS = "tests/Test-13.0.0.photoslibrary"
|
||||
else:
|
||||
TEST_LIBRARY = None
|
||||
TEST_LIBRARY_TIMEWARP = None
|
||||
TEST_LIBRARY_SYNC = None
|
||||
TEST_LIBRARY_ADD_LOCATIONS = "tests/Test-13.0.0.photoslibrary"
|
||||
TEST_LIBRARY_ADD_LOCATIONS = None
|
||||
|
||||
|
||||
@pytest.fixture(scope="session", autouse=True)
|
||||
def setup_photos_timewarp():
|
||||
if not TEST_TIMEWARP:
|
||||
return
|
||||
copy_photos_library(TEST_LIBRARY_TIMEWARP, delay=10)
|
||||
copy_photos_library(TEST_LIBRARY_TIMEWARP, delay=5)
|
||||
|
||||
|
||||
@pytest.fixture(scope="session", autouse=True)
|
||||
|
||||
@ -25,9 +25,7 @@ CompareValues = namedtuple(
|
||||
def parse_inspect_output(output: str) -> List[InspectValues]:
|
||||
"""Parse output of --inspect and return list of InspectValues named tuple"""
|
||||
|
||||
with open(output, "r") as f:
|
||||
lines = f.readlines()
|
||||
lines = [line for line in lines if line.strip()]
|
||||
lines = [line for line in output.split("\n") if line.strip()]
|
||||
# remove header
|
||||
lines.pop(0)
|
||||
values = []
|
||||
@ -40,9 +38,7 @@ def parse_inspect_output(output: str) -> List[InspectValues]:
|
||||
|
||||
def parse_compare_exif(output: str) -> List[CompareValues]:
|
||||
"""Parse output of --compare-exif and return list of CompareValues named tuple"""
|
||||
with open(output, "r") as f:
|
||||
lines = f.readlines()
|
||||
lines = [line for line in lines if line.strip()]
|
||||
lines = [line for line in output.split("\n") if line.strip()]
|
||||
# remove header
|
||||
lines.pop(0)
|
||||
values = []
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -14,9 +14,9 @@ import pytest
|
||||
import osxphotos
|
||||
from osxphotos._constants import _UNKNOWN_PERSON
|
||||
from osxphotos.photoexporter import PhotoExporter
|
||||
from osxphotos.utils import _get_os_version
|
||||
from osxphotos.utils import get_macos_version
|
||||
|
||||
OS_VERSION = _get_os_version()
|
||||
OS_VERSION = get_macos_version()
|
||||
SKIP_TEST = "OSXPHOTOS_TEST_EXPORT" not in os.environ or OS_VERSION[1] != "15"
|
||||
PHOTOS_DB_LOCAL = os.path.expanduser("~/Pictures/Photos Library.photoslibrary")
|
||||
|
||||
|
||||
@ -73,18 +73,18 @@ def test_select_pears(photoslib, suspend_capture):
|
||||
|
||||
|
||||
@pytest.mark.timewarp
|
||||
def test_inspect(photoslib, suspend_capture, output_file):
|
||||
def test_inspect(photoslib, suspend_capture):
|
||||
"""Test --inspect. NOTE: this test requires user interaction"""
|
||||
from osxphotos.cli.timewarp import timewarp
|
||||
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
timewarp,
|
||||
["--inspect", "--plain", "--force", "-o", output_file],
|
||||
["--inspect", "--plain", "--force"],
|
||||
terminal_width=TERMINAL_WIDTH,
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
values = parse_inspect_output(output_file)
|
||||
values = parse_inspect_output(result.output)
|
||||
assert TEST_DATA["inspect"]["expected"] == values
|
||||
|
||||
|
||||
@ -111,7 +111,7 @@ def test_date(photoslib, suspend_capture):
|
||||
|
||||
@pytest.mark.timewarp
|
||||
@pytest.mark.parametrize("input_value,expected", TEST_DATA["date_delta"]["parameters"])
|
||||
def test_date_delta(photoslib, suspend_capture, input_value, expected, output_file):
|
||||
def test_date_delta(photoslib, suspend_capture, input_value, expected):
|
||||
"""Test --date-delta"""
|
||||
from osxphotos.cli.timewarp import timewarp
|
||||
|
||||
@ -129,16 +129,16 @@ def test_date_delta(photoslib, suspend_capture, input_value, expected, output_fi
|
||||
assert result.exit_code == 0
|
||||
result = runner.invoke(
|
||||
timewarp,
|
||||
["--inspect", "--plain", "--force", "-o", output_file],
|
||||
["--inspect", "--plain", "--force"],
|
||||
terminal_width=TERMINAL_WIDTH,
|
||||
)
|
||||
output_values = parse_inspect_output(output_file)
|
||||
output_values = parse_inspect_output(result.output)
|
||||
assert output_values[0].date_tz == expected
|
||||
|
||||
|
||||
@pytest.mark.timewarp
|
||||
@pytest.mark.parametrize("input_value,expected", TEST_DATA["time"]["parameters"])
|
||||
def test_time(photoslib, suspend_capture, input_value, expected, output_file):
|
||||
def test_time(photoslib, suspend_capture, input_value, expected):
|
||||
"""Test --time"""
|
||||
from osxphotos.cli.timewarp import timewarp
|
||||
|
||||
@ -158,16 +158,16 @@ def test_time(photoslib, suspend_capture, input_value, expected, output_file):
|
||||
# don't use photo.date as it will return local time instead of the time in the timezone
|
||||
result = runner.invoke(
|
||||
timewarp,
|
||||
["--inspect", "--plain", "--force", "-o", output_file],
|
||||
["--inspect", "--plain", "--force"],
|
||||
terminal_width=TERMINAL_WIDTH,
|
||||
)
|
||||
output_values = parse_inspect_output(output_file)
|
||||
output_values = parse_inspect_output(result.output)
|
||||
assert output_values[0].date_tz == expected
|
||||
|
||||
|
||||
@pytest.mark.timewarp
|
||||
@pytest.mark.parametrize("input_value,expected", TEST_DATA["time_delta"]["parameters"])
|
||||
def test_time_delta(photoslib, suspend_capture, input_value, expected, output_file):
|
||||
def test_time_delta(photoslib, suspend_capture, input_value, expected):
|
||||
"""Test --time-delta"""
|
||||
from osxphotos.cli.timewarp import timewarp
|
||||
|
||||
@ -185,10 +185,10 @@ def test_time_delta(photoslib, suspend_capture, input_value, expected, output_fi
|
||||
assert result.exit_code == 0
|
||||
result = runner.invoke(
|
||||
timewarp,
|
||||
["--inspect", "--plain", "--force", "-o", output_file],
|
||||
["--inspect", "--plain", "--force"],
|
||||
terminal_width=TERMINAL_WIDTH,
|
||||
)
|
||||
output_values = parse_inspect_output(output_file)
|
||||
output_values = parse_inspect_output(result.output)
|
||||
assert output_values[0].date_tz == expected
|
||||
|
||||
|
||||
@ -216,34 +216,28 @@ def test_time_zone(
|
||||
assert result.exit_code == 0
|
||||
result = runner.invoke(
|
||||
timewarp,
|
||||
["--inspect", "--plain", "--force", "-o", output_file],
|
||||
["--inspect", "--plain", "--force"],
|
||||
terminal_width=TERMINAL_WIDTH,
|
||||
)
|
||||
output_values = parse_inspect_output(output_file)
|
||||
output_values = parse_inspect_output(result.output)
|
||||
assert output_values[0].date_tz == expected_date
|
||||
assert output_values[0].tz_offset == expected_tz
|
||||
|
||||
|
||||
@pytest.mark.timewarp
|
||||
@pytest.mark.parametrize("expected", TEST_DATA["compare_exif"]["expected"])
|
||||
def test_compare_exif(photoslib, suspend_capture, expected, output_file):
|
||||
def test_compare_exif(photoslib, suspend_capture, expected):
|
||||
"""Test --compare-exif"""
|
||||
from osxphotos.cli.timewarp import timewarp
|
||||
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
timewarp,
|
||||
[
|
||||
"--compare-exif",
|
||||
"--plain",
|
||||
"--force",
|
||||
"-o",
|
||||
output_file,
|
||||
],
|
||||
["--compare-exif", "--plain", "--force"],
|
||||
terminal_width=TERMINAL_WIDTH,
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
output_values = parse_compare_exif(output_file)
|
||||
output_values = parse_compare_exif(result.output)
|
||||
assert output_values[0] == expected
|
||||
|
||||
|
||||
@ -281,24 +275,24 @@ def test_select_sunflowers(photoslib, suspend_capture):
|
||||
|
||||
@pytest.mark.timewarp
|
||||
@pytest.mark.parametrize("expected", TEST_DATA["compare_exif_3"]["expected"])
|
||||
def test_compare_exif_3(photoslib, suspend_capture, expected, output_file):
|
||||
def test_compare_exif_3(photoslib, suspend_capture, expected):
|
||||
"""Test --compare-exif"""
|
||||
from osxphotos.cli.timewarp import timewarp
|
||||
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
timewarp,
|
||||
["--compare-exif", "--plain", "--force", "-o", output_file],
|
||||
["--compare-exif", "--plain", "--force"],
|
||||
terminal_width=TERMINAL_WIDTH,
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
output_values = parse_compare_exif(output_file)
|
||||
output_values = parse_compare_exif(result.output)
|
||||
assert output_values[0] == expected
|
||||
|
||||
|
||||
@pytest.mark.timewarp
|
||||
@pytest.mark.parametrize("input_value,expected", TEST_DATA["match"]["parameters"])
|
||||
def test_match(photoslib, suspend_capture, input_value, expected, output_file):
|
||||
def test_match(photoslib, suspend_capture, input_value, expected):
|
||||
"""Test --timezone --match"""
|
||||
from osxphotos.cli.timewarp import timewarp
|
||||
|
||||
@ -317,10 +311,10 @@ def test_match(photoslib, suspend_capture, input_value, expected, output_file):
|
||||
assert result.exit_code == 0
|
||||
result = runner.invoke(
|
||||
timewarp,
|
||||
["--inspect", "--plain", "--force", "-o", output_file],
|
||||
["--inspect", "--plain", "--force"],
|
||||
terminal_width=TERMINAL_WIDTH,
|
||||
)
|
||||
output_values = parse_inspect_output(output_file)
|
||||
output_values = parse_inspect_output(result.output)
|
||||
assert output_values[0].date_tz == expected
|
||||
|
||||
|
||||
@ -380,10 +374,10 @@ def test_push_exif_1(
|
||||
assert result.exit_code == 0
|
||||
result = runner.invoke(
|
||||
timewarp,
|
||||
["--inspect", "--plain", "--force", "-o", output_file],
|
||||
["--inspect", "--plain", "--force"],
|
||||
terminal_width=TERMINAL_WIDTH,
|
||||
)
|
||||
output_values = parse_inspect_output(output_file)
|
||||
output_values = parse_inspect_output(result.output)
|
||||
assert output_values[0].date_tz == expected_date
|
||||
|
||||
photo = photoslib.selection[0]
|
||||
@ -402,7 +396,7 @@ def test_select_pears_2(photoslib, suspend_capture):
|
||||
|
||||
|
||||
@pytest.mark.timewarp
|
||||
def test_push_exif_2(photoslib, suspend_capture, output_file):
|
||||
def test_push_exif_2(photoslib, suspend_capture):
|
||||
"""Test --push-exif"""
|
||||
pre_test = TEST_DATA["push_exif"]["pre"]
|
||||
post_test = TEST_DATA["push_exif"]["post"]
|
||||
@ -413,10 +407,10 @@ def test_push_exif_2(photoslib, suspend_capture, output_file):
|
||||
|
||||
result = runner.invoke(
|
||||
timewarp,
|
||||
["--compare-exif", "--plain", "--force", "-o", output_file],
|
||||
["--compare-exif", "--plain", "--force"],
|
||||
terminal_width=TERMINAL_WIDTH,
|
||||
)
|
||||
output_values = parse_compare_exif(output_file)
|
||||
output_values = parse_compare_exif(result.output)
|
||||
assert output_values[0] == pre_test
|
||||
|
||||
result = runner.invoke(
|
||||
@ -433,15 +427,15 @@ def test_push_exif_2(photoslib, suspend_capture, output_file):
|
||||
|
||||
result = runner.invoke(
|
||||
timewarp,
|
||||
["--compare-exif", "--plain", "--force", "-o", output_file],
|
||||
["--compare-exif", "--plain", "--force"],
|
||||
terminal_width=TERMINAL_WIDTH,
|
||||
)
|
||||
output_values = parse_compare_exif(output_file)
|
||||
output_values = parse_compare_exif(result.output)
|
||||
assert output_values[0] == post_test
|
||||
|
||||
|
||||
@pytest.mark.timewarp
|
||||
def test_pull_exif_1(photoslib, suspend_capture, output_file):
|
||||
def test_pull_exif_1(photoslib, suspend_capture):
|
||||
"""Test --pull-exif"""
|
||||
pre_test = TEST_DATA["pull_exif_1"]["pre"]
|
||||
post_test = TEST_DATA["pull_exif_1"]["post"]
|
||||
@ -460,10 +454,10 @@ def test_pull_exif_1(photoslib, suspend_capture, output_file):
|
||||
|
||||
result = runner.invoke(
|
||||
timewarp,
|
||||
["--compare-exif", "--plain", "--force", "-o", output_file],
|
||||
["--compare-exif", "--plain", "--force"],
|
||||
terminal_width=TERMINAL_WIDTH,
|
||||
)
|
||||
output_values = parse_compare_exif(output_file)
|
||||
output_values = parse_compare_exif(result.output)
|
||||
assert output_values[0] == pre_test
|
||||
|
||||
result = runner.invoke(
|
||||
@ -480,10 +474,10 @@ def test_pull_exif_1(photoslib, suspend_capture, output_file):
|
||||
|
||||
result = runner.invoke(
|
||||
timewarp,
|
||||
["--compare-exif", "--plain", "--force", "-o", output_file],
|
||||
["--compare-exif", "--plain", "--force"],
|
||||
terminal_width=TERMINAL_WIDTH,
|
||||
)
|
||||
output_values = parse_compare_exif(output_file)
|
||||
output_values = parse_compare_exif(result.output)
|
||||
assert output_values[0] == post_test
|
||||
|
||||
|
||||
@ -494,7 +488,7 @@ def test_select_apple_tree(photoslib, suspend_capture):
|
||||
|
||||
|
||||
@pytest.mark.timewarp
|
||||
def test_pull_exif_no_time(photoslib, suspend_capture, output_file):
|
||||
def test_pull_exif_no_time(photoslib, suspend_capture):
|
||||
"""Test --pull-exif when photo has invalid date/time in EXIF"""
|
||||
pre_test = TEST_DATA["pull_exif_no_time"]["pre"]
|
||||
post_test = TEST_DATA["pull_exif_no_time"]["post"]
|
||||
@ -505,10 +499,10 @@ def test_pull_exif_no_time(photoslib, suspend_capture, output_file):
|
||||
|
||||
result = runner.invoke(
|
||||
timewarp,
|
||||
["--compare-exif", "--plain", "--force", "-o", output_file],
|
||||
["--compare-exif", "--plain", "--force"],
|
||||
terminal_width=TERMINAL_WIDTH,
|
||||
)
|
||||
output_values = parse_compare_exif(output_file)
|
||||
output_values = parse_compare_exif(result.output)
|
||||
assert output_values[0] == pre_test
|
||||
|
||||
result = runner.invoke(
|
||||
@ -525,10 +519,10 @@ def test_pull_exif_no_time(photoslib, suspend_capture, output_file):
|
||||
|
||||
result = runner.invoke(
|
||||
timewarp,
|
||||
["--compare-exif", "--plain", "--force", "-o", output_file],
|
||||
["--compare-exif", "--plain", "--force"],
|
||||
terminal_width=TERMINAL_WIDTH,
|
||||
)
|
||||
output_values = parse_compare_exif(output_file)
|
||||
output_values = parse_compare_exif(result.output)
|
||||
assert output_values[0] == post_test
|
||||
|
||||
|
||||
@ -539,7 +533,7 @@ def test_select_marigolds(photoslib, suspend_capture):
|
||||
|
||||
|
||||
@pytest.mark.timewarp
|
||||
def test_pull_exif_no_offset(photoslib, suspend_capture, output_file):
|
||||
def test_pull_exif_no_offset(photoslib, suspend_capture):
|
||||
"""Test --pull-exif when photo has no offset in EXIF"""
|
||||
pre_test = TEST_DATA["pull_exif_no_offset"]["pre"]
|
||||
post_test = TEST_DATA["pull_exif_no_offset"]["post"]
|
||||
@ -550,10 +544,10 @@ def test_pull_exif_no_offset(photoslib, suspend_capture, output_file):
|
||||
|
||||
result = runner.invoke(
|
||||
timewarp,
|
||||
["--compare-exif", "--plain", "--force", "-o", output_file],
|
||||
["--compare-exif", "--plain", "--force"],
|
||||
terminal_width=TERMINAL_WIDTH,
|
||||
)
|
||||
output_values = parse_compare_exif(output_file)
|
||||
output_values = parse_compare_exif(result.output)
|
||||
assert output_values[0] == pre_test
|
||||
|
||||
result = runner.invoke(
|
||||
@ -570,10 +564,10 @@ def test_pull_exif_no_offset(photoslib, suspend_capture, output_file):
|
||||
|
||||
result = runner.invoke(
|
||||
timewarp,
|
||||
["--compare-exif", "--plain", "--force", "-o", output_file],
|
||||
["--compare-exif", "--plain", "--force"],
|
||||
terminal_width=TERMINAL_WIDTH,
|
||||
)
|
||||
output_values = parse_compare_exif(output_file)
|
||||
output_values = parse_compare_exif(result.output)
|
||||
assert output_values[0] == post_test
|
||||
|
||||
|
||||
@ -586,7 +580,7 @@ def test_select_zinnias(photoslib, suspend_capture):
|
||||
|
||||
|
||||
@pytest.mark.timewarp
|
||||
def test_pull_exif_no_data(photoslib, suspend_capture, output_file):
|
||||
def test_pull_exif_no_data(photoslib, suspend_capture):
|
||||
"""Test --pull-exif when photo has no data in EXIF"""
|
||||
pre_test = TEST_DATA["pull_exif_no_data"]["pre"]
|
||||
post_test = TEST_DATA["pull_exif_no_data"]["post"]
|
||||
@ -597,10 +591,10 @@ def test_pull_exif_no_data(photoslib, suspend_capture, output_file):
|
||||
|
||||
result = runner.invoke(
|
||||
timewarp,
|
||||
["--compare-exif", "--plain", "--force", "-o", output_file],
|
||||
["--compare-exif", "--plain", "--force"],
|
||||
terminal_width=TERMINAL_WIDTH,
|
||||
)
|
||||
output_values = parse_compare_exif(output_file)
|
||||
output_values = parse_compare_exif(result.output)
|
||||
assert output_values[0] == pre_test
|
||||
|
||||
result = runner.invoke(
|
||||
@ -618,15 +612,15 @@ def test_pull_exif_no_data(photoslib, suspend_capture, output_file):
|
||||
|
||||
result = runner.invoke(
|
||||
timewarp,
|
||||
["--compare-exif", "--plain", "--force", "-o", output_file],
|
||||
["--compare-exif", "--plain", "--force"],
|
||||
terminal_width=TERMINAL_WIDTH,
|
||||
)
|
||||
output_values = parse_compare_exif(output_file)
|
||||
output_values = parse_compare_exif(result.output)
|
||||
assert output_values[0] == post_test
|
||||
|
||||
|
||||
@pytest.mark.timewarp
|
||||
def test_pull_exif_no_data_use_file_time(photoslib, suspend_capture, output_file):
|
||||
def test_pull_exif_no_data_use_file_time(photoslib, suspend_capture):
|
||||
"""Test --pull-exif when photo has no data in EXIF with --use-file-time"""
|
||||
pre_test = TEST_DATA["pull_exif_no_data_use_file_time"]["pre"]
|
||||
post_test = TEST_DATA["pull_exif_no_data_use_file_time"]["post"]
|
||||
@ -637,10 +631,10 @@ def test_pull_exif_no_data_use_file_time(photoslib, suspend_capture, output_file
|
||||
|
||||
result = runner.invoke(
|
||||
timewarp,
|
||||
["--compare-exif", "--plain", "--force", "-o", output_file],
|
||||
["--compare-exif", "--plain", "--force"],
|
||||
terminal_width=TERMINAL_WIDTH,
|
||||
)
|
||||
output_values = parse_compare_exif(output_file)
|
||||
output_values = parse_compare_exif(result.output)
|
||||
assert output_values[0] == pre_test
|
||||
|
||||
result = runner.invoke(
|
||||
@ -659,10 +653,10 @@ def test_pull_exif_no_data_use_file_time(photoslib, suspend_capture, output_file
|
||||
|
||||
result = runner.invoke(
|
||||
timewarp,
|
||||
["--compare-exif", "--plain", "--force", "-o", output_file],
|
||||
["--compare-exif", "--plain", "--force"],
|
||||
terminal_width=TERMINAL_WIDTH,
|
||||
)
|
||||
output_values = parse_compare_exif(output_file)
|
||||
output_values = parse_compare_exif(result.output)
|
||||
assert output_values[0] == post_test
|
||||
|
||||
|
||||
@ -674,7 +668,7 @@ def test_select_sunset_video(photoslib, suspend_capture):
|
||||
|
||||
@pytest.mark.timewarp
|
||||
@pytest.mark.parametrize("expected", TEST_DATA["compare_video_1"]["expected"])
|
||||
def test_video_compare_exif(photoslib, suspend_capture, expected, output_file):
|
||||
def test_video_compare_exif(photoslib, suspend_capture, expected):
|
||||
"""Test --compare-exif with video"""
|
||||
from osxphotos.cli.timewarp import timewarp
|
||||
|
||||
@ -685,13 +679,11 @@ def test_video_compare_exif(photoslib, suspend_capture, expected, output_file):
|
||||
"--compare-exif",
|
||||
"--plain",
|
||||
"--force",
|
||||
"-o",
|
||||
output_file,
|
||||
],
|
||||
terminal_width=TERMINAL_WIDTH,
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
output_values = parse_compare_exif(output_file)
|
||||
output_values = parse_compare_exif(result.output)
|
||||
assert output_values[0] == expected
|
||||
|
||||
|
||||
@ -708,22 +700,17 @@ def test_video_date_delta(
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
timewarp,
|
||||
[
|
||||
"--date-delta",
|
||||
input_value,
|
||||
"--plain",
|
||||
"--force",
|
||||
],
|
||||
["--date-delta", input_value, "--plain", "--force"],
|
||||
terminal_width=TERMINAL_WIDTH,
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
|
||||
result = runner.invoke(
|
||||
timewarp,
|
||||
["--inspect", "--plain", "--force", "-o", output_file],
|
||||
["--inspect", "--plain", "--force"],
|
||||
terminal_width=TERMINAL_WIDTH,
|
||||
)
|
||||
output_values = parse_inspect_output(output_file)
|
||||
output_values = parse_inspect_output(result.output)
|
||||
assert output_values[0].date_tz == expected
|
||||
|
||||
|
||||
@ -751,16 +738,16 @@ def test_video_time_delta(
|
||||
assert result.exit_code == 0
|
||||
result = runner.invoke(
|
||||
timewarp,
|
||||
["--inspect", "--plain", "--force", "-o", output_file],
|
||||
["--inspect", "--plain", "--force"],
|
||||
terminal_width=TERMINAL_WIDTH,
|
||||
)
|
||||
output_values = parse_inspect_output(output_file)
|
||||
output_values = parse_inspect_output(result.output)
|
||||
assert output_values[0].date_tz == expected
|
||||
|
||||
|
||||
@pytest.mark.timewarp
|
||||
@pytest.mark.parametrize("input_value,expected", TEST_DATA["video_date"]["parameters"])
|
||||
def test_video_date(photoslib, suspend_capture, input_value, expected, output_file):
|
||||
def test_video_date(photoslib, suspend_capture, input_value, expected):
|
||||
"""Test --date with video"""
|
||||
from osxphotos.cli.timewarp import timewarp
|
||||
|
||||
@ -780,16 +767,16 @@ def test_video_date(photoslib, suspend_capture, input_value, expected, output_fi
|
||||
# don't use photo.date as it will return local time instead of the time in the timezone
|
||||
result = runner.invoke(
|
||||
timewarp,
|
||||
["--inspect", "--plain", "--force", "-o", output_file],
|
||||
["--inspect", "--plain", "--force"],
|
||||
terminal_width=TERMINAL_WIDTH,
|
||||
)
|
||||
output_values = parse_inspect_output(output_file)
|
||||
output_values = parse_inspect_output(result.output)
|
||||
assert output_values[0].date_tz == expected
|
||||
|
||||
|
||||
@pytest.mark.timewarp
|
||||
@pytest.mark.parametrize("input_value,expected", TEST_DATA["video_time"]["parameters"])
|
||||
def test_video_time(photoslib, suspend_capture, input_value, expected, output_file):
|
||||
def test_video_time(photoslib, suspend_capture, input_value, expected):
|
||||
"""Test --time with video"""
|
||||
from osxphotos.cli.timewarp import timewarp
|
||||
|
||||
@ -809,10 +796,10 @@ def test_video_time(photoslib, suspend_capture, input_value, expected, output_fi
|
||||
# don't use photo.date as it will return local time instead of the time in the timezone
|
||||
result = runner.invoke(
|
||||
timewarp,
|
||||
["--inspect", "--plain", "--force", "-o", output_file],
|
||||
["--inspect", "--plain", "--force"],
|
||||
terminal_width=TERMINAL_WIDTH,
|
||||
)
|
||||
output_values = parse_inspect_output(output_file)
|
||||
output_values = parse_inspect_output(result.output)
|
||||
assert output_values[0].date_tz == expected
|
||||
|
||||
|
||||
@ -840,17 +827,17 @@ def test_video_time_zone(
|
||||
assert result.exit_code == 0
|
||||
result = runner.invoke(
|
||||
timewarp,
|
||||
["--inspect", "--plain", "--force", "-o", output_file],
|
||||
["--inspect", "--plain", "--force"],
|
||||
terminal_width=TERMINAL_WIDTH,
|
||||
)
|
||||
output_values = parse_inspect_output(output_file)
|
||||
output_values = parse_inspect_output(result.output)
|
||||
assert output_values[0].date_tz == expected_date
|
||||
assert output_values[0].tz_offset == expected_tz
|
||||
|
||||
|
||||
@pytest.mark.timewarp
|
||||
@pytest.mark.parametrize("input_value,expected", TEST_DATA["video_match"]["parameters"])
|
||||
def test_video_match(photoslib, suspend_capture, input_value, expected, output_file):
|
||||
def test_video_match(photoslib, suspend_capture, input_value, expected):
|
||||
"""Test --timezone --match with video"""
|
||||
from osxphotos.cli.timewarp import timewarp
|
||||
|
||||
@ -869,15 +856,15 @@ def test_video_match(photoslib, suspend_capture, input_value, expected, output_f
|
||||
assert result.exit_code == 0
|
||||
result = runner.invoke(
|
||||
timewarp,
|
||||
["--inspect", "--plain", "--force", "-o", output_file],
|
||||
["--inspect", "--plain", "--force"],
|
||||
terminal_width=TERMINAL_WIDTH,
|
||||
)
|
||||
output_values = parse_inspect_output(output_file)
|
||||
output_values = parse_inspect_output(result.output)
|
||||
assert output_values[0].date_tz == expected
|
||||
|
||||
|
||||
@pytest.mark.timewarp
|
||||
def test_video_push_exif(photoslib, suspend_capture, output_file):
|
||||
def test_video_push_exif(photoslib, suspend_capture):
|
||||
"""Test --push-exif with video"""
|
||||
pre_test = TEST_DATA["video_push_exif"]["pre"]
|
||||
post_test = TEST_DATA["video_push_exif"]["post"]
|
||||
@ -888,10 +875,10 @@ def test_video_push_exif(photoslib, suspend_capture, output_file):
|
||||
|
||||
result = runner.invoke(
|
||||
timewarp,
|
||||
["--compare-exif", "--plain", "--force", "-o", output_file],
|
||||
["--compare-exif", "--plain", "--force"],
|
||||
terminal_width=TERMINAL_WIDTH,
|
||||
)
|
||||
output_values = parse_compare_exif(output_file)
|
||||
output_values = parse_compare_exif(result.output)
|
||||
assert output_values[0] == pre_test
|
||||
|
||||
result = runner.invoke(
|
||||
@ -908,15 +895,15 @@ def test_video_push_exif(photoslib, suspend_capture, output_file):
|
||||
|
||||
result = runner.invoke(
|
||||
timewarp,
|
||||
["--compare-exif", "--plain", "--force", "-o", output_file],
|
||||
["--compare-exif", "--plain", "--force"],
|
||||
terminal_width=TERMINAL_WIDTH,
|
||||
)
|
||||
output_values = parse_compare_exif(output_file)
|
||||
output_values = parse_compare_exif(result.output)
|
||||
assert output_values[0] == post_test
|
||||
|
||||
|
||||
@pytest.mark.timewarp
|
||||
def test_video_pull_exif(photoslib, suspend_capture, output_file):
|
||||
def test_video_pull_exif(photoslib, suspend_capture):
|
||||
"""Test --pull-exif with video"""
|
||||
pre_test = TEST_DATA["video_pull_exif"]["pre"]
|
||||
post_test = TEST_DATA["video_pull_exif"]["post"]
|
||||
@ -946,10 +933,10 @@ def test_video_pull_exif(photoslib, suspend_capture, output_file):
|
||||
|
||||
result = runner.invoke(
|
||||
timewarp,
|
||||
["--compare-exif", "--plain", "--force", "-o", output_file],
|
||||
["--compare-exif", "--plain", "--force"],
|
||||
terminal_width=TERMINAL_WIDTH,
|
||||
)
|
||||
output_values = parse_compare_exif(output_file)
|
||||
output_values = parse_compare_exif(result.output)
|
||||
assert output_values[0] == pre_test
|
||||
|
||||
result = runner.invoke(
|
||||
@ -966,10 +953,10 @@ def test_video_pull_exif(photoslib, suspend_capture, output_file):
|
||||
|
||||
result = runner.invoke(
|
||||
timewarp,
|
||||
["--compare-exif", "--plain", "--force", "-o", output_file],
|
||||
["--compare-exif", "--plain", "--force"],
|
||||
terminal_width=TERMINAL_WIDTH,
|
||||
)
|
||||
output_values = parse_compare_exif(output_file)
|
||||
output_values = parse_compare_exif(result.output)
|
||||
assert output_values[0] == post_test
|
||||
|
||||
|
||||
@ -980,7 +967,7 @@ def test_select_pears_3(photoslib, suspend_capture):
|
||||
|
||||
|
||||
@pytest.mark.timewarp
|
||||
def test_function(photoslib, suspend_capture, output_file):
|
||||
def test_function(photoslib, suspend_capture):
|
||||
"""Test timewarp function"""
|
||||
from osxphotos.cli.timewarp import timewarp
|
||||
|
||||
@ -999,10 +986,10 @@ def test_function(photoslib, suspend_capture, output_file):
|
||||
assert result.exit_code == 0
|
||||
result = runner.invoke(
|
||||
timewarp,
|
||||
["--inspect", "--plain", "--force", "-o", output_file],
|
||||
["--inspect", "--plain", "--force"],
|
||||
terminal_width=TERMINAL_WIDTH,
|
||||
)
|
||||
output_values = parse_inspect_output(output_file)
|
||||
output_values = parse_inspect_output(result.output)
|
||||
assert output_values[0] == expected
|
||||
|
||||
|
||||
@ -1013,8 +1000,7 @@ def test_select_palm_tree_1(photoslib, suspend_capture):
|
||||
|
||||
|
||||
@pytest.mark.timewarp
|
||||
@pytest.mark.skipif(get_os_version()[0] != "13", reason="test requires macOS 13")
|
||||
def test_parse_date(photoslib, suspend_capture, output_file):
|
||||
def test_parse_date(photoslib, suspend_capture):
|
||||
"""Test --parse-date"""
|
||||
from osxphotos.cli.timewarp import timewarp
|
||||
|
||||
@ -1033,18 +1019,17 @@ def test_parse_date(photoslib, suspend_capture, output_file):
|
||||
assert result.exit_code == 0
|
||||
result = runner.invoke(
|
||||
timewarp,
|
||||
["--inspect", "--plain", "--force", "-o", output_file],
|
||||
["--inspect", "--plain", "--force"],
|
||||
terminal_width=TERMINAL_WIDTH,
|
||||
)
|
||||
output_values = parse_inspect_output(output_file)
|
||||
output_values = parse_inspect_output(result.output)
|
||||
assert output_values[0].date_local == expected.date_local
|
||||
assert output_values[0].date_tz == expected.date_tz
|
||||
assert output_values[0].tz_offset == expected.tz_offset
|
||||
|
||||
|
||||
@pytest.mark.timewarp
|
||||
@pytest.mark.skipif(get_os_version()[0] != "13", reason="test requires macOS 13")
|
||||
def test_parse_date_tz(photoslib, suspend_capture, output_file):
|
||||
def test_parse_date_tz(photoslib, suspend_capture):
|
||||
"""Test --parse-date with a timezone"""
|
||||
from osxphotos.cli.timewarp import timewarp
|
||||
|
||||
@ -1063,10 +1048,10 @@ def test_parse_date_tz(photoslib, suspend_capture, output_file):
|
||||
assert result.exit_code == 0
|
||||
result = runner.invoke(
|
||||
timewarp,
|
||||
["--inspect", "--plain", "--force", "-o", output_file],
|
||||
["--inspect", "--plain", "--force"],
|
||||
terminal_width=TERMINAL_WIDTH,
|
||||
)
|
||||
output_values = parse_inspect_output(output_file)
|
||||
output_values = parse_inspect_output(result.output)
|
||||
assert output_values[0].date_local == expected.date_local
|
||||
assert output_values[0].date_tz == expected.date_tz
|
||||
assert output_values[0].tz_offset == expected.tz_offset
|
||||
|
||||
@ -2,6 +2,8 @@
|
||||
|
||||
import pytest
|
||||
|
||||
from osxphotos.queryoptions import load_uuid_from_file
|
||||
|
||||
UUID_FILE = "tests/uuid_from_file.txt"
|
||||
MISSING_UUID_FILE = "tests/uuid_not_found.txt"
|
||||
|
||||
@ -14,7 +16,6 @@ UUID_EXPECTED_FROM_FILE = [
|
||||
|
||||
def test_load_uuid_from_file():
|
||||
"""Test load_uuid_from_file function"""
|
||||
from osxphotos.cli import load_uuid_from_file
|
||||
|
||||
uuid_got = load_uuid_from_file(UUID_FILE)
|
||||
assert uuid_got == UUID_EXPECTED_FROM_FILE
|
||||
@ -22,7 +23,6 @@ def test_load_uuid_from_file():
|
||||
|
||||
def test_load_uuid_from_file_filenotfound():
|
||||
"""Test load_uuid_from_file function raises error if file not found"""
|
||||
from osxphotos.cli import load_uuid_from_file
|
||||
|
||||
with pytest.raises(FileNotFoundError) as err:
|
||||
uuid_got = load_uuid_from_file(MISSING_UUID_FILE)
|
||||
|
||||
116
tests/test_cli_verbose.py
Normal file
116
tests/test_cli_verbose.py
Normal file
@ -0,0 +1,116 @@
|
||||
"""Test verbose functions"""
|
||||
|
||||
import re
|
||||
from io import StringIO
|
||||
|
||||
|
||||
from osxphotos.cli.verbose import (
|
||||
get_verbose_level,
|
||||
set_verbose_level,
|
||||
verbose,
|
||||
verbose_print,
|
||||
_reset_verbose_globals,
|
||||
)
|
||||
|
||||
|
||||
def test_set_get_verbose_level(capsys):
|
||||
"""Test verbose_print"""
|
||||
set_verbose_level(2)
|
||||
assert get_verbose_level() == 2
|
||||
|
||||
|
||||
def test_verbose_print_no_rich(capsys):
|
||||
"""Test verbose_print"""
|
||||
set_verbose_level(1)
|
||||
verbose = verbose_print(1, False, False)
|
||||
verbose("test")
|
||||
captured = capsys.readouterr()
|
||||
assert captured.out.strip() == "test"
|
||||
|
||||
verbose("test2", level=1)
|
||||
captured = capsys.readouterr()
|
||||
assert captured.out.strip() == "test2"
|
||||
|
||||
verbose("test3", level=2)
|
||||
captured = capsys.readouterr()
|
||||
assert captured.out.strip() == ""
|
||||
|
||||
|
||||
def test_verbose_print_rich(capsys):
|
||||
"""Test verbose with rich"""
|
||||
set_verbose_level(1)
|
||||
verbose = verbose_print(1, False, True)
|
||||
verbose("test")
|
||||
captured = capsys.readouterr()
|
||||
assert captured.out.strip() == "test"
|
||||
|
||||
verbose("test2", level=1)
|
||||
captured = capsys.readouterr()
|
||||
assert captured.out.strip() == "test2"
|
||||
|
||||
verbose("test3", level=2)
|
||||
captured = capsys.readouterr()
|
||||
assert captured.out.strip() == ""
|
||||
|
||||
|
||||
def test_verbose_print_timestamp(capsys):
|
||||
"""Test verbose with timestamp"""
|
||||
set_verbose_level(1)
|
||||
verbose = verbose_print(1, True, False)
|
||||
verbose("test")
|
||||
captured = capsys.readouterr()
|
||||
|
||||
# regex to match timestamp in this format: 2023-01-25 06:40:18.216297
|
||||
assert re.match(r"\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{6}", captured.out.strip())
|
||||
assert captured.out.strip().endswith("test")
|
||||
|
||||
verbose("test2", level=1)
|
||||
captured = capsys.readouterr()
|
||||
assert re.match(r"\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{6}", captured.out.strip())
|
||||
assert captured.out.strip().endswith("test2")
|
||||
|
||||
verbose("test3", level=2)
|
||||
captured = capsys.readouterr()
|
||||
assert captured.out.strip() == ""
|
||||
|
||||
|
||||
def test_verbose_print_file():
|
||||
"""Test verbose with file"""
|
||||
set_verbose_level(1)
|
||||
stream = StringIO()
|
||||
verbose = verbose_print(1, False, False, file=stream)
|
||||
verbose("test")
|
||||
assert stream.getvalue().strip() == "test"
|
||||
|
||||
|
||||
def test_verbose_print_noop(capsys):
|
||||
"""Test verbose with noop"""
|
||||
set_verbose_level(1)
|
||||
verbose = verbose_print(0, False, False)
|
||||
verbose("test")
|
||||
captured = capsys.readouterr()
|
||||
assert captured.out.strip() == ""
|
||||
|
||||
|
||||
def test_verbose(capsys):
|
||||
""" "Test verbose()"""
|
||||
# reset verbose module globals for testing
|
||||
_reset_verbose_globals()
|
||||
set_verbose_level(0)
|
||||
verbose("test")
|
||||
captured = capsys.readouterr()
|
||||
assert captured.out.strip() == ""
|
||||
|
||||
set_verbose_level(1)
|
||||
verbose("test")
|
||||
captured = capsys.readouterr()
|
||||
assert captured.out.strip() == "test"
|
||||
|
||||
verbose("test2", level=2)
|
||||
captured = capsys.readouterr()
|
||||
assert captured.out.strip() == ""
|
||||
|
||||
set_verbose_level(2)
|
||||
verbose("test2", level=2)
|
||||
captured = capsys.readouterr()
|
||||
assert captured.out.strip() == "test2"
|
||||
@ -7,14 +7,29 @@ from osxphotos.debug import is_debug, set_debug
|
||||
|
||||
|
||||
def test_debug_enable():
|
||||
"""test set_debug()"""
|
||||
set_debug(True)
|
||||
logger = osxphotos._get_logger()
|
||||
assert logger.isEnabledFor(logging.DEBUG)
|
||||
assert osxphotos.logger.isEnabledFor(logging.DEBUG)
|
||||
assert is_debug()
|
||||
|
||||
|
||||
def test_debug_disable():
|
||||
"""test set_debug()"""
|
||||
set_debug(False)
|
||||
logger = osxphotos._get_logger()
|
||||
assert not logger.isEnabledFor(logging.DEBUG)
|
||||
assert not osxphotos.logger.isEnabledFor(logging.DEBUG)
|
||||
assert not is_debug()
|
||||
|
||||
|
||||
def test_debug_print_true(caplog):
|
||||
"""test debug()"""
|
||||
set_debug(True)
|
||||
logger = osxphotos.logger
|
||||
logger.debug("test debug")
|
||||
assert "test debug" in caplog.text
|
||||
|
||||
|
||||
def test_debug_print_false(caplog):
|
||||
set_debug(False)
|
||||
logger = osxphotos.logger
|
||||
logger.debug("test debug")
|
||||
assert caplog.text == ""
|
||||
|
||||
@ -2,9 +2,9 @@ import os
|
||||
import pytest
|
||||
|
||||
from osxphotos._constants import _UNKNOWN_PERSON
|
||||
from osxphotos.utils import _get_os_version
|
||||
from osxphotos.utils import get_macos_version
|
||||
|
||||
OS_VERSION = _get_os_version()
|
||||
OS_VERSION = get_macos_version()
|
||||
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(
|
||||
|
||||
@ -14,9 +14,9 @@ import pytest
|
||||
import osxphotos
|
||||
from osxphotos._constants import _UNKNOWN_PERSON
|
||||
from osxphotos.photoexporter import PhotoExporter
|
||||
from osxphotos.utils import _get_os_version
|
||||
from osxphotos.utils import get_macos_version
|
||||
|
||||
OS_VERSION = _get_os_version()
|
||||
OS_VERSION = get_macos_version()
|
||||
# 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")
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user