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:
@@ -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)
|
||||
query_options = query_options_from_kwargs(**kwargs)
|
||||
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
|
||||
@@ -1401,7 +1394,7 @@ def export(
|
||||
no_title=no_title,
|
||||
not_burst=not_burst,
|
||||
not_cloudasset=not_cloudasset,
|
||||
not_edited = not_edited,
|
||||
not_edited=not_edited,
|
||||
not_favorite=not_favorite,
|
||||
not_hdr=not_hdr,
|
||||
not_hidden=not_hidden,
|
||||
@@ -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:
|
||||
query_options = query_options_from_kwargs(**kwargs)
|
||||
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,10 +351,9 @@ 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(
|
||||
f"_dbfile = {self._dbfile}, _dbfile_actual = {self._dbfile_actual}"
|
||||
)
|
||||
logger.debug(
|
||||
f"_dbfile = {self._dbfile}, _dbfile_actual = {self._dbfile_actual}"
|
||||
)
|
||||
|
||||
library_path = os.path.dirname(os.path.abspath(dbfile))
|
||||
(library_path, _) = os.path.split(library_path) # drop /database from path
|
||||
@@ -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,21 +1320,19 @@ class PhotosDB:
|
||||
if resource_type == 4:
|
||||
# photo
|
||||
if "edit_resource_id_photo" in self._dbphotos[uuid]:
|
||||
if is_debug():
|
||||
logging.debug(
|
||||
f"WARNING: found more than one edit_resource_id_photo for "
|
||||
f"UUID {row[0]},adjustmentUUID {row[1]}, modelID {row[2]}"
|
||||
)
|
||||
logger.debug(
|
||||
f"WARNING: found more than one edit_resource_id_photo for "
|
||||
f"UUID {row[0]},adjustmentUUID {row[1]}, modelID {row[2]}"
|
||||
)
|
||||
self._dbphotos[uuid]["edit_resource_id_photo"] = row[2]
|
||||
self._dbphotos[uuid]["UTI_edited_photo"] = row[4]
|
||||
elif resource_type == 8:
|
||||
# video
|
||||
if "edit_resource_id_video" in self._dbphotos[uuid]:
|
||||
if is_debug():
|
||||
logging.debug(
|
||||
f"WARNING: found more than one edit_resource_id_video for "
|
||||
f"UUID {row[0]},adjustmentUUID {row[1]}, modelID {row[2]}"
|
||||
)
|
||||
logger.debug(
|
||||
f"WARNING: found more than one edit_resource_id_video for "
|
||||
f"UUID {row[0]},adjustmentUUID {row[1]}, modelID {row[2]}"
|
||||
)
|
||||
self._dbphotos[uuid]["edit_resource_id_video"] = row[2]
|
||||
self._dbphotos[uuid]["UTI_edited_video"] = row[4]
|
||||
|
||||
@@ -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,10 +2263,9 @@ class PhotosDB:
|
||||
if uuid in self._dbphotos:
|
||||
self._dbphotos[uuid]["extendedDescription"] = normalize_unicode(row[1])
|
||||
else:
|
||||
if is_debug():
|
||||
logging.debug(
|
||||
f"WARNING: found description {row[1]} but no photo for {uuid}"
|
||||
)
|
||||
logger.debug(
|
||||
f"WARNING: found description {row[1]} but no photo for {uuid}"
|
||||
)
|
||||
|
||||
# get information about adjusted/edited photos
|
||||
c.execute(
|
||||
@@ -2319,10 +2281,9 @@ class PhotosDB:
|
||||
if uuid in self._dbphotos:
|
||||
self._dbphotos[uuid]["adjustmentFormatID"] = row[2]
|
||||
else:
|
||||
if is_debug():
|
||||
logging.debug(
|
||||
f"WARNING: found adjustmentformatidentifier {row[2]} but no photo for uuid {row[0]}"
|
||||
)
|
||||
logger.debug(
|
||||
f"WARNING: found adjustmentformatidentifier {row[2]} but no photo for uuid {row[0]}"
|
||||
)
|
||||
|
||||
# Find missing photos
|
||||
# TODO: this code is very kludgy and I had to make lots of assumptions
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user