Feature add query command (#970)
* Added query_command and example * Refactored QUERY_OPTIONS, added query_command, refactored verbose, #930, #931 * Added query options to debug-dump, #966 * Refactored query, #602 * Added precedence test for --load-config * Refactored handling of query options * Refactored export_photo * Removed extraneous print * Updated API_README * Updated examples
This commit is contained in:
@@ -25,13 +25,12 @@ from .queryoptions import QueryOptions
|
||||
from .scoreinfo import ScoreInfo
|
||||
from .searchinfo import SearchInfo
|
||||
|
||||
# configure logging; every module in osxphotos should use this 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)
|
||||
|
||||
|
||||
@@ -47,17 +47,30 @@ 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
|
||||
from .cli_commands import (
|
||||
abort,
|
||||
echo,
|
||||
echo_error,
|
||||
logger,
|
||||
query_command,
|
||||
selection_command,
|
||||
verbose,
|
||||
)
|
||||
from .cli_params import DB_OPTION, DEBUG_OPTIONS, JSON_OPTION
|
||||
from .common import OSXPHOTOS_HIDDEN, get_photos_db
|
||||
from .debug_dump import debug_dump
|
||||
from .docs import docs_command
|
||||
from .dump import dump
|
||||
from .exiftool_cli import exiftool
|
||||
from .export import export
|
||||
from .exportdb import exportdb
|
||||
from .grep import grep
|
||||
from .help import help
|
||||
from .import_cli import import_cli
|
||||
from .info import info
|
||||
from .install_uninstall_run import install, run, uninstall
|
||||
from .keywords import keywords
|
||||
from .kvstore import kvstore
|
||||
from .labels import labels
|
||||
from .list import _list_libraries, list_libraries
|
||||
from .orphans import orphans
|
||||
@@ -67,39 +80,53 @@ from .places import places
|
||||
from .query import query
|
||||
from .repl import repl
|
||||
from .snap_diff import diff, snap
|
||||
from .sync import sync
|
||||
from .theme import theme
|
||||
from .timewarp import timewarp
|
||||
from .tutorial import tutorial
|
||||
from .uuid import uuid
|
||||
from .version import version
|
||||
|
||||
install_traceback()
|
||||
|
||||
__all__ = [
|
||||
"abort",
|
||||
"about",
|
||||
"add_locations",
|
||||
"albums",
|
||||
"cli_main",
|
||||
"debug_dump",
|
||||
"diff",
|
||||
"docs_command",
|
||||
"dump",
|
||||
"echo",
|
||||
"echo_error",
|
||||
"exiftool_cli",
|
||||
"export",
|
||||
"exportdb",
|
||||
"grep",
|
||||
"help",
|
||||
"import_cli",
|
||||
"info",
|
||||
"install",
|
||||
"keywords",
|
||||
"kvstore",
|
||||
"labels",
|
||||
"list_libraries",
|
||||
"list_libraries",
|
||||
"logger",
|
||||
"orphans",
|
||||
"persons",
|
||||
"photo_inspect",
|
||||
"places",
|
||||
"query",
|
||||
"query_command",
|
||||
"repl",
|
||||
"run",
|
||||
"selection_command",
|
||||
"set_debug",
|
||||
"snap",
|
||||
"tutorial",
|
||||
"uuid",
|
||||
"verbose",
|
||||
]
|
||||
|
||||
@@ -11,9 +11,9 @@ import osxphotos
|
||||
from osxphotos.queryoptions import IncompatibleQueryOptions, query_options_from_kwargs
|
||||
from osxphotos.utils import pluralize
|
||||
|
||||
from .cli_params import QUERY_OPTIONS, THEME_OPTION, TIMESTAMP_OPTION, VERBOSE_OPTION
|
||||
from .click_rich_echo import rich_click_echo as echo
|
||||
from .click_rich_echo import rich_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
|
||||
|
||||
@@ -6,12 +6,12 @@ import click
|
||||
import yaml
|
||||
|
||||
import osxphotos
|
||||
|
||||
from .common import DB_ARGUMENT, DB_OPTION, JSON_OPTION, get_photos_db
|
||||
from .list import _list_libraries
|
||||
|
||||
from osxphotos._constants import _PHOTOS_4_VERSION
|
||||
|
||||
from .cli_params import DB_ARGUMENT, DB_OPTION, JSON_OPTION
|
||||
from .common import get_photos_db
|
||||
from .list import _list_libraries
|
||||
|
||||
|
||||
@click.command()
|
||||
@DB_OPTION
|
||||
@@ -36,7 +36,8 @@ def albums(ctx, cli_obj, db, json_, photos_library):
|
||||
if photosdb.db_version > _PHOTOS_4_VERSION:
|
||||
albums["shared albums"] = photosdb.albums_shared_as_dict
|
||||
|
||||
if json_ or cli_obj.json:
|
||||
# cli_obj will be None if called from pytest
|
||||
if json_ or (cli_obj and cli_obj.json):
|
||||
click.echo(json.dumps(albums, ensure_ascii=False))
|
||||
else:
|
||||
click.echo(yaml.dump(albums, sort_keys=False, allow_unicode=True))
|
||||
|
||||
@@ -13,9 +13,10 @@ from osxphotos._version import __version__
|
||||
from .about import about
|
||||
from .add_locations import add_locations
|
||||
from .albums import albums
|
||||
from .common import DB_OPTION, JSON_OPTION, OSXPHOTOS_HIDDEN
|
||||
from .cli_params import DB_OPTION, DEBUG_OPTIONS, JSON_OPTION
|
||||
from .common import OSXPHOTOS_HIDDEN
|
||||
from .debug_dump import debug_dump
|
||||
from .docs import docs
|
||||
from .docs import docs_command
|
||||
from .dump import dump
|
||||
from .exiftool_cli import exiftool
|
||||
from .export import export
|
||||
@@ -41,7 +42,6 @@ from .timewarp import timewarp
|
||||
from .tutorial import tutorial
|
||||
from .uuid import uuid
|
||||
from .version import version
|
||||
from .common import DEBUG_OPTIONS
|
||||
|
||||
|
||||
# Click CLI object & context settings
|
||||
@@ -110,7 +110,7 @@ for command in [
|
||||
albums,
|
||||
debug_dump,
|
||||
diff,
|
||||
docs,
|
||||
docs_command,
|
||||
dump,
|
||||
exiftool,
|
||||
export,
|
||||
|
||||
305
osxphotos/cli/cli_commands.py
Normal file
305
osxphotos/cli/cli_commands.py
Normal file
@@ -0,0 +1,305 @@
|
||||
"""Helper functions to make writing an osxphotos CLI tool easy.
|
||||
|
||||
Includes decorator to create an osxphotos query command to be run via `osxphotos run example.py`.
|
||||
|
||||
May also be run via `python example.py` if you have pip installed osxphotos
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import sys
|
||||
import typing as t # match style used in Click source code
|
||||
|
||||
import click
|
||||
|
||||
from osxphotos.photosdb import PhotosDB
|
||||
from osxphotos.queryoptions import QueryOptions, query_options_from_kwargs
|
||||
from osxphotos.sqlitekvstore import SQLiteKVStore
|
||||
|
||||
from .cli_params import (
|
||||
_DB_PARAMETER,
|
||||
_QUERY_PARAMETERS_DICT,
|
||||
DB_OPTION,
|
||||
THEME_OPTION,
|
||||
TIMESTAMP_OPTION,
|
||||
VERBOSE_OPTION,
|
||||
)
|
||||
from .click_rich_echo import rich_click_echo as echo
|
||||
from .click_rich_echo import rich_echo_error as echo_error
|
||||
from .verbose import verbose, verbose_print
|
||||
|
||||
logger = logging.getLogger("osxphotos")
|
||||
|
||||
__all__ = [
|
||||
"abort",
|
||||
"echo",
|
||||
"echo_error",
|
||||
"logger",
|
||||
"query_command",
|
||||
"selection_command",
|
||||
"verbose",
|
||||
]
|
||||
|
||||
|
||||
def abort(message: str, exit_code: int = 1):
|
||||
"""Abort with error message and exit code"""
|
||||
echo_error(f"[error]{message}[/]")
|
||||
sys.exit(exit_code)
|
||||
|
||||
|
||||
def config_verbose_callback(ctx: click.Context, param: click.Parameter, value: t.Any):
|
||||
"""Callback for --verbose option"""
|
||||
# calling verbose_print() will set the verbose level for the verbose() function
|
||||
theme = ctx.params.get("theme")
|
||||
timestamp = ctx.params.get("timestamp")
|
||||
verbose_print(verbose=value, timestamp=timestamp, theme=theme)
|
||||
return value
|
||||
|
||||
|
||||
def get_photos_for_query(ctx: click.Context):
|
||||
"""Return list of PhotoInfo objects for the photos matching the query options in ctx.params"""
|
||||
db = ctx.params.get("db")
|
||||
photosdb = PhotosDB(dbfile=db, verbose=verbose)
|
||||
options = query_options_from_kwargs(**ctx.params)
|
||||
return photosdb.query(options=options)
|
||||
|
||||
|
||||
def get_selected_photos(ctx: click.Context):
|
||||
"""Return list of PhotoInfo objects for the photos currently selected in Photos.app"""
|
||||
photosdb = PhotosDB(verbose=verbose)
|
||||
return photosdb.query(options=QueryOptions(selected=True))
|
||||
|
||||
|
||||
class QueryCommand(click.Command):
|
||||
"""
|
||||
Click command to create an osxphotos query command.
|
||||
|
||||
This class is used by the query_command decorator to create a click command
|
||||
that runs an osxphotos query. It will automatically add the query options as
|
||||
well as the --verbose, --timestamp, --theme, and --db options.
|
||||
"""
|
||||
|
||||
standalone_mode = False
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: t.Optional[str],
|
||||
context_settings: t.Optional[t.Dict[str, t.Any]] = None,
|
||||
callback: t.Optional[t.Callable[..., t.Any]] = None,
|
||||
params: t.Optional[t.List[click.Parameter]] = None,
|
||||
help: t.Optional[str] = None,
|
||||
epilog: t.Optional[str] = None,
|
||||
short_help: t.Optional[str] = None,
|
||||
options_metavar: t.Optional[str] = "[OPTIONS]",
|
||||
add_help_option: bool = True,
|
||||
no_args_is_help: bool = False,
|
||||
hidden: bool = False,
|
||||
deprecated: bool = False,
|
||||
) -> None:
|
||||
self.params = params or []
|
||||
self.params.append(
|
||||
click.Option(
|
||||
param_decls=["--verbose", "-V", "verbose_flag"],
|
||||
count=True,
|
||||
help="Print verbose output; may be specified multiple times for more verbose output.",
|
||||
callback=config_verbose_callback,
|
||||
)
|
||||
)
|
||||
self.params.append(
|
||||
click.Option(
|
||||
param_decls=["--timestamp"],
|
||||
is_flag=True,
|
||||
help="Add time stamp to verbose output",
|
||||
)
|
||||
)
|
||||
self.params.append(
|
||||
click.Option(
|
||||
param_decls=["--theme"],
|
||||
metavar="THEME",
|
||||
type=click.Choice(
|
||||
["dark", "light", "mono", "plain"], case_sensitive=False
|
||||
),
|
||||
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.",
|
||||
)
|
||||
)
|
||||
self.params.append(_DB_PARAMETER)
|
||||
self.params.extend(_QUERY_PARAMETERS_DICT.values())
|
||||
|
||||
super().__init__(
|
||||
name,
|
||||
context_settings,
|
||||
callback,
|
||||
self.params,
|
||||
help,
|
||||
epilog,
|
||||
short_help,
|
||||
options_metavar,
|
||||
add_help_option,
|
||||
no_args_is_help,
|
||||
hidden,
|
||||
deprecated,
|
||||
)
|
||||
|
||||
def make_context(
|
||||
self,
|
||||
info_name: t.Optional[str],
|
||||
args: t.List[str],
|
||||
parent: t.Optional[click.Context] = None,
|
||||
**extra: t.Any,
|
||||
) -> click.Context:
|
||||
ctx = super().make_context(info_name, args, parent, **extra)
|
||||
ctx.obj = self
|
||||
photos = get_photos_for_query(ctx)
|
||||
ctx.params["photos"] = photos
|
||||
|
||||
# remove params handled by this class
|
||||
ctx.params.pop("verbose_flag")
|
||||
ctx.params.pop("timestamp")
|
||||
ctx.params.pop("theme")
|
||||
return ctx
|
||||
|
||||
|
||||
class SelectionCommand(click.Command):
|
||||
"""
|
||||
Click command to create an osxphotos selection command that runs on selected photos.
|
||||
|
||||
This class is used by the query_command decorator to create a click command
|
||||
that runs on currently selected photos.
|
||||
|
||||
The --verbose, --timestamp, --theme, and --db options will also be added to the command.
|
||||
"""
|
||||
|
||||
standalone_mode = False
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: t.Optional[str],
|
||||
context_settings: t.Optional[t.Dict[str, t.Any]] = None,
|
||||
callback: t.Optional[t.Callable[..., t.Any]] = None,
|
||||
params: t.Optional[t.List[click.Parameter]] = None,
|
||||
help: t.Optional[str] = None,
|
||||
epilog: t.Optional[str] = None,
|
||||
short_help: t.Optional[str] = None,
|
||||
options_metavar: t.Optional[str] = "[OPTIONS]",
|
||||
add_help_option: bool = True,
|
||||
no_args_is_help: bool = False,
|
||||
hidden: bool = False,
|
||||
deprecated: bool = False,
|
||||
) -> None:
|
||||
self.params = params or []
|
||||
self.params.append(
|
||||
click.Option(
|
||||
param_decls=["--verbose", "-V", "verbose_flag"],
|
||||
count=True,
|
||||
help="Print verbose output; may be specified multiple times for more verbose output.",
|
||||
callback=config_verbose_callback,
|
||||
)
|
||||
)
|
||||
self.params.append(
|
||||
click.Option(
|
||||
param_decls=["--timestamp"],
|
||||
is_flag=True,
|
||||
help="Add time stamp to verbose output",
|
||||
)
|
||||
)
|
||||
self.params.append(
|
||||
click.Option(
|
||||
param_decls=["--theme"],
|
||||
metavar="THEME",
|
||||
type=click.Choice(
|
||||
["dark", "light", "mono", "plain"], case_sensitive=False
|
||||
),
|
||||
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.",
|
||||
)
|
||||
)
|
||||
self.params.append(
|
||||
click.Option(
|
||||
param_decls=["--library", "--db"],
|
||||
required=False,
|
||||
metavar="PHOTOS_LIBRARY_PATH",
|
||||
default=None,
|
||||
help=(
|
||||
"Specify Photos database path. "
|
||||
"Path to Photos library/database can be specified using either --db "
|
||||
"or directly as PHOTOS_LIBRARY positional argument. "
|
||||
"If neither --db or PHOTOS_LIBRARY provided, will attempt to find the library "
|
||||
"to use in the following order: 1. last opened library, 2. system library, 3. ~/Pictures/Photos Library.photoslibrary"
|
||||
),
|
||||
type=click.Path(exists=True),
|
||||
)
|
||||
)
|
||||
super().__init__(
|
||||
name,
|
||||
context_settings,
|
||||
callback,
|
||||
self.params,
|
||||
help,
|
||||
epilog,
|
||||
short_help,
|
||||
options_metavar,
|
||||
add_help_option,
|
||||
no_args_is_help,
|
||||
hidden,
|
||||
deprecated,
|
||||
)
|
||||
|
||||
def make_context(
|
||||
self,
|
||||
info_name: t.Optional[str],
|
||||
args: t.List[str],
|
||||
parent: t.Optional[click.Context] = None,
|
||||
**extra: t.Any,
|
||||
) -> click.Context:
|
||||
ctx = super().make_context(info_name, args, parent, **extra)
|
||||
ctx.obj = self
|
||||
photos = get_selected_photos(ctx)
|
||||
ctx.params["photos"] = photos
|
||||
|
||||
# remove params handled by this class
|
||||
ctx.params.pop("verbose_flag")
|
||||
ctx.params.pop("timestamp")
|
||||
ctx.params.pop("theme")
|
||||
return ctx
|
||||
|
||||
|
||||
def query_command(name=None, cls=QueryCommand, **attrs):
|
||||
"""Decorator to create an osxphotos command to be run via `osxphotos run example.py`
|
||||
|
||||
The command will be passed a list of PhotoInfo objects for all photos in Photos
|
||||
matching the query options or all photos if no query options are specified.
|
||||
|
||||
The standard osxphotos query options will be added to the command.
|
||||
|
||||
The CLI will also be passed the following options:
|
||||
|
||||
--verbose
|
||||
--timestamp
|
||||
--theme
|
||||
--db
|
||||
"""
|
||||
if callable(name) and cls:
|
||||
return click.command(cls=cls, **attrs)(name)
|
||||
|
||||
return click.command(name, cls=cls, **attrs)
|
||||
|
||||
|
||||
def selection_command(name=None, cls=SelectionCommand, **attrs):
|
||||
"""Decorator to create an osxphotos command to be run via `osxphotos run example.py`
|
||||
|
||||
The command will be passed a list of PhotoInfo objects for all photos selected in Photos.
|
||||
The CLI will also be passed the following options:
|
||||
|
||||
--verbose
|
||||
--timestamp
|
||||
--theme
|
||||
--db
|
||||
"""
|
||||
if callable(name) and cls:
|
||||
return click.command(cls=cls, **attrs)(name)
|
||||
|
||||
return click.command(name, cls=cls, **attrs)
|
||||
677
osxphotos/cli/cli_params.py
Normal file
677
osxphotos/cli/cli_params.py
Normal file
@@ -0,0 +1,677 @@
|
||||
"""Common options & parameters for osxphotos CLI commands"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import functools
|
||||
from typing import Any, Callable
|
||||
|
||||
import click
|
||||
|
||||
from .common import OSXPHOTOS_HIDDEN
|
||||
from .param_types import *
|
||||
|
||||
__all__ = [
|
||||
"DB_ARGUMENT",
|
||||
"DB_OPTION",
|
||||
"DEBUG_OPTIONS",
|
||||
"DELETED_OPTIONS",
|
||||
"FIELD_OPTION",
|
||||
"JSON_OPTION",
|
||||
"QUERY_OPTIONS",
|
||||
"THEME_OPTION",
|
||||
"VERBOSE_OPTION",
|
||||
"TIMESTAMP_OPTION",
|
||||
]
|
||||
|
||||
|
||||
def _param_memo(f: Callable[..., Any], param: click.Parameter) -> None:
|
||||
"""Add param to the list of params for a click.Command
|
||||
This is directly from the click source code and
|
||||
the implementation is thus tightly coupled to click internals
|
||||
"""
|
||||
if isinstance(f, click.Command):
|
||||
f.params.append(param)
|
||||
else:
|
||||
if not hasattr(f, "__click_params__"):
|
||||
f.__click_params__ = [] # type: ignore
|
||||
|
||||
f.__click_params__.append(param) # type: ignore
|
||||
|
||||
|
||||
def make_click_option_decorator(*params: click.Parameter) -> Callable[..., Any]:
|
||||
"""Make a decorator for a click option from one or more click Parameter objects"""
|
||||
|
||||
def decorator(wrapped=None) -> Callable[..., Any]:
|
||||
"""Function decorator to add option to a click command.
|
||||
|
||||
Args:
|
||||
wrapped: function to decorate (this is normally passed automatically
|
||||
"""
|
||||
|
||||
if wrapped is None:
|
||||
return decorator
|
||||
|
||||
def _add_options(wrapped):
|
||||
"""Add query options to wrapped function"""
|
||||
for param in params:
|
||||
_param_memo(wrapped, param)
|
||||
return wrapped
|
||||
|
||||
return _add_options(wrapped)
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
VERSION_CHECK_OPTION = click.option("--no-version-check", required=False, is_flag=True)
|
||||
|
||||
_DB_PARAMETER = click.Option(
|
||||
["--library", "--db", "db"],
|
||||
required=False,
|
||||
metavar="PHOTOS_LIBRARY_PATH",
|
||||
default=None,
|
||||
help=(
|
||||
"Specify path to Photos library. "
|
||||
"If not provided, will attempt to find the library to use in the following order: "
|
||||
"1. last opened library, 2. system library, 3. ~/Pictures/Photos Library.photoslibrary"
|
||||
),
|
||||
type=click.Path(exists=True),
|
||||
)
|
||||
|
||||
DB_OPTION = make_click_option_decorator(_DB_PARAMETER)
|
||||
|
||||
DB_ARGUMENT = click.argument(
|
||||
"photos_library",
|
||||
nargs=-1,
|
||||
type=DeprecatedPath(
|
||||
exists=True,
|
||||
deprecation_warning="The PHOTOS_LIBRARY argument is deprecated and "
|
||||
"will be removed in a future version of osxphotos. "
|
||||
"Use --library instead to specify the Photos Library path.",
|
||||
),
|
||||
)
|
||||
|
||||
_JSON_PARAMETER = click.Option(
|
||||
["--json", "json_"],
|
||||
required=False,
|
||||
is_flag=True,
|
||||
default=False,
|
||||
help="Print output in JSON format.",
|
||||
)
|
||||
|
||||
JSON_OPTION = make_click_option_decorator(_JSON_PARAMETER)
|
||||
|
||||
_FIELD_PARAMETER = click.Option(
|
||||
["--field", "-f"],
|
||||
metavar="FIELD TEMPLATE",
|
||||
multiple=True,
|
||||
nargs=2,
|
||||
help="Output only specified custom fields. "
|
||||
"FIELD is the name of the field and TEMPLATE is the template to use as the field value. "
|
||||
"May be repeated to output multiple fields. "
|
||||
"For example, to output photo uuid, name, and title: "
|
||||
'`--field uuid "{uuid}" --field name "{original_name}" --field title "{title}"`.',
|
||||
)
|
||||
|
||||
FIELD_OPTION = make_click_option_decorator(_FIELD_PARAMETER)
|
||||
|
||||
_DELETED_PARAMETERS = [
|
||||
click.Option(
|
||||
["--deleted"],
|
||||
is_flag=True,
|
||||
help="Include photos from the 'Recently Deleted' folder.",
|
||||
),
|
||||
click.Option(
|
||||
["--deleted-only"],
|
||||
is_flag=True,
|
||||
help="Include only photos from the 'Recently Deleted' folder.",
|
||||
),
|
||||
]
|
||||
|
||||
DELETED_OPTIONS = make_click_option_decorator(*_DELETED_PARAMETERS)
|
||||
|
||||
# The following are used by the query command and by
|
||||
# QUERY_OPTIONS to add the query options to other commands
|
||||
# To add new query options, add them to _QUERY_OPTIONS as
|
||||
# a click.Option, add them to osxphotos/photosdb/photosdb.py::PhotosDB.query(),
|
||||
# and to osxphotos/query_options.py::QueryOptions
|
||||
_QUERY_PARAMETERS_DICT = {
|
||||
"--keyword": click.Option(
|
||||
["--keyword"],
|
||||
metavar="KEYWORD",
|
||||
default=None,
|
||||
multiple=True,
|
||||
help="Search for photos with keyword KEYWORD. "
|
||||
'If more than one keyword, treated as "OR", e.g. find photos matching any keyword',
|
||||
),
|
||||
"--no-keyword": click.Option(
|
||||
["--no-keyword"],
|
||||
is_flag=True,
|
||||
help="Search for photos with no keyword.",
|
||||
),
|
||||
"--person": click.Option(
|
||||
["--person"],
|
||||
metavar="PERSON",
|
||||
default=None,
|
||||
multiple=True,
|
||||
help="Search for photos with person PERSON. "
|
||||
'If more than one person, treated as "OR", e.g. find photos matching any person',
|
||||
),
|
||||
"--album": click.Option(
|
||||
["--album"],
|
||||
metavar="ALBUM",
|
||||
default=None,
|
||||
multiple=True,
|
||||
help="Search for photos in album ALBUM. "
|
||||
'If more than one album, treated as "OR", e.g. find photos matching any album',
|
||||
),
|
||||
"--folder": click.Option(
|
||||
["--folder"],
|
||||
metavar="FOLDER",
|
||||
default=None,
|
||||
multiple=True,
|
||||
help="Search for photos in an album in folder FOLDER. "
|
||||
'If more than one folder, treated as "OR", e.g. find photos in any FOLDER. '
|
||||
"Only searches top level folders (e.g. does not look at subfolders)",
|
||||
),
|
||||
"--name": click.Option(
|
||||
["--name"],
|
||||
metavar="FILENAME",
|
||||
default=None,
|
||||
multiple=True,
|
||||
help="Search for photos with filename matching FILENAME. "
|
||||
'If more than one --name options is specified, they are treated as "OR", '
|
||||
"e.g. find photos matching any FILENAME. ",
|
||||
),
|
||||
"--uuid": click.Option(
|
||||
["--uuid"],
|
||||
metavar="UUID",
|
||||
default=None,
|
||||
multiple=True,
|
||||
help="Search for photos with UUID(s). "
|
||||
"May be repeated to include multiple UUIDs.",
|
||||
),
|
||||
"--uuid-from-file": click.Option(
|
||||
["--uuid-from-file"],
|
||||
metavar="FILE",
|
||||
default=None,
|
||||
multiple=False,
|
||||
help="Search for photos with UUID(s) loaded from FILE. "
|
||||
"Format is a single UUID per line. Lines preceded with # are ignored.",
|
||||
type=click.Path(exists=True),
|
||||
),
|
||||
"--title": click.Option(
|
||||
["--title"],
|
||||
metavar="TITLE",
|
||||
default=None,
|
||||
multiple=True,
|
||||
help="Search for TITLE in title of photo.",
|
||||
),
|
||||
"--no-title": click.Option(
|
||||
["--no-title"], is_flag=True, help="Search for photos with no title."
|
||||
),
|
||||
"--description": click.Option(
|
||||
["--description"],
|
||||
metavar="DESC",
|
||||
default=None,
|
||||
multiple=True,
|
||||
help="Search for DESC in description of photo.",
|
||||
),
|
||||
"--no-description": click.Option(
|
||||
["--no-description"],
|
||||
is_flag=True,
|
||||
help="Search for photos with no description.",
|
||||
),
|
||||
"--place": click.Option(
|
||||
["--place"],
|
||||
metavar="PLACE",
|
||||
default=None,
|
||||
multiple=True,
|
||||
help="Search for PLACE in photo's reverse geolocation info",
|
||||
),
|
||||
"--no-place": click.Option(
|
||||
["--no-place"],
|
||||
is_flag=True,
|
||||
help="Search for photos with no associated place name info (no reverse geolocation info)",
|
||||
),
|
||||
"--location": click.Option(
|
||||
["--location"],
|
||||
is_flag=True,
|
||||
help="Search for photos with associated location info (e.g. GPS coordinates)",
|
||||
),
|
||||
"--no-location": click.Option(
|
||||
["--no-location"],
|
||||
is_flag=True,
|
||||
help="Search for photos with no associated location info (e.g. no GPS coordinates)",
|
||||
),
|
||||
"--label": click.Option(
|
||||
["--label"],
|
||||
metavar="LABEL",
|
||||
multiple=True,
|
||||
help="Search for photos with image classification label LABEL (Photos 5+ only). "
|
||||
'If more than one label, treated as "OR", e.g. find photos matching any label',
|
||||
),
|
||||
"--uti": click.Option(
|
||||
["--uti"],
|
||||
metavar="UTI",
|
||||
default=None,
|
||||
multiple=False,
|
||||
help="Search for photos whose uniform type identifier (UTI) matches UTI",
|
||||
),
|
||||
"--ignore_case": click.Option(
|
||||
["-i", "--ignore-case"],
|
||||
is_flag=True,
|
||||
help="Case insensitive search for title, description, place, keyword, person, or album.",
|
||||
),
|
||||
"--edited": click.Option(
|
||||
["--edited"],
|
||||
is_flag=True,
|
||||
help="Search for photos that have been edited.",
|
||||
),
|
||||
"--not-edited": click.Option(
|
||||
["--not-edited"],
|
||||
is_flag=True,
|
||||
help="Search for photos that have not been edited.",
|
||||
),
|
||||
"--external-edit": click.Option(
|
||||
["--external-edit"],
|
||||
is_flag=True,
|
||||
help="Search for photos edited in external editor.",
|
||||
),
|
||||
"--favorite": click.Option(
|
||||
["--favorite"], is_flag=True, help="Search for photos marked favorite."
|
||||
),
|
||||
"--not-favorite": click.Option(
|
||||
["--not-favorite"],
|
||||
is_flag=True,
|
||||
help="Search for photos not marked favorite.",
|
||||
),
|
||||
"--hidden": click.Option(
|
||||
["--hidden"], is_flag=True, help="Search for photos marked hidden."
|
||||
),
|
||||
"--not-hidden": click.Option(
|
||||
["--not-hidden"],
|
||||
is_flag=True,
|
||||
help="Search for photos not marked hidden.",
|
||||
),
|
||||
"--shared": click.Option(
|
||||
["--shared"],
|
||||
is_flag=True,
|
||||
help="Search for photos in shared iCloud album (Photos 5+ only).",
|
||||
),
|
||||
"--not-shared": click.Option(
|
||||
["--not-shared"],
|
||||
is_flag=True,
|
||||
help="Search for photos not in shared iCloud album (Photos 5+ only).",
|
||||
),
|
||||
"--burst": click.Option(
|
||||
["--burst"],
|
||||
is_flag=True,
|
||||
help="Search for photos that were taken in a burst.",
|
||||
),
|
||||
"--not-burst": click.Option(
|
||||
["--not-burst"],
|
||||
is_flag=True,
|
||||
help="Search for photos that are not part of a burst.",
|
||||
),
|
||||
"--live": click.Option(
|
||||
["--live"], is_flag=True, help="Search for Apple live photos"
|
||||
),
|
||||
"--not-live": click.Option(
|
||||
["--not-live"],
|
||||
is_flag=True,
|
||||
help="Search for photos that are not Apple live photos.",
|
||||
),
|
||||
"--portrait": click.Option(
|
||||
["--portrait"],
|
||||
is_flag=True,
|
||||
help="Search for Apple portrait mode photos.",
|
||||
),
|
||||
"--not-portrait": click.Option(
|
||||
["--not-portrait"],
|
||||
is_flag=True,
|
||||
help="Search for photos that are not Apple portrait mode photos.",
|
||||
),
|
||||
"--screenshot": click.Option(
|
||||
["--screenshot"], is_flag=True, help="Search for screenshot photos."
|
||||
),
|
||||
"--not-screenshot": click.Option(
|
||||
["--not-screenshot"],
|
||||
is_flag=True,
|
||||
help="Search for photos that are not screenshot photos.",
|
||||
),
|
||||
"--slow-mo": click.Option(
|
||||
["--slow-mo"], is_flag=True, help="Search for slow motion videos."
|
||||
),
|
||||
"--not-slow-mo": click.Option(
|
||||
["--not-slow-mo"],
|
||||
is_flag=True,
|
||||
help="Search for photos that are not slow motion videos.",
|
||||
),
|
||||
"--time-lapse": click.Option(
|
||||
["--time-lapse"], is_flag=True, help="Search for time lapse videos."
|
||||
),
|
||||
"--not-time-lapse": click.Option(
|
||||
["--not-time-lapse"],
|
||||
is_flag=True,
|
||||
help="Search for photos that are not time lapse videos.",
|
||||
),
|
||||
"--hdr": click.Option(
|
||||
["--hdr"],
|
||||
is_flag=True,
|
||||
help="Search for high dynamic range (HDR) photos.",
|
||||
),
|
||||
"--not-hdr": click.Option(
|
||||
["--not-hdr"],
|
||||
is_flag=True,
|
||||
help="Search for photos that are not HDR photos.",
|
||||
),
|
||||
"--selfie": click.Option(
|
||||
["--selfie"],
|
||||
is_flag=True,
|
||||
help="Search for selfies (photos taken with front-facing cameras).",
|
||||
),
|
||||
"--not-selfie": click.Option(
|
||||
["--not-selfie"],
|
||||
is_flag=True,
|
||||
help="Search for photos that are not selfies.",
|
||||
),
|
||||
"--panorama": click.Option(
|
||||
["--panorama"], is_flag=True, help="Search for panorama photos."
|
||||
),
|
||||
"--not-panorama": click.Option(
|
||||
["--not-panorama"],
|
||||
is_flag=True,
|
||||
help="Search for photos that are not panoramas.",
|
||||
),
|
||||
"--has-raw": click.Option(
|
||||
["--has-raw"],
|
||||
is_flag=True,
|
||||
help="Search for photos with both a jpeg and raw version",
|
||||
),
|
||||
"--only-movies": click.Option(
|
||||
["--only-movies"],
|
||||
is_flag=True,
|
||||
help="Search only for movies (default searches both images and movies).",
|
||||
),
|
||||
"--only-photos": click.Option(
|
||||
["--only-photos"],
|
||||
is_flag=True,
|
||||
help="Search only for photos/images (default searches both images and movies).",
|
||||
),
|
||||
"--from-date": click.Option(
|
||||
["--from-date"],
|
||||
help="Search by item start date, e.g. 2000-01-12T12:00:00, 2001-01-12T12:00:00-07:00, or 2000-12-31 (ISO 8601 with/without timezone).",
|
||||
type=DateTimeISO8601(),
|
||||
),
|
||||
"--to-date": click.Option(
|
||||
["--to-date"],
|
||||
help="Search by item end date, e.g. 2000-01-12T12:00:00, 2001-01-12T12:00:00-07:00, or 2000-12-31 (ISO 8601 with/without timezone).",
|
||||
type=DateTimeISO8601(),
|
||||
),
|
||||
"--from-time": click.Option(
|
||||
["--from-time"],
|
||||
help="Search by item start time of day, e.g. 12:00, or 12:00:00.",
|
||||
type=TimeISO8601(),
|
||||
),
|
||||
"--to-time": click.Option(
|
||||
["--to-time"],
|
||||
help="Search by item end time of day, e.g. 12:00 or 12:00:00.",
|
||||
type=TimeISO8601(),
|
||||
),
|
||||
"--year": click.Option(
|
||||
["--year"],
|
||||
metavar="YEAR",
|
||||
help="Search for items from a specific year, e.g. --year 2022 to find all photos from the year 2022. "
|
||||
"May be repeated to search multiple years.",
|
||||
multiple=True,
|
||||
type=int,
|
||||
),
|
||||
"--added-before": click.Option(
|
||||
["--added-before"],
|
||||
metavar="DATE",
|
||||
help="Search for items added to the library before a specific date/time, "
|
||||
"e.g. --added-before e.g. 2000-01-12T12:00:00, 2001-01-12T12:00:00-07:00, or 2000-12-31 (ISO 8601 with/without timezone).",
|
||||
type=DateTimeISO8601(),
|
||||
),
|
||||
"--added-after": click.Option(
|
||||
["--added-after"],
|
||||
metavar="DATE",
|
||||
help="Search for items added to the libray after a specific date/time, "
|
||||
"e.g. --added-after e.g. 2000-01-12T12:00:00, 2001-01-12T12:00:00-07:00, or 2000-12-31 (ISO 8601 with/without timezone).",
|
||||
type=DateTimeISO8601(),
|
||||
),
|
||||
"--added-in-last": click.Option(
|
||||
["--added-in-last"],
|
||||
metavar="TIME_DELTA",
|
||||
help="Search for items added to the library in the last TIME_DELTA, "
|
||||
"where TIME_DELTA is a string like "
|
||||
"'12 hrs', '1 day', '1d', '1 week', '2weeks', '1 month', '1 year'. "
|
||||
"for example, `--added-in-last 7d` and `--added-in-last '1 week'` are equivalent. "
|
||||
"months are assumed to be 30 days and years are assumed to be 365 days. "
|
||||
"Common English abbreviations are accepted, e.g. d, day, days or m, min, minutes.",
|
||||
type=TimeOffset(),
|
||||
),
|
||||
"--has-comment": click.Option(
|
||||
["--has-comment"],
|
||||
is_flag=True,
|
||||
help="Search for photos that have comments.",
|
||||
),
|
||||
"--no-comment": click.Option(
|
||||
["--no-comment"],
|
||||
is_flag=True,
|
||||
help="Search for photos with no comments.",
|
||||
),
|
||||
"--has-likes": click.Option(
|
||||
["--has-likes"], is_flag=True, help="Search for photos that have likes."
|
||||
),
|
||||
"--no-likes": click.Option(
|
||||
["--no-likes"], is_flag=True, help="Search for photos with no likes."
|
||||
),
|
||||
"--is-reference": click.Option(
|
||||
["--is-reference"],
|
||||
is_flag=True,
|
||||
help="Search for photos that were imported as referenced files (not copied into Photos library).",
|
||||
),
|
||||
"--not-reference": click.Option(
|
||||
["--not-reference"],
|
||||
is_flag=True,
|
||||
help="Search for photos that are not references, that is, they were copied into the Photos library "
|
||||
"and are managed by Photos.",
|
||||
),
|
||||
"--in-album": click.Option(
|
||||
["--in-album"],
|
||||
is_flag=True,
|
||||
help="Search for photos that are in one or more albums.",
|
||||
),
|
||||
"--not-in-album": click.Option(
|
||||
["--not-in-album"],
|
||||
is_flag=True,
|
||||
help="Search for photos that are not in any albums.",
|
||||
),
|
||||
"--duplicate": click.Option(
|
||||
["--duplicate"],
|
||||
is_flag=True,
|
||||
help="Search for photos with possible duplicates. osxphotos will compare signatures of photos, "
|
||||
"evaluating date created, size, height, width, and edited status to find *possible* duplicates. "
|
||||
"This does not compare images byte-for-byte nor compare hashes but should find photos imported multiple "
|
||||
"times or duplicated within Photos.",
|
||||
),
|
||||
"--min-size": click.Option(
|
||||
["--min-size"],
|
||||
metavar="SIZE",
|
||||
type=BitMathSize(),
|
||||
help="Search for photos with size >= SIZE bytes. "
|
||||
"The size evaluated is the photo's original size (when imported to Photos). "
|
||||
"Size may be specified as integer bytes or using SI or NIST units. "
|
||||
"For example, the following are all valid and equivalent sizes: '1048576' '1.048576MB', '1 MiB'.",
|
||||
),
|
||||
"--max-size": click.Option(
|
||||
["--max-size"],
|
||||
metavar="SIZE",
|
||||
type=BitMathSize(),
|
||||
help="Search for photos with size <= SIZE bytes. "
|
||||
"The size evaluated is the photo's original size (when imported to Photos). "
|
||||
"Size may be specified as integer bytes or using SI or NIST units. "
|
||||
"For example, the following are all valid and equivalent sizes: '1048576' '1.048576MB', '1 MiB'.",
|
||||
),
|
||||
"--missing": click.Option(
|
||||
["--missing"], is_flag=True, help="Search for photos missing from disk."
|
||||
),
|
||||
"--not-missing": click.Option(
|
||||
["--not-missing"],
|
||||
is_flag=True,
|
||||
help="Search for photos present on disk (e.g. not missing).",
|
||||
),
|
||||
"--cloudasset": click.Option(
|
||||
["--cloudasset"],
|
||||
is_flag=True,
|
||||
help="Search for photos that are part of an iCloud library",
|
||||
),
|
||||
"--not-cloudasset": click.Option(
|
||||
["--not-cloudasset"],
|
||||
is_flag=True,
|
||||
help="Search for photos that are not part of an iCloud library",
|
||||
),
|
||||
"--incloud": click.Option(
|
||||
["--incloud"],
|
||||
is_flag=True,
|
||||
help="Search for photos that are in iCloud (have been synched)",
|
||||
),
|
||||
"--not-incloud": click.Option(
|
||||
["--not-incloud"],
|
||||
is_flag=True,
|
||||
help="Search for photos that are not in iCloud (have not been synched)",
|
||||
),
|
||||
"--regex": click.Option(
|
||||
["--regex"],
|
||||
metavar="REGEX TEMPLATE",
|
||||
nargs=2,
|
||||
multiple=True,
|
||||
help="Search for photos where TEMPLATE matches regular expression REGEX. "
|
||||
"For example, to find photos in an album that begins with 'Beach': '--regex \"^Beach\" \"{album}\"'. "
|
||||
"You may specify more than one regular expression match by repeating '--regex' with different arguments.",
|
||||
),
|
||||
"--selected": click.Option(
|
||||
["--selected"],
|
||||
is_flag=True,
|
||||
help="Filter for photos that are currently selected in Photos.",
|
||||
),
|
||||
"--exif": click.Option(
|
||||
["--exif"],
|
||||
metavar="EXIF_TAG VALUE",
|
||||
nargs=2,
|
||||
multiple=True,
|
||||
help="Search for photos where EXIF_TAG exists in photo's EXIF data and contains VALUE. "
|
||||
"For example, to find photos created by Adobe Photoshop: `--exif Software 'Adobe Photoshop' `"
|
||||
"or to find all photos shot on a Canon camera: `--exif Make Canon`. "
|
||||
"EXIF_TAG can be any valid exiftool tag, with or without group name, e.g. `EXIF:Make` or `Make`. "
|
||||
"To use --exif, exiftool must be installed and in the path.",
|
||||
),
|
||||
"--query-eval": click.Option(
|
||||
["--query-eval"],
|
||||
metavar="CRITERIA",
|
||||
multiple=True,
|
||||
help="Evaluate CRITERIA to filter photos. "
|
||||
"CRITERIA will be evaluated in context of the following python list comprehension: "
|
||||
"`photos = [photo for photo in photos if CRITERIA]` "
|
||||
"where photo represents a PhotoInfo object. "
|
||||
"For example: `--query-eval photo.favorite` returns all photos that have been "
|
||||
"favorited and is equivalent to --favorite. "
|
||||
"You may specify more than one CRITERIA by using --query-eval multiple times. "
|
||||
"CRITERIA must be a valid python expression. "
|
||||
"See https://rhettbull.github.io/osxphotos/ for additional documentation on the PhotoInfo class.",
|
||||
),
|
||||
"--query-function": click.Option(
|
||||
["--query-function"],
|
||||
metavar="filename.py::function",
|
||||
multiple=True,
|
||||
type=FunctionCall(),
|
||||
help="Run function to filter photos. Use this in format: --query-function filename.py::function where filename.py is a python "
|
||||
+ "file you've created and function is the name of the function in the python file you want to call. "
|
||||
+ "Your function will be passed a list of PhotoInfo objects and is expected to return a filtered list of PhotoInfo objects. "
|
||||
+ "You may use more than one function by repeating the --query-function option with a different value. "
|
||||
+ "Your query function will be called after all other query options have been evaluated. "
|
||||
+ "See https://github.com/RhetTbull/osxphotos/blob/master/examples/query_function.py for example of how to use this option.",
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def QUERY_OPTIONS(
|
||||
wrapped=None, *, exclude: list[str] | None = None
|
||||
) -> Callable[..., Any]:
|
||||
"""Function decorator to add query options to a click command.
|
||||
|
||||
Args:
|
||||
wrapped: function to decorate (this is normally passed automatically
|
||||
exclude: list of query options to exclude from the command, for example `exclude=["--shared"]
|
||||
"""
|
||||
if wrapped is None:
|
||||
return functools.partial(QUERY_OPTIONS, exclude=exclude)
|
||||
|
||||
exclude = exclude or []
|
||||
|
||||
def _add_options(wrapped):
|
||||
"""Add query options to wrapped function"""
|
||||
for option in reversed(_QUERY_PARAMETERS_DICT.keys()):
|
||||
if option in exclude:
|
||||
continue
|
||||
click_opt = _QUERY_PARAMETERS_DICT[option]
|
||||
_param_memo(wrapped, click_opt)
|
||||
return wrapped
|
||||
|
||||
return _add_options(wrapped)
|
||||
|
||||
|
||||
_DEBUG_PARAMETERS = [
|
||||
click.Option(
|
||||
["--debug"],
|
||||
is_flag=True,
|
||||
help="Enable debug output.",
|
||||
hidden=OSXPHOTOS_HIDDEN,
|
||||
),
|
||||
click.Option(
|
||||
["--watch"],
|
||||
metavar="MODULE::NAME",
|
||||
multiple=True,
|
||||
help="Watch function or method calls. The function to watch must be in the form "
|
||||
"MODULE::NAME where MODULE is the module path and NAME is the function or method name "
|
||||
"contained in the module. For example, to watch all calls to FileUtil.copy() which is in "
|
||||
"osxphotos.fileutil, use: "
|
||||
"'--watch osxphotos.fileutil::FileUtil.copy'. More than one --watch option can be specified.",
|
||||
hidden=OSXPHOTOS_HIDDEN,
|
||||
),
|
||||
click.Option(
|
||||
["--breakpoint"],
|
||||
metavar="MODULE::NAME",
|
||||
multiple=True,
|
||||
help="Add breakpoint to function calls. The function to watch must be in the form "
|
||||
"MODULE::NAME where MODULE is the module path and NAME is the function or method name "
|
||||
"contained in the module. For example, to set a breakpoint for calls to "
|
||||
"FileUtil.copy() which is in osxphotos.fileutil, use: "
|
||||
"'--breakpoint osxphotos.fileutil::FileUtil.copy'. More than one --breakpoint option can be specified.",
|
||||
hidden=OSXPHOTOS_HIDDEN,
|
||||
),
|
||||
]
|
||||
DEBUG_OPTIONS = make_click_option_decorator(*_DEBUG_PARAMETERS)
|
||||
|
||||
_THEME_PARAMETER = click.Option(
|
||||
["--theme"],
|
||||
metavar="THEME",
|
||||
type=click.Choice(["dark", "light", "mono", "plain"], case_sensitive=False),
|
||||
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.",
|
||||
)
|
||||
THEME_OPTION = make_click_option_decorator(_THEME_PARAMETER)
|
||||
|
||||
_VERBOSE_PARAMETER = click.Option(
|
||||
["--verbose", "-V", "verbose_flag"],
|
||||
count=True,
|
||||
help="Print verbose output; may be specified multiple times for more verbose output.",
|
||||
)
|
||||
VERBOSE_OPTION = make_click_option_decorator(_VERBOSE_PARAMETER)
|
||||
|
||||
_TIMESTAMP_PARAMETER = click.Option(
|
||||
["--timestamp"], is_flag=True, help="Add time stamp to verbose output"
|
||||
)
|
||||
TIMESTAMP_OPTION = make_click_option_decorator(_TIMESTAMP_PARAMETER)
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Globals and constants use by the CLI commands"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import pathlib
|
||||
@@ -14,8 +15,6 @@ from osxphotos._constants import APP_NAME
|
||||
from osxphotos._version import __version__
|
||||
from osxphotos.utils import get_latest_version
|
||||
|
||||
from .param_types import *
|
||||
|
||||
# used to show/hide hidden commands
|
||||
OSXPHOTOS_HIDDEN = not bool(os.getenv("OSXPHOTOS_SHOW_HIDDEN", default=False))
|
||||
|
||||
@@ -31,16 +30,6 @@ CLI_COLOR_WARNING = "yellow"
|
||||
__all__ = [
|
||||
"CLI_COLOR_ERROR",
|
||||
"CLI_COLOR_WARNING",
|
||||
"DB_ARGUMENT",
|
||||
"DB_OPTION",
|
||||
"DEBUG_OPTIONS",
|
||||
"DELETED_OPTIONS",
|
||||
"FIELD_OPTION",
|
||||
"JSON_OPTION",
|
||||
"QUERY_OPTIONS",
|
||||
"THEME_OPTION",
|
||||
"VERBOSE_OPTION",
|
||||
"TIMESTAMP_OPTION",
|
||||
"get_photos_db",
|
||||
"noop",
|
||||
"time_stamp",
|
||||
@@ -90,540 +79,6 @@ def get_photos_db(*db_options):
|
||||
return None
|
||||
|
||||
|
||||
VERSION_CHECK_OPTION = click.option("--no-version-check", required=False, is_flag=True)
|
||||
|
||||
DB_OPTION = click.option(
|
||||
"--db",
|
||||
required=False,
|
||||
metavar="PHOTOS_LIBRARY_PATH",
|
||||
default=None,
|
||||
help=(
|
||||
"Specify Photos database path. "
|
||||
"Path to Photos library/database can be specified using either --db "
|
||||
"or directly as PHOTOS_LIBRARY positional argument. "
|
||||
"If neither --db or PHOTOS_LIBRARY provided, will attempt to find the library "
|
||||
"to use in the following order: 1. last opened library, 2. system library, 3. ~/Pictures/Photos Library.photoslibrary"
|
||||
),
|
||||
type=click.Path(exists=True),
|
||||
)
|
||||
|
||||
DB_ARGUMENT = click.argument("photos_library", nargs=-1, type=click.Path(exists=True))
|
||||
|
||||
JSON_OPTION = click.option(
|
||||
"--json",
|
||||
"json_",
|
||||
required=False,
|
||||
is_flag=True,
|
||||
default=False,
|
||||
help="Print output in JSON format.",
|
||||
)
|
||||
|
||||
FIELD_OPTION = click.option(
|
||||
"--field",
|
||||
"-f",
|
||||
metavar="FIELD TEMPLATE",
|
||||
multiple=True,
|
||||
nargs=2,
|
||||
help="Output only specified custom fields. "
|
||||
"FIELD is the name of the field and TEMPLATE is the template to use as the field value. "
|
||||
"May be repeated to output multiple fields. "
|
||||
"For example, to output photo uuid, name, and title: "
|
||||
'`--field uuid "{uuid}" --field name "{original_name}" --field title "{title}"`.',
|
||||
)
|
||||
|
||||
|
||||
def DELETED_OPTIONS(f):
|
||||
o = click.option
|
||||
options = [
|
||||
o(
|
||||
"--deleted",
|
||||
is_flag=True,
|
||||
help="Include photos from the 'Recently Deleted' folder.",
|
||||
),
|
||||
o(
|
||||
"--deleted-only",
|
||||
is_flag=True,
|
||||
help="Include only photos from the 'Recently Deleted' folder.",
|
||||
),
|
||||
]
|
||||
for o in options[::-1]:
|
||||
f = o(f)
|
||||
return f
|
||||
|
||||
|
||||
def QUERY_OPTIONS(f):
|
||||
o = click.option
|
||||
options = [
|
||||
o(
|
||||
"--keyword",
|
||||
metavar="KEYWORD",
|
||||
default=None,
|
||||
multiple=True,
|
||||
help="Search for photos with keyword KEYWORD. "
|
||||
'If more than one keyword, treated as "OR", e.g. find photos matching any keyword',
|
||||
),
|
||||
o(
|
||||
"--no-keyword",
|
||||
is_flag=True,
|
||||
help="Search for photos with no keyword.",
|
||||
),
|
||||
o(
|
||||
"--person",
|
||||
metavar="PERSON",
|
||||
default=None,
|
||||
multiple=True,
|
||||
help="Search for photos with person PERSON. "
|
||||
'If more than one person, treated as "OR", e.g. find photos matching any person',
|
||||
),
|
||||
o(
|
||||
"--album",
|
||||
metavar="ALBUM",
|
||||
default=None,
|
||||
multiple=True,
|
||||
help="Search for photos in album ALBUM. "
|
||||
'If more than one album, treated as "OR", e.g. find photos matching any album',
|
||||
),
|
||||
o(
|
||||
"--folder",
|
||||
metavar="FOLDER",
|
||||
default=None,
|
||||
multiple=True,
|
||||
help="Search for photos in an album in folder FOLDER. "
|
||||
'If more than one folder, treated as "OR", e.g. find photos in any FOLDER. '
|
||||
"Only searches top level folders (e.g. does not look at subfolders)",
|
||||
),
|
||||
o(
|
||||
"--name",
|
||||
metavar="FILENAME",
|
||||
default=None,
|
||||
multiple=True,
|
||||
help="Search for photos with filename matching FILENAME. "
|
||||
'If more than one --name options is specified, they are treated as "OR", '
|
||||
"e.g. find photos matching any FILENAME. ",
|
||||
),
|
||||
o(
|
||||
"--uuid",
|
||||
metavar="UUID",
|
||||
default=None,
|
||||
multiple=True,
|
||||
help="Search for photos with UUID(s). "
|
||||
"May be repeated to include multiple UUIDs.",
|
||||
),
|
||||
o(
|
||||
"--uuid-from-file",
|
||||
metavar="FILE",
|
||||
default=None,
|
||||
multiple=False,
|
||||
help="Search for photos with UUID(s) loaded from FILE. "
|
||||
"Format is a single UUID per line. Lines preceded with # are ignored.",
|
||||
type=click.Path(exists=True),
|
||||
),
|
||||
o(
|
||||
"--title",
|
||||
metavar="TITLE",
|
||||
default=None,
|
||||
multiple=True,
|
||||
help="Search for TITLE in title of photo.",
|
||||
),
|
||||
o("--no-title", is_flag=True, help="Search for photos with no title."),
|
||||
o(
|
||||
"--description",
|
||||
metavar="DESC",
|
||||
default=None,
|
||||
multiple=True,
|
||||
help="Search for DESC in description of photo.",
|
||||
),
|
||||
o(
|
||||
"--no-description",
|
||||
is_flag=True,
|
||||
help="Search for photos with no description.",
|
||||
),
|
||||
o(
|
||||
"--place",
|
||||
metavar="PLACE",
|
||||
default=None,
|
||||
multiple=True,
|
||||
help="Search for PLACE in photo's reverse geolocation info",
|
||||
),
|
||||
o(
|
||||
"--no-place",
|
||||
is_flag=True,
|
||||
help="Search for photos with no associated place name info (no reverse geolocation info)",
|
||||
),
|
||||
o(
|
||||
"--location",
|
||||
is_flag=True,
|
||||
help="Search for photos with associated location info (e.g. GPS coordinates)",
|
||||
),
|
||||
o(
|
||||
"--no-location",
|
||||
is_flag=True,
|
||||
help="Search for photos with no associated location info (e.g. no GPS coordinates)",
|
||||
),
|
||||
o(
|
||||
"--label",
|
||||
metavar="LABEL",
|
||||
multiple=True,
|
||||
help="Search for photos with image classification label LABEL (Photos 5+ only). "
|
||||
'If more than one label, treated as "OR", e.g. find photos matching any label',
|
||||
),
|
||||
o(
|
||||
"--uti",
|
||||
metavar="UTI",
|
||||
default=None,
|
||||
multiple=False,
|
||||
help="Search for photos whose uniform type identifier (UTI) matches UTI",
|
||||
),
|
||||
o(
|
||||
"-i",
|
||||
"--ignore-case",
|
||||
is_flag=True,
|
||||
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(
|
||||
"--external-edit",
|
||||
is_flag=True,
|
||||
help="Search for photos edited in external editor.",
|
||||
),
|
||||
o("--favorite", is_flag=True, help="Search for photos marked favorite."),
|
||||
o(
|
||||
"--not-favorite",
|
||||
is_flag=True,
|
||||
help="Search for photos not marked favorite.",
|
||||
),
|
||||
o("--hidden", is_flag=True, help="Search for photos marked hidden."),
|
||||
o("--not-hidden", is_flag=True, help="Search for photos not marked hidden."),
|
||||
o(
|
||||
"--shared",
|
||||
is_flag=True,
|
||||
help="Search for photos in shared iCloud album (Photos 5+ only).",
|
||||
),
|
||||
o(
|
||||
"--not-shared",
|
||||
is_flag=True,
|
||||
help="Search for photos not in shared iCloud album (Photos 5+ only).",
|
||||
),
|
||||
o(
|
||||
"--burst",
|
||||
is_flag=True,
|
||||
help="Search for photos that were taken in a burst.",
|
||||
),
|
||||
o(
|
||||
"--not-burst",
|
||||
is_flag=True,
|
||||
help="Search for photos that are not part of a burst.",
|
||||
),
|
||||
o("--live", is_flag=True, help="Search for Apple live photos"),
|
||||
o(
|
||||
"--not-live",
|
||||
is_flag=True,
|
||||
help="Search for photos that are not Apple live photos.",
|
||||
),
|
||||
o("--portrait", is_flag=True, help="Search for Apple portrait mode photos."),
|
||||
o(
|
||||
"--not-portrait",
|
||||
is_flag=True,
|
||||
help="Search for photos that are not Apple portrait mode photos.",
|
||||
),
|
||||
o("--screenshot", is_flag=True, help="Search for screenshot photos."),
|
||||
o(
|
||||
"--not-screenshot",
|
||||
is_flag=True,
|
||||
help="Search for photos that are not screenshot photos.",
|
||||
),
|
||||
o("--slow-mo", is_flag=True, help="Search for slow motion videos."),
|
||||
o(
|
||||
"--not-slow-mo",
|
||||
is_flag=True,
|
||||
help="Search for photos that are not slow motion videos.",
|
||||
),
|
||||
o("--time-lapse", is_flag=True, help="Search for time lapse videos."),
|
||||
o(
|
||||
"--not-time-lapse",
|
||||
is_flag=True,
|
||||
help="Search for photos that are not time lapse videos.",
|
||||
),
|
||||
o("--hdr", is_flag=True, help="Search for high dynamic range (HDR) photos."),
|
||||
o("--not-hdr", is_flag=True, help="Search for photos that are not HDR photos."),
|
||||
o(
|
||||
"--selfie",
|
||||
is_flag=True,
|
||||
help="Search for selfies (photos taken with front-facing cameras).",
|
||||
),
|
||||
o("--not-selfie", is_flag=True, help="Search for photos that are not selfies."),
|
||||
o("--panorama", is_flag=True, help="Search for panorama photos."),
|
||||
o(
|
||||
"--not-panorama",
|
||||
is_flag=True,
|
||||
help="Search for photos that are not panoramas.",
|
||||
),
|
||||
o(
|
||||
"--has-raw",
|
||||
is_flag=True,
|
||||
help="Search for photos with both a jpeg and raw version",
|
||||
),
|
||||
o(
|
||||
"--only-movies",
|
||||
is_flag=True,
|
||||
help="Search only for movies (default searches both images and movies).",
|
||||
),
|
||||
o(
|
||||
"--only-photos",
|
||||
is_flag=True,
|
||||
help="Search only for photos/images (default searches both images and movies).",
|
||||
),
|
||||
o(
|
||||
"--from-date",
|
||||
help="Search by item start date, e.g. 2000-01-12T12:00:00, 2001-01-12T12:00:00-07:00, or 2000-12-31 (ISO 8601 with/without timezone).",
|
||||
type=DateTimeISO8601(),
|
||||
),
|
||||
o(
|
||||
"--to-date",
|
||||
help="Search by item end date, e.g. 2000-01-12T12:00:00, 2001-01-12T12:00:00-07:00, or 2000-12-31 (ISO 8601 with/without timezone).",
|
||||
type=DateTimeISO8601(),
|
||||
),
|
||||
o(
|
||||
"--from-time",
|
||||
help="Search by item start time of day, e.g. 12:00, or 12:00:00.",
|
||||
type=TimeISO8601(),
|
||||
),
|
||||
o(
|
||||
"--to-time",
|
||||
help="Search by item end time of day, e.g. 12:00 or 12:00:00.",
|
||||
type=TimeISO8601(),
|
||||
),
|
||||
o(
|
||||
"--year",
|
||||
metavar="YEAR",
|
||||
help="Search for items from a specific year, e.g. --year 2022 to find all photos from the year 2022. "
|
||||
"May be repeated to search multiple years.",
|
||||
multiple=True,
|
||||
type=int,
|
||||
),
|
||||
o(
|
||||
"--added-before",
|
||||
metavar="DATE",
|
||||
help="Search for items added to the library before a specific date/time, "
|
||||
"e.g. --added-before e.g. 2000-01-12T12:00:00, 2001-01-12T12:00:00-07:00, or 2000-12-31 (ISO 8601 with/without timezone).",
|
||||
type=DateTimeISO8601(),
|
||||
),
|
||||
o(
|
||||
"--added-after",
|
||||
metavar="DATE",
|
||||
help="Search for items added to the libray after a specific date/time, "
|
||||
"e.g. --added-after e.g. 2000-01-12T12:00:00, 2001-01-12T12:00:00-07:00, or 2000-12-31 (ISO 8601 with/without timezone).",
|
||||
type=DateTimeISO8601(),
|
||||
),
|
||||
o(
|
||||
"--added-in-last",
|
||||
metavar="TIME_DELTA",
|
||||
help="Search for items added to the library in the last TIME_DELTA, "
|
||||
"where TIME_DELTA is a string like "
|
||||
"'12 hrs', '1 day', '1d', '1 week', '2weeks', '1 month', '1 year'. "
|
||||
"for example, `--added-in-last 7d` and `--added-in-last '1 week'` are equivalent. "
|
||||
"months are assumed to be 30 days and years are assumed to be 365 days. "
|
||||
"Common English abbreviations are accepted, e.g. d, day, days or m, min, minutes.",
|
||||
type=TimeOffset(),
|
||||
),
|
||||
o("--has-comment", is_flag=True, help="Search for photos that have comments."),
|
||||
o("--no-comment", is_flag=True, help="Search for photos with no comments."),
|
||||
o("--has-likes", is_flag=True, help="Search for photos that have likes."),
|
||||
o("--no-likes", is_flag=True, help="Search for photos with no likes."),
|
||||
o(
|
||||
"--is-reference",
|
||||
is_flag=True,
|
||||
help="Search for photos that were imported as referenced files (not copied into Photos library).",
|
||||
),
|
||||
o(
|
||||
"--not-reference",
|
||||
is_flag=True,
|
||||
help="Search for photos that are not references, that is, they were copied into the Photos library "
|
||||
"and are managed by Photos.",
|
||||
),
|
||||
o(
|
||||
"--in-album",
|
||||
is_flag=True,
|
||||
help="Search for photos that are in one or more albums.",
|
||||
),
|
||||
o(
|
||||
"--not-in-album",
|
||||
is_flag=True,
|
||||
help="Search for photos that are not in any albums.",
|
||||
),
|
||||
o(
|
||||
"--duplicate",
|
||||
is_flag=True,
|
||||
help="Search for photos with possible duplicates. osxphotos will compare signatures of photos, "
|
||||
"evaluating date created, size, height, width, and edited status to find *possible* duplicates. "
|
||||
"This does not compare images byte-for-byte nor compare hashes but should find photos imported multiple "
|
||||
"times or duplicated within Photos.",
|
||||
),
|
||||
o(
|
||||
"--min-size",
|
||||
metavar="SIZE",
|
||||
type=BitMathSize(),
|
||||
help="Search for photos with size >= SIZE bytes. "
|
||||
"The size evaluated is the photo's original size (when imported to Photos). "
|
||||
"Size may be specified as integer bytes or using SI or NIST units. "
|
||||
"For example, the following are all valid and equivalent sizes: '1048576' '1.048576MB', '1 MiB'.",
|
||||
),
|
||||
o(
|
||||
"--max-size",
|
||||
metavar="SIZE",
|
||||
type=BitMathSize(),
|
||||
help="Search for photos with size <= SIZE bytes. "
|
||||
"The size evaluated is the photo's original size (when imported to Photos). "
|
||||
"Size may be specified as integer bytes or using SI or NIST units. "
|
||||
"For example, the following are all valid and equivalent sizes: '1048576' '1.048576MB', '1 MiB'.",
|
||||
),
|
||||
o("--missing", is_flag=True, help="Search for photos missing from disk."),
|
||||
o(
|
||||
"--not-missing",
|
||||
is_flag=True,
|
||||
help="Search for photos present on disk (e.g. not missing).",
|
||||
),
|
||||
o(
|
||||
"--cloudasset",
|
||||
is_flag=True,
|
||||
help="Search for photos that are part of an iCloud library",
|
||||
),
|
||||
o(
|
||||
"--not-cloudasset",
|
||||
is_flag=True,
|
||||
help="Search for photos that are not part of an iCloud library",
|
||||
),
|
||||
o(
|
||||
"--incloud",
|
||||
is_flag=True,
|
||||
help="Search for photos that are in iCloud (have been synched)",
|
||||
),
|
||||
o(
|
||||
"--not-incloud",
|
||||
is_flag=True,
|
||||
help="Search for photos that are not in iCloud (have not been synched)",
|
||||
),
|
||||
o(
|
||||
"--regex",
|
||||
metavar="REGEX TEMPLATE",
|
||||
nargs=2,
|
||||
multiple=True,
|
||||
help="Search for photos where TEMPLATE matches regular expression REGEX. "
|
||||
"For example, to find photos in an album that begins with 'Beach': '--regex \"^Beach\" \"{album}\"'. "
|
||||
"You may specify more than one regular expression match by repeating '--regex' with different arguments.",
|
||||
),
|
||||
o(
|
||||
"--selected",
|
||||
is_flag=True,
|
||||
help="Filter for photos that are currently selected in Photos.",
|
||||
),
|
||||
o(
|
||||
"--exif",
|
||||
metavar="EXIF_TAG VALUE",
|
||||
nargs=2,
|
||||
multiple=True,
|
||||
help="Search for photos where EXIF_TAG exists in photo's EXIF data and contains VALUE. "
|
||||
"For example, to find photos created by Adobe Photoshop: `--exif Software 'Adobe Photoshop' `"
|
||||
"or to find all photos shot on a Canon camera: `--exif Make Canon`. "
|
||||
"EXIF_TAG can be any valid exiftool tag, with or without group name, e.g. `EXIF:Make` or `Make`. "
|
||||
"To use --exif, exiftool must be installed and in the path.",
|
||||
),
|
||||
o(
|
||||
"--query-eval",
|
||||
metavar="CRITERIA",
|
||||
multiple=True,
|
||||
help="Evaluate CRITERIA to filter photos. "
|
||||
"CRITERIA will be evaluated in context of the following python list comprehension: "
|
||||
"`photos = [photo for photo in photos if CRITERIA]` "
|
||||
"where photo represents a PhotoInfo object. "
|
||||
"For example: `--query-eval photo.favorite` returns all photos that have been "
|
||||
"favorited and is equivalent to --favorite. "
|
||||
"You may specify more than one CRITERIA by using --query-eval multiple times. "
|
||||
"CRITERIA must be a valid python expression. "
|
||||
"See https://rhettbull.github.io/osxphotos/ for additional documentation on the PhotoInfo class.",
|
||||
),
|
||||
o(
|
||||
"--query-function",
|
||||
metavar="filename.py::function",
|
||||
multiple=True,
|
||||
type=FunctionCall(),
|
||||
help="Run function to filter photos. Use this in format: --query-function filename.py::function where filename.py is a python "
|
||||
+ "file you've created and function is the name of the function in the python file you want to call. "
|
||||
+ "Your function will be passed a list of PhotoInfo objects and is expected to return a filtered list of PhotoInfo objects. "
|
||||
+ "You may use more than one function by repeating the --query-function option with a different value. "
|
||||
+ "Your query function will be called after all other query options have been evaluated. "
|
||||
+ "See https://github.com/RhetTbull/osxphotos/blob/master/examples/query_function.py for example of how to use this option.",
|
||||
),
|
||||
]
|
||||
for o in options[::-1]:
|
||||
f = o(f)
|
||||
return f
|
||||
|
||||
|
||||
def DEBUG_OPTIONS(f):
|
||||
o = click.option
|
||||
options = [
|
||||
o(
|
||||
"--debug",
|
||||
is_flag=True,
|
||||
help="Enable debug output.",
|
||||
hidden=OSXPHOTOS_HIDDEN,
|
||||
),
|
||||
o(
|
||||
"--watch",
|
||||
metavar="MODULE::NAME",
|
||||
multiple=True,
|
||||
help="Watch function or method calls. The function to watch must be in the form "
|
||||
"MODULE::NAME where MODULE is the module path and NAME is the function or method name "
|
||||
"contained in the module. For example, to watch all calls to FileUtil.copy() which is in "
|
||||
"osxphotos.fileutil, use: "
|
||||
"'--watch osxphotos.fileutil::FileUtil.copy'. More than one --watch option can be specified.",
|
||||
hidden=OSXPHOTOS_HIDDEN,
|
||||
),
|
||||
o(
|
||||
"--breakpoint",
|
||||
metavar="MODULE::NAME",
|
||||
multiple=True,
|
||||
help="Add breakpoint to function calls. The function to watch must be in the form "
|
||||
"MODULE::NAME where MODULE is the module path and NAME is the function or method name "
|
||||
"contained in the module. For example, to set a breakpoint for calls to "
|
||||
"FileUtil.copy() which is in osxphotos.fileutil, use: "
|
||||
"'--breakpoint osxphotos.fileutil::FileUtil.copy'. More than one --breakpoint option can be specified.",
|
||||
hidden=OSXPHOTOS_HIDDEN,
|
||||
),
|
||||
]
|
||||
for o in options[::-1]:
|
||||
f = o(f)
|
||||
return f
|
||||
|
||||
|
||||
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 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.",
|
||||
)
|
||||
|
||||
TIMESTAMP_OPTION = click.option(
|
||||
"--timestamp", is_flag=True, help="Add time stamp to verbose output"
|
||||
)
|
||||
|
||||
|
||||
def get_config_dir() -> pathlib.Path:
|
||||
"""Get the directory where config files are stored; create it if necessary."""
|
||||
config_dir = xdg_config_home() / APP_NAME
|
||||
|
||||
@@ -8,16 +8,17 @@ from rich import print
|
||||
|
||||
import osxphotos
|
||||
from osxphotos._constants import _PHOTOS_4_VERSION, _UNKNOWN_PLACE
|
||||
from osxphotos.queryoptions import query_options_from_kwargs
|
||||
|
||||
from .common import (
|
||||
from .cli_params import (
|
||||
DB_ARGUMENT,
|
||||
DB_OPTION,
|
||||
JSON_OPTION,
|
||||
OSXPHOTOS_HIDDEN,
|
||||
QUERY_OPTIONS,
|
||||
TIMESTAMP_OPTION,
|
||||
VERBOSE_OPTION,
|
||||
get_photos_db,
|
||||
)
|
||||
from .common import OSXPHOTOS_HIDDEN, get_photos_db
|
||||
from .list import _list_libraries
|
||||
from .verbose import verbose_print
|
||||
|
||||
@@ -32,22 +33,24 @@ from .verbose import verbose_print
|
||||
+ "can also use albums, persons, keywords, photos to dump related attributes.",
|
||||
multiple=True,
|
||||
)
|
||||
@click.option(
|
||||
"--uuid",
|
||||
metavar="UUID",
|
||||
help="Use with '--dump photos' to dump only certain UUIDs. "
|
||||
"May be repeated to include multiple UUIDs.",
|
||||
multiple=True,
|
||||
)
|
||||
@VERBOSE_OPTION
|
||||
@TIMESTAMP_OPTION
|
||||
@QUERY_OPTIONS
|
||||
@click.pass_obj
|
||||
@click.pass_context
|
||||
def debug_dump(ctx, cli_obj, db, photos_library, dump, uuid, verbose_flag, timestamp):
|
||||
"""Print out debug info"""
|
||||
def debug_dump(
|
||||
ctx, cli_obj, db, photos_library, dump, verbose_flag, timestamp, **kwargs
|
||||
):
|
||||
"""Print out debug info.
|
||||
|
||||
When run with --dump photos, any of the query options can be used to limit the
|
||||
photos printed. For example, to print info on currently selected photos:
|
||||
|
||||
osxphotos debug-dump --dump photos --selected
|
||||
"""
|
||||
|
||||
verbose = verbose_print(verbose_flag, timestamp)
|
||||
db = get_photos_db(*photos_library, db, cli_obj.db)
|
||||
db = get_photos_db(*photos_library, db, cli_obj.db if cli_obj else None)
|
||||
if db is None:
|
||||
click.echo(ctx.obj.group.commands["debug-dump"].get_help(ctx), err=True)
|
||||
click.echo("\n\nLocated the following Photos library databases: ", err=True)
|
||||
@@ -87,16 +90,15 @@ def debug_dump(ctx, cli_obj, db, photos_library, dump, uuid, verbose_flag, times
|
||||
print("_dbpersons_fullname:")
|
||||
pprint.pprint(photosdb._dbpersons_fullname)
|
||||
elif attr == "photos":
|
||||
if uuid:
|
||||
for uuid_ in uuid:
|
||||
print(f"_dbphotos['{uuid_}']:")
|
||||
try:
|
||||
pprint.pprint(photosdb._dbphotos[uuid_])
|
||||
except KeyError:
|
||||
print(f"Did not find uuid {uuid_} in _dbphotos")
|
||||
else:
|
||||
print("_dbphotos:")
|
||||
pprint.pprint(photosdb._dbphotos)
|
||||
query_options = query_options_from_kwargs(**kwargs)
|
||||
photos = photosdb.query(options=query_options)
|
||||
uuid = [photo.uuid for photo in photos]
|
||||
for uuid_ in uuid:
|
||||
print(f"_dbphotos['{uuid_}']:")
|
||||
try:
|
||||
pprint.pprint(photosdb._dbphotos[uuid_])
|
||||
except KeyError:
|
||||
print(f"Did not find uuid {uuid_} in _dbphotos")
|
||||
else:
|
||||
try:
|
||||
val = getattr(photosdb, attr)
|
||||
|
||||
@@ -13,10 +13,10 @@ from osxphotos._version import __version__
|
||||
from .common import get_config_dir, get_data_dir
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.command(name="docs")
|
||||
@click.pass_obj
|
||||
@click.pass_context
|
||||
def docs(ctx, cli_obj):
|
||||
def docs_command(ctx, cli_obj):
|
||||
"""Open osxphotos documentation in your browser."""
|
||||
|
||||
# first check if docs installed in old location in confir dir and if so, delete them
|
||||
|
||||
@@ -11,15 +11,15 @@ from osxphotos.cli.click_rich_echo import (
|
||||
from osxphotos.phototemplate import RenderOptions
|
||||
from osxphotos.queryoptions import QueryOptions
|
||||
|
||||
from .color_themes import get_default_theme
|
||||
from .common import (
|
||||
from .cli_params import (
|
||||
DB_ARGUMENT,
|
||||
DB_OPTION,
|
||||
DELETED_OPTIONS,
|
||||
FIELD_OPTION,
|
||||
JSON_OPTION,
|
||||
get_photos_db,
|
||||
)
|
||||
from .color_themes import get_default_theme
|
||||
from .common import get_photos_db
|
||||
from .list import _list_libraries
|
||||
from .print_photo_info import print_photo_fields, print_photo_info
|
||||
from .verbose import get_verbose_console
|
||||
@@ -56,7 +56,11 @@ def dump(
|
||||
photos_library,
|
||||
print_template,
|
||||
):
|
||||
"""Print list of all photos & associated info from the Photos library."""
|
||||
"""Print list of all photos & associated info from the Photos library.
|
||||
|
||||
NOTE: dump is DEPRECATED and will be removed in a future release.
|
||||
Use `osxphotos query` instead.
|
||||
"""
|
||||
|
||||
# below needed for to make CliRunner work for testing
|
||||
cli_db = cli_obj.db if cli_obj is not None else None
|
||||
|
||||
@@ -17,14 +17,9 @@ from osxphotos.fileutil import FileUtil, FileUtilNoOp
|
||||
from osxphotos.photoexporter import ExportOptions, ExportResults, PhotoExporter
|
||||
from osxphotos.utils import pluralize
|
||||
|
||||
from .cli_params import DB_OPTION, THEME_OPTION, TIMESTAMP_OPTION, VERBOSE_OPTION
|
||||
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 .common import 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
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""export command for osxphotos CLI"""
|
||||
|
||||
import inspect
|
||||
import os
|
||||
import pathlib
|
||||
import platform
|
||||
@@ -55,29 +56,31 @@ from osxphotos.photokit import (
|
||||
)
|
||||
from osxphotos.photosalbum import PhotosAlbum
|
||||
from osxphotos.phototemplate import PhotoTemplate, RenderOptions
|
||||
from osxphotos.queryoptions import QueryOptions, load_uuid_from_file
|
||||
from osxphotos.queryoptions import load_uuid_from_file, query_options_from_kwargs
|
||||
from osxphotos.uti import get_preferred_uti_extension
|
||||
from osxphotos.utils import (
|
||||
get_macos_version,
|
||||
format_sec_to_hhmmss,
|
||||
get_macos_version,
|
||||
normalize_fs_path,
|
||||
pluralize,
|
||||
)
|
||||
|
||||
from .click_rich_echo import rich_click_echo, rich_echo, rich_echo_error
|
||||
from .common import (
|
||||
CLI_COLOR_ERROR,
|
||||
CLI_COLOR_WARNING,
|
||||
from .cli_params import (
|
||||
DB_ARGUMENT,
|
||||
DB_OPTION,
|
||||
DELETED_OPTIONS,
|
||||
JSON_OPTION,
|
||||
OSXPHOTOS_CRASH_LOG,
|
||||
OSXPHOTOS_HIDDEN,
|
||||
QUERY_OPTIONS,
|
||||
THEME_OPTION,
|
||||
TIMESTAMP_OPTION,
|
||||
VERBOSE_OPTION,
|
||||
)
|
||||
from .click_rich_echo import rich_click_echo, rich_echo, rich_echo_error
|
||||
from .common import (
|
||||
CLI_COLOR_ERROR,
|
||||
CLI_COLOR_WARNING,
|
||||
OSXPHOTOS_CRASH_LOG,
|
||||
OSXPHOTOS_HIDDEN,
|
||||
get_photos_db,
|
||||
noop,
|
||||
)
|
||||
@@ -940,7 +943,8 @@ def export(
|
||||
|
||||
# re-set the local vars to the corresponding config value
|
||||
# this isn't elegant but avoids having to rewrite this function to use cfg.varname for every parameter
|
||||
|
||||
# the query options appear to be unaccessed but they are used below by query_options_from_kwargs
|
||||
# which accesses them via locals() to avoid a long list of parameters
|
||||
add_exported_to_album = cfg.add_exported_to_album
|
||||
add_missing_to_album = cfg.add_missing_to_album
|
||||
add_skipped_to_album = cfg.add_skipped_to_album
|
||||
@@ -1165,14 +1169,14 @@ def export(
|
||||
|
||||
if config_only and not save_config:
|
||||
rich_click_echo(
|
||||
"[error]--config-only must be used with --save-config",
|
||||
"[error]Incompatible export options: --config-only must be used with --save-config",
|
||||
err=True,
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
if all(x in [s.lower() for s in sidecar] for x in ["json", "exiftool"]):
|
||||
rich_click_echo(
|
||||
"[error]Cannot use --sidecar json with --sidecar exiftool due to name collisions",
|
||||
"[error]Incompatible export options:: cannot use --sidecar json with --sidecar exiftool due to name collisions",
|
||||
err=True,
|
||||
)
|
||||
sys.exit(1)
|
||||
@@ -1263,12 +1267,6 @@ def export(
|
||||
if only_photos:
|
||||
movies = False
|
||||
|
||||
# load UUIDs if necessary and append to any uuids passed with --uuid
|
||||
if uuid_from_file:
|
||||
uuid_list = list(uuid) # Click option is a tuple
|
||||
uuid_list.extend(load_uuid_from_file(uuid_from_file))
|
||||
uuid = tuple(uuid_list)
|
||||
|
||||
# below needed for to make CliRunner work for testing
|
||||
cli_db = cli_obj.db if cli_obj is not None else None
|
||||
db = get_photos_db(*photos_library, db, cli_db)
|
||||
@@ -1345,92 +1343,12 @@ def export(
|
||||
# enable beta features if requested
|
||||
photosdb._beta = beta
|
||||
|
||||
query_options = QueryOptions(
|
||||
added_after=added_after,
|
||||
added_before=added_before,
|
||||
added_in_last=added_in_last,
|
||||
album=album,
|
||||
burst_photos=export_bursts,
|
||||
burst=burst,
|
||||
cloudasset=cloudasset,
|
||||
deleted_only=deleted_only,
|
||||
deleted=deleted,
|
||||
description=description,
|
||||
duplicate=duplicate,
|
||||
edited=edited,
|
||||
exif=exif,
|
||||
external_edit=external_edit,
|
||||
favorite=favorite,
|
||||
folder=folder,
|
||||
from_date=from_date,
|
||||
from_time=from_time,
|
||||
function=query_function,
|
||||
has_comment=has_comment,
|
||||
has_likes=has_likes,
|
||||
has_raw=has_raw,
|
||||
hdr=hdr,
|
||||
hidden=hidden,
|
||||
ignore_case=ignore_case,
|
||||
in_album=in_album,
|
||||
incloud=incloud,
|
||||
is_reference=is_reference,
|
||||
keyword=keyword,
|
||||
label=label,
|
||||
live=live,
|
||||
location=location,
|
||||
max_size=max_size,
|
||||
min_size=min_size,
|
||||
# skip missing bursts if using --download-missing by itself as AppleScript otherwise causes errors
|
||||
missing_bursts=(download_missing and use_photokit) or not download_missing,
|
||||
missing=missing,
|
||||
movies=movies,
|
||||
name=name,
|
||||
no_comment=no_comment,
|
||||
no_description=no_description,
|
||||
no_likes=no_likes,
|
||||
no_location=no_location,
|
||||
no_keyword=no_keyword,
|
||||
no_place=no_place,
|
||||
no_title=no_title,
|
||||
not_burst=not_burst,
|
||||
not_cloudasset=not_cloudasset,
|
||||
not_edited=not_edited,
|
||||
not_favorite=not_favorite,
|
||||
not_hdr=not_hdr,
|
||||
not_hidden=not_hidden,
|
||||
not_in_album=not_in_album,
|
||||
not_incloud=not_incloud,
|
||||
not_live=not_live,
|
||||
not_missing=not_missing,
|
||||
not_panorama=not_panorama,
|
||||
not_portrait=not_portrait,
|
||||
not_reference=not_reference,
|
||||
not_screenshot=not_screenshot,
|
||||
not_selfie=not_selfie,
|
||||
not_shared=not_shared,
|
||||
not_slow_mo=not_slow_mo,
|
||||
not_time_lapse=not_time_lapse,
|
||||
panorama=panorama,
|
||||
person=person,
|
||||
photos=photos,
|
||||
place=place,
|
||||
portrait=portrait,
|
||||
query_eval=query_eval,
|
||||
regex=regex,
|
||||
screenshot=screenshot,
|
||||
selected=selected,
|
||||
selfie=selfie,
|
||||
shared=shared,
|
||||
slow_mo=slow_mo,
|
||||
time_lapse=time_lapse,
|
||||
title=title,
|
||||
to_date=to_date,
|
||||
to_time=to_time,
|
||||
uti=uti,
|
||||
uuid=uuid,
|
||||
year=year,
|
||||
query_kwargs = locals()
|
||||
# skip missing bursts if using --download-missing by itself as AppleScript otherwise causes errors
|
||||
query_kwargs["missing_bursts"] = (
|
||||
(download_missing and use_photokit) or not download_missing,
|
||||
)
|
||||
|
||||
query_options = query_options_from_kwargs(**query_kwargs)
|
||||
try:
|
||||
photos = photosdb.query(query_options)
|
||||
except ValueError as e:
|
||||
@@ -1495,59 +1413,16 @@ def export(
|
||||
)
|
||||
for p in photos:
|
||||
photo_num += 1
|
||||
export_results = export_photo(
|
||||
photo=p,
|
||||
dest=dest,
|
||||
album_keyword=album_keyword,
|
||||
convert_to_jpeg=convert_to_jpeg,
|
||||
description_template=description_template,
|
||||
directory=directory,
|
||||
download_missing=download_missing,
|
||||
dry_run=dry_run,
|
||||
edited_suffix=edited_suffix,
|
||||
exiftool_merge_keywords=exiftool_merge_keywords,
|
||||
exiftool_merge_persons=exiftool_merge_persons,
|
||||
exiftool_option=exiftool_option,
|
||||
exiftool=exiftool,
|
||||
export_as_hardlink=export_as_hardlink,
|
||||
export_by_date=export_by_date,
|
||||
export_db=export_db,
|
||||
export_dir=dest,
|
||||
export_edited=export_edited,
|
||||
export_live=export_live,
|
||||
export_preview=preview,
|
||||
export_raw=export_raw,
|
||||
favorite_rating=favorite_rating,
|
||||
filename_template=filename_template,
|
||||
fileutil=fileutil,
|
||||
force_update=force_update,
|
||||
ignore_date_modified=ignore_date_modified,
|
||||
ignore_signature=ignore_signature,
|
||||
jpeg_ext=jpeg_ext,
|
||||
jpeg_quality=jpeg_quality,
|
||||
keyword_template=keyword_template,
|
||||
num_photos=num_photos,
|
||||
original_name=original_name,
|
||||
original_suffix=original_suffix,
|
||||
overwrite=overwrite,
|
||||
person_keyword=person_keyword,
|
||||
photo_num=photo_num,
|
||||
preview_if_missing=preview_if_missing,
|
||||
preview_suffix=preview_suffix,
|
||||
replace_keywords=replace_keywords,
|
||||
retry=retry,
|
||||
sidecar_drop_ext=sidecar_drop_ext,
|
||||
sidecar=sidecar,
|
||||
skip_original_if_edited=skip_original_if_edited,
|
||||
strip=strip,
|
||||
touch_file=touch_file,
|
||||
update=update,
|
||||
update_errors=update_errors,
|
||||
use_photokit=use_photokit,
|
||||
use_photos_export=use_photos_export,
|
||||
verbose=verbose,
|
||||
tmpdir=tmpdir,
|
||||
)
|
||||
# hack to avoid passing all the options to export_photo
|
||||
kwargs = {
|
||||
k: v
|
||||
for k, v in locals().items()
|
||||
if k in inspect.getfullargspec(export_photo).args
|
||||
}
|
||||
kwargs["photo"] = p
|
||||
kwargs["export_dir"] = dest
|
||||
kwargs["export_preview"] = preview
|
||||
export_results = export_photo(**kwargs)
|
||||
|
||||
if post_function:
|
||||
for function in post_function:
|
||||
@@ -1896,7 +1771,6 @@ def export_photo(
|
||||
Raises:
|
||||
ValueError on invalid filename_template
|
||||
"""
|
||||
|
||||
export_original = not (skip_original_if_edited and photo.hasadjustments)
|
||||
|
||||
# can't export edited if photo doesn't have edited versions
|
||||
|
||||
@@ -34,7 +34,7 @@ from .click_rich_echo import (
|
||||
set_rich_theme,
|
||||
)
|
||||
from .color_themes import get_theme
|
||||
from .common import TIMESTAMP_OPTION, VERBOSE_OPTION
|
||||
from .cli_params 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
|
||||
|
||||
@@ -8,7 +8,8 @@ from rich import print
|
||||
from osxphotos.photosdb.photosdb_utils import get_photos_library_version
|
||||
from osxphotos.sqlgrep import sqlgrep
|
||||
|
||||
from .common import DB_OPTION, OSXPHOTOS_HIDDEN, get_photos_db
|
||||
from .cli_params import DB_OPTION, OSXPHOTOS_HIDDEN
|
||||
from .common import get_photos_db
|
||||
|
||||
|
||||
@click.command(name="grep", hidden=OSXPHOTOS_HIDDEN)
|
||||
|
||||
@@ -27,7 +27,8 @@ from strpdatetime import strpdatetime
|
||||
|
||||
from osxphotos._constants import _OSXPHOTOS_NONE_SENTINEL
|
||||
from osxphotos._version import __version__
|
||||
from osxphotos.cli.common import TIMESTAMP_OPTION, VERBOSE_OPTION, get_data_dir
|
||||
from osxphotos.cli.cli_params import TIMESTAMP_OPTION, VERBOSE_OPTION
|
||||
from osxphotos.cli.common import 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,8 +45,8 @@ from osxphotos.phototemplate import PhotoTemplate, RenderOptions
|
||||
from osxphotos.sqlitekvstore import SQLiteKVStore
|
||||
from osxphotos.utils import pluralize
|
||||
|
||||
from .cli_params import THEME_OPTION
|
||||
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
|
||||
|
||||
|
||||
@@ -8,7 +8,8 @@ import yaml
|
||||
import osxphotos
|
||||
from osxphotos._constants import _PHOTOS_4_VERSION
|
||||
|
||||
from .common import DB_ARGUMENT, DB_OPTION, JSON_OPTION, get_photos_db
|
||||
from .cli_params import DB_ARGUMENT, DB_OPTION, JSON_OPTION
|
||||
from .common import get_photos_db
|
||||
from .list import _list_libraries
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""install/uninstall/run commands for osxphotos CLI"""
|
||||
|
||||
|
||||
import contextlib
|
||||
import sys
|
||||
from runpy import run_module, run_path
|
||||
|
||||
@@ -50,14 +52,19 @@ def uninstall(packages, yes):
|
||||
|
||||
|
||||
@click.command(name="run", cls=RunCommand)
|
||||
# help command passed just to keep click from intercepting help
|
||||
# and allowing --help to be passed to the script being run
|
||||
@click.option("--help", "-h", is_flag=True, help="Show this message and exit")
|
||||
@click.argument("python_file", nargs=1, type=click.Path(exists=True))
|
||||
@click.argument("args", metavar="ARGS", nargs=-1)
|
||||
def run(python_file, help, args):
|
||||
"""Run a python file using same environment as osxphotos.
|
||||
Any args are made available to the python file."""
|
||||
# drop first two arguments, which are the osxphotos script and run command
|
||||
sys.argv = sys.argv[2:]
|
||||
|
||||
# Need to drop all the args from sys.argv up to and including the run command
|
||||
# For example, command could be one of the following:
|
||||
# osxphotos run example.py --help
|
||||
# osxphotos --debug run example.py --verbose --db /path/to/photos.db
|
||||
# etc.
|
||||
with contextlib.suppress(ValueError):
|
||||
index = sys.argv.index("run")
|
||||
sys.argv = sys.argv[index + 1 :]
|
||||
run_path(python_file, run_name="__main__")
|
||||
|
||||
@@ -7,7 +7,8 @@ import yaml
|
||||
|
||||
import osxphotos
|
||||
|
||||
from .common import DB_ARGUMENT, DB_OPTION, JSON_OPTION, get_photos_db
|
||||
from .cli_params import DB_ARGUMENT, DB_OPTION, JSON_OPTION
|
||||
from .common import get_photos_db
|
||||
from .list import _list_libraries
|
||||
|
||||
|
||||
|
||||
50
osxphotos/cli/kvstore.py
Normal file
50
osxphotos/cli/kvstore.py
Normal file
@@ -0,0 +1,50 @@
|
||||
"""Simple interface to SQLiteKVStore for storing state between runs of the CLI tool."""
|
||||
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import atexit
|
||||
import contextlib
|
||||
import datetime
|
||||
|
||||
from osxphotos.sqlitekvstore import SQLiteKVStore
|
||||
|
||||
from .common import get_data_dir
|
||||
|
||||
__all__ = ["kvstore"]
|
||||
|
||||
# Store open connections
|
||||
__kvstores = []
|
||||
|
||||
|
||||
@atexit.register
|
||||
def close_kvstore():
|
||||
"""Close any open SQLiteKVStore databases"""
|
||||
global __kvstores
|
||||
for kv in __kvstores:
|
||||
with contextlib.suppress(Exception):
|
||||
kv.close()
|
||||
|
||||
|
||||
def kvstore(name: str) -> SQLiteKVStore:
|
||||
"""Return a key/value store for storing state between commands.
|
||||
|
||||
The key/value store is a SQLite database stored in the user's XDG data directory,
|
||||
usually `~/.local/share/`. The key/value store can be used like a dict to store
|
||||
arbitrary key/value pairs which persist between runs of the CLI tool.
|
||||
|
||||
Args:
|
||||
name: a unique name for the key/value store
|
||||
|
||||
Returns:
|
||||
SQLiteKVStore object
|
||||
"""
|
||||
global __kvstores
|
||||
data_dir = get_data_dir()
|
||||
if not name.endswith(".db"):
|
||||
name += ".db"
|
||||
kv = SQLiteKVStore(str(data_dir / name), wal=True)
|
||||
if not kv.about:
|
||||
kv.about = f"Key/value store for {name}, created by osxphotos CLI on {datetime.datetime.now()}"
|
||||
__kvstores.append(kv)
|
||||
return kv
|
||||
@@ -7,7 +7,8 @@ import yaml
|
||||
|
||||
import osxphotos
|
||||
|
||||
from .common import DB_ARGUMENT, DB_OPTION, JSON_OPTION, get_photos_db
|
||||
from .cli_params import DB_ARGUMENT, DB_OPTION, JSON_OPTION
|
||||
from .common import get_photos_db
|
||||
from .list import _list_libraries
|
||||
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import click
|
||||
|
||||
import osxphotos
|
||||
|
||||
from .common import JSON_OPTION
|
||||
from .cli_params import JSON_OPTION
|
||||
|
||||
|
||||
@click.command(name="list")
|
||||
|
||||
@@ -18,14 +18,9 @@ from osxphotos._constants import _PHOTOS_4_VERSION
|
||||
from osxphotos.fileutil import FileUtil
|
||||
from osxphotos.utils import increment_filename, pluralize
|
||||
|
||||
from .cli_params import DB_OPTION, THEME_OPTION, TIMESTAMP_OPTION, VERBOSE_OPTION
|
||||
from .click_rich_echo import rich_click_echo as echo
|
||||
from .common import (
|
||||
DB_OPTION,
|
||||
THEME_OPTION,
|
||||
TIMESTAMP_OPTION,
|
||||
VERBOSE_OPTION,
|
||||
get_photos_db,
|
||||
)
|
||||
from .common import get_photos_db
|
||||
from .help import get_help_msg
|
||||
from .list import _list_libraries
|
||||
from .verbose import verbose_print
|
||||
|
||||
@@ -20,6 +20,7 @@ __all__ = [
|
||||
"BitMathSize",
|
||||
"DateOffset",
|
||||
"DateTimeISO8601",
|
||||
"DeprecatedPath",
|
||||
"ExportDBType",
|
||||
"FunctionCall",
|
||||
"StrpDateTimePattern",
|
||||
@@ -31,6 +32,26 @@ __all__ = [
|
||||
]
|
||||
|
||||
|
||||
class DeprecatedPath(click.Path):
|
||||
"""A click.Path that prints a deprecation warning when used."""
|
||||
|
||||
name = "DEPRECATED_PATH"
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
if "deprecation_warning" in kwargs:
|
||||
self.deprecation_warning = kwargs.pop("deprecation_warning")
|
||||
else:
|
||||
self.deprecation_warning = "This option is deprecated and will be removed in a future version of osxphotos."
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def convert(self, value, param, ctx):
|
||||
click.echo(
|
||||
f"WARNING: {param.name} is deprecated. {self.deprecation_warning}",
|
||||
err=True,
|
||||
)
|
||||
return super().convert(value, param, ctx)
|
||||
|
||||
|
||||
class DateTimeISO8601(click.ParamType):
|
||||
|
||||
name = "DATETIME"
|
||||
|
||||
@@ -6,7 +6,8 @@ import yaml
|
||||
|
||||
import osxphotos
|
||||
|
||||
from .common import DB_ARGUMENT, DB_OPTION, JSON_OPTION, get_photos_db
|
||||
from .cli_params import DB_ARGUMENT, DB_OPTION, JSON_OPTION
|
||||
from .common import get_photos_db
|
||||
from .list import _list_libraries
|
||||
|
||||
|
||||
|
||||
@@ -26,8 +26,9 @@ from osxphotos.rich_utils import add_rich_markup_tag
|
||||
from osxphotos.text_detection import detect_text as detect_text_in_photo
|
||||
from osxphotos.utils import dd_to_dms_str
|
||||
|
||||
from .cli_params import DB_OPTION, THEME_OPTION
|
||||
from .color_themes import get_theme
|
||||
from .common import DB_OPTION, THEME_OPTION, get_photos_db
|
||||
from .common import get_photos_db
|
||||
|
||||
# global that tracks UUID being inspected
|
||||
CURRENT_UUID = None
|
||||
|
||||
@@ -8,7 +8,8 @@ import yaml
|
||||
import osxphotos
|
||||
from osxphotos._constants import _PHOTOS_4_VERSION, _UNKNOWN_PLACE
|
||||
|
||||
from .common import DB_ARGUMENT, DB_OPTION, JSON_OPTION, get_photos_db
|
||||
from .cli_params import DB_ARGUMENT, DB_OPTION, JSON_OPTION
|
||||
from .common import get_photos_db
|
||||
from .list import _list_libraries
|
||||
|
||||
|
||||
|
||||
@@ -11,21 +11,18 @@ 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, load_uuid_from_file
|
||||
from osxphotos.queryoptions import query_options_from_kwargs
|
||||
|
||||
from .color_themes import get_default_theme
|
||||
from .common import (
|
||||
CLI_COLOR_ERROR,
|
||||
CLI_COLOR_WARNING,
|
||||
from .cli_params import (
|
||||
DB_ARGUMENT,
|
||||
DB_OPTION,
|
||||
DELETED_OPTIONS,
|
||||
FIELD_OPTION,
|
||||
JSON_OPTION,
|
||||
OSXPHOTOS_HIDDEN,
|
||||
QUERY_OPTIONS,
|
||||
get_photos_db,
|
||||
)
|
||||
from .color_themes import get_default_theme
|
||||
from .common import CLI_COLOR_ERROR, CLI_COLOR_WARNING, OSXPHOTOS_HIDDEN, get_photos_db
|
||||
from .list import _list_libraries
|
||||
from .print_photo_info import print_photo_fields, print_photo_info
|
||||
from .verbose import get_verbose_console
|
||||
@@ -36,7 +33,6 @@ from .verbose import get_verbose_console
|
||||
@JSON_OPTION
|
||||
@QUERY_OPTIONS
|
||||
@DELETED_OPTIONS
|
||||
|
||||
@click.option(
|
||||
"--add-to-album",
|
||||
metavar="ALBUM",
|
||||
@@ -63,9 +59,6 @@ from .verbose import get_verbose_console
|
||||
"Most useful with --quiet. "
|
||||
"May be repeated to print multiple template strings. ",
|
||||
)
|
||||
@click.option(
|
||||
"--debug", required=False, is_flag=True, default=False, hidden=OSXPHOTOS_HIDDEN
|
||||
)
|
||||
@DB_ARGUMENT
|
||||
@click.pass_obj
|
||||
@click.pass_context
|
||||
@@ -73,94 +66,13 @@ def query(
|
||||
ctx,
|
||||
cli_obj,
|
||||
db,
|
||||
photos_library,
|
||||
add_to_album,
|
||||
added_after,
|
||||
added_before,
|
||||
added_in_last,
|
||||
album,
|
||||
burst,
|
||||
cloudasset,
|
||||
deleted_only,
|
||||
deleted,
|
||||
description,
|
||||
duplicate,
|
||||
edited,
|
||||
exif,
|
||||
external_edit,
|
||||
favorite,
|
||||
field,
|
||||
folder,
|
||||
from_date,
|
||||
from_time,
|
||||
has_comment,
|
||||
has_likes,
|
||||
has_raw,
|
||||
hdr,
|
||||
hidden,
|
||||
ignore_case,
|
||||
in_album,
|
||||
incloud,
|
||||
is_reference,
|
||||
json_,
|
||||
keyword,
|
||||
label,
|
||||
live,
|
||||
location,
|
||||
max_size,
|
||||
min_size,
|
||||
missing,
|
||||
name,
|
||||
no_comment,
|
||||
no_description,
|
||||
no_likes,
|
||||
no_location,
|
||||
no_keyword,
|
||||
no_place,
|
||||
no_title,
|
||||
not_burst,
|
||||
not_cloudasset,
|
||||
not_edited,
|
||||
not_favorite,
|
||||
not_hdr,
|
||||
not_hidden,
|
||||
not_in_album,
|
||||
not_incloud,
|
||||
not_live,
|
||||
not_missing,
|
||||
not_panorama,
|
||||
not_portrait,
|
||||
not_reference,
|
||||
not_screenshot,
|
||||
not_selfie,
|
||||
not_shared,
|
||||
not_slow_mo,
|
||||
not_time_lapse,
|
||||
only_movies,
|
||||
only_photos,
|
||||
panorama,
|
||||
person,
|
||||
place,
|
||||
portrait,
|
||||
print_template,
|
||||
query_eval,
|
||||
query_function,
|
||||
quiet,
|
||||
regex,
|
||||
screenshot,
|
||||
selected,
|
||||
selfie,
|
||||
shared,
|
||||
slow_mo,
|
||||
time_lapse,
|
||||
title,
|
||||
to_date,
|
||||
to_time,
|
||||
uti,
|
||||
uuid_from_file,
|
||||
uuid,
|
||||
year,
|
||||
debug, # handled in cli/__init__.py
|
||||
add_to_album,
|
||||
photos_library,
|
||||
**kwargs,
|
||||
):
|
||||
"""Query the Photos database using 1 or more search options;
|
||||
if more than one different option is provided, they are treated as "AND"
|
||||
@@ -173,95 +85,14 @@ def query(
|
||||
osxphotos query --person "John Doe" --person "Jane Doe" --keyword "vacation"
|
||||
|
||||
will return all photos with either person of ("John Doe" OR "Jane Doe") AND keyword of "vacation"
|
||||
"""
|
||||
|
||||
# if no query terms, show help and return
|
||||
# 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_from_file,
|
||||
uuid,
|
||||
year,
|
||||
]
|
||||
exclusive = [
|
||||
(any(description), no_description),
|
||||
(any(place), no_place),
|
||||
(any(title), no_title),
|
||||
(any(keyword), no_keyword),
|
||||
(burst, not_burst),
|
||||
(cloudasset, not_cloudasset),
|
||||
(deleted, deleted_only),
|
||||
(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),
|
||||
(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),
|
||||
(is_reference, not_reference),
|
||||
]
|
||||
# print help if no non-exclusive term or a double exclusive term is given
|
||||
if any(all(bb) for bb in exclusive) or not any(
|
||||
nonexclusive + [b ^ n for b, n in exclusive]
|
||||
):
|
||||
click.echo("Incompatible query options", err=True)
|
||||
click.echo(ctx.obj.group.commands["query"].get_help(ctx), err=True)
|
||||
return
|
||||
If not query options are provided, all photos in the library will be returned.
|
||||
"""
|
||||
|
||||
# set console for rich_echo to be same as for verbose_
|
||||
set_rich_console(get_verbose_console())
|
||||
set_rich_theme(get_default_theme())
|
||||
|
||||
# actually have something to query
|
||||
# default searches for everything
|
||||
photos = True
|
||||
movies = True
|
||||
if only_movies:
|
||||
photos = False
|
||||
if only_photos:
|
||||
movies = False
|
||||
|
||||
# load UUIDs if necessary and append to any uuids passed with --uuid
|
||||
if uuid_from_file:
|
||||
uuid_list = list(uuid) # Click option is a tuple
|
||||
uuid_list.extend(load_uuid_from_file(uuid_from_file))
|
||||
uuid = tuple(uuid_list)
|
||||
|
||||
# below needed for to make CliRunner work for testing
|
||||
cli_db = cli_obj.db if cli_obj is not None else None
|
||||
db = get_photos_db(*photos_library, db, cli_db)
|
||||
@@ -272,99 +103,21 @@ def query(
|
||||
return
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=db)
|
||||
query_options = QueryOptions(
|
||||
added_after=added_after,
|
||||
added_before=added_before,
|
||||
added_in_last=added_in_last,
|
||||
album=album,
|
||||
burst=burst,
|
||||
cloudasset=cloudasset,
|
||||
deleted_only=deleted_only,
|
||||
deleted=deleted,
|
||||
description=description,
|
||||
duplicate=duplicate,
|
||||
edited=edited,
|
||||
exif=exif,
|
||||
external_edit=external_edit,
|
||||
favorite=favorite,
|
||||
folder=folder,
|
||||
from_date=from_date,
|
||||
from_time=from_time,
|
||||
function=query_function,
|
||||
has_comment=has_comment,
|
||||
has_likes=has_likes,
|
||||
has_raw=has_raw,
|
||||
hdr=hdr,
|
||||
hidden=hidden,
|
||||
ignore_case=ignore_case,
|
||||
in_album=in_album,
|
||||
incloud=incloud,
|
||||
is_reference=is_reference,
|
||||
keyword=keyword,
|
||||
label=label,
|
||||
live=live,
|
||||
location=location,
|
||||
max_size=max_size,
|
||||
min_size=min_size,
|
||||
missing=missing,
|
||||
movies=movies,
|
||||
name=name,
|
||||
no_comment=no_comment,
|
||||
no_description=no_description,
|
||||
no_likes=no_likes,
|
||||
no_location=no_location,
|
||||
no_keyword=no_keyword,
|
||||
no_place=no_place,
|
||||
no_title=no_title,
|
||||
not_burst=not_burst,
|
||||
not_cloudasset=not_cloudasset,
|
||||
not_edited=not_edited,
|
||||
not_favorite=not_favorite,
|
||||
not_hdr=not_hdr,
|
||||
not_hidden=not_hidden,
|
||||
not_in_album=not_in_album,
|
||||
not_incloud=not_incloud,
|
||||
not_live=not_live,
|
||||
not_missing=not_missing,
|
||||
not_panorama=not_panorama,
|
||||
not_portrait=not_portrait,
|
||||
not_reference=not_reference,
|
||||
not_screenshot=not_screenshot,
|
||||
not_selfie=not_selfie,
|
||||
not_shared=not_shared,
|
||||
not_slow_mo=not_slow_mo,
|
||||
not_time_lapse=not_time_lapse,
|
||||
panorama=panorama,
|
||||
person=person,
|
||||
photos=photos,
|
||||
place=place,
|
||||
portrait=portrait,
|
||||
query_eval=query_eval,
|
||||
regex=regex,
|
||||
screenshot=screenshot,
|
||||
selected=selected,
|
||||
selfie=selfie,
|
||||
shared=shared,
|
||||
slow_mo=slow_mo,
|
||||
time_lapse=time_lapse,
|
||||
title=title,
|
||||
to_date=to_date,
|
||||
to_time=to_time,
|
||||
uti=uti,
|
||||
uuid=uuid,
|
||||
year=year,
|
||||
)
|
||||
try:
|
||||
query_options = query_options_from_kwargs(**kwargs)
|
||||
except Exception as e:
|
||||
raise click.BadOptionUsage("query", str(e)) from e
|
||||
|
||||
try:
|
||||
photos = photosdb.query(query_options)
|
||||
except ValueError as e:
|
||||
if "Invalid query_eval CRITERIA:" in str(e):
|
||||
msg = str(e).split(":")[1]
|
||||
raise click.BadOptionUsage(
|
||||
"query_eval", f"Invalid query-eval CRITERIA: {msg}"
|
||||
)
|
||||
else:
|
||||
raise ValueError(e)
|
||||
if "Invalid query_eval CRITERIA:" not in str(e):
|
||||
raise ValueError(e) from e
|
||||
|
||||
msg = str(e).split(":")[1]
|
||||
raise click.BadOptionUsage(
|
||||
"query_eval", f"Invalid query-eval CRITERIA: {msg}"
|
||||
) from e
|
||||
|
||||
# below needed for to make CliRunner work for testing
|
||||
cli_json = cli_obj.json if cli_obj is not None else None
|
||||
|
||||
@@ -23,13 +23,8 @@ from osxphotos.queryoptions import (
|
||||
query_options_from_kwargs,
|
||||
)
|
||||
|
||||
from .common import (
|
||||
DB_ARGUMENT,
|
||||
DB_OPTION,
|
||||
DELETED_OPTIONS,
|
||||
QUERY_OPTIONS,
|
||||
get_photos_db,
|
||||
)
|
||||
from .cli_params import DB_ARGUMENT, DB_OPTION, DELETED_OPTIONS, QUERY_OPTIONS
|
||||
from .common import get_photos_db
|
||||
|
||||
|
||||
@click.command(name="repl")
|
||||
|
||||
@@ -12,13 +12,8 @@ from rich.syntax import Syntax
|
||||
|
||||
import osxphotos
|
||||
|
||||
from .common import (
|
||||
DB_OPTION,
|
||||
OSXPHOTOS_SNAPSHOT_DIR,
|
||||
TIMESTAMP_OPTION,
|
||||
VERBOSE_OPTION,
|
||||
get_photos_db,
|
||||
)
|
||||
from .cli_params import DB_OPTION, TIMESTAMP_OPTION, VERBOSE_OPTION
|
||||
from .common import OSXPHOTOS_SNAPSHOT_DIR, get_photos_db
|
||||
from .verbose import verbose_print
|
||||
|
||||
|
||||
@@ -36,7 +31,7 @@ def snap(ctx, cli_obj, db):
|
||||
Works only on Photos library versions since Catalina (10.15) or newer.
|
||||
"""
|
||||
|
||||
db = get_photos_db(db, cli_obj.db)
|
||||
db = get_photos_db(db, cli_obj.db if cli_obj else None)
|
||||
db_path = pathlib.Path(db)
|
||||
if db_path.is_file():
|
||||
# assume it's the sqlite file
|
||||
@@ -127,7 +122,7 @@ def diff(ctx, cli_obj, db, raw_output, style, db2, verbose_flag, timestamp):
|
||||
ctx.exit(2)
|
||||
verbose(f"sqldiff found at '{sqldiff}'")
|
||||
|
||||
db = get_photos_db(db, cli_obj.db)
|
||||
db = get_photos_db(db, cli_obj.db if cli_obj else None)
|
||||
db_path = pathlib.Path(db)
|
||||
if db_path.is_file():
|
||||
# assume it's the sqlite file
|
||||
|
||||
@@ -24,15 +24,15 @@ from osxphotos.queryoptions import (
|
||||
from osxphotos.sqlitekvstore import SQLiteKVStore
|
||||
from osxphotos.utils import pluralize
|
||||
|
||||
from .click_rich_echo import rich_click_echo as echo
|
||||
from .click_rich_echo import rich_echo_error as echo_error
|
||||
from .common import (
|
||||
from .cli_params import (
|
||||
DB_OPTION,
|
||||
QUERY_OPTIONS,
|
||||
THEME_OPTION,
|
||||
TIMESTAMP_OPTION,
|
||||
VERBOSE_OPTION,
|
||||
)
|
||||
from .click_rich_echo import rich_click_echo as echo
|
||||
from .click_rich_echo import rich_echo_error as echo_error
|
||||
from .param_types import TemplateString
|
||||
from .report_writer import sync_report_writer_factory
|
||||
from .rich_progress import rich_progress
|
||||
@@ -640,7 +640,7 @@ def print_import_summary(results: SyncResults):
|
||||
)
|
||||
@VERBOSE_OPTION
|
||||
@TIMESTAMP_OPTION
|
||||
@QUERY_OPTIONS
|
||||
@QUERY_OPTIONS(exclude=["--shared", "--not-shared"])
|
||||
@DB_OPTION
|
||||
@THEME_OPTION
|
||||
@click.pass_obj
|
||||
@@ -721,14 +721,6 @@ def sync(
|
||||
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"):
|
||||
echo_error(
|
||||
"[warning]--shared cannot be used with --import/--export "
|
||||
"as photos in shared iCloud albums cannot be updated; "
|
||||
"--shared will be ignored[/]"
|
||||
)
|
||||
kwargs["shared"] = False
|
||||
kwargs["not_shared"] = True
|
||||
|
||||
set_ = parse_set_merge(set_)
|
||||
|
||||
@@ -25,10 +25,10 @@ from osxphotos.photosalbum import PhotosAlbumPhotoScript
|
||||
from osxphotos.phototz import PhotoTimeZone, PhotoTimeZoneUpdater
|
||||
from osxphotos.utils import noop, pluralize
|
||||
|
||||
from .cli_params import THEME_OPTION, TIMESTAMP_OPTION, VERBOSE_OPTION
|
||||
from .click_rich_echo import rich_click_echo as echo
|
||||
from .click_rich_echo import rich_echo_error as echo_error
|
||||
from .color_themes import get_theme
|
||||
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 (
|
||||
|
||||
@@ -56,21 +56,22 @@ def noop(*args, **kwargs):
|
||||
pass
|
||||
|
||||
|
||||
def verbose(*args, level: int = 1, **kwargs):
|
||||
def verbose(*args, level: int = 1):
|
||||
"""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
|
||||
|
||||
"""
|
||||
|
||||
# Notes:
|
||||
# Normally you should use verbose_print() to get the verbose function instead of calling this directly
|
||||
# This is here so that verbose can be directly imported and used in other modules without calling verbose_print()
|
||||
# Use of verbose_print() will set the verbose function so that calling verbose() will work as expected
|
||||
global __verbose_function
|
||||
if __verbose_function is None:
|
||||
return
|
||||
__verbose_function(*args, level=level, **kwargs)
|
||||
__verbose_function(*args, level=level)
|
||||
|
||||
|
||||
def set_verbose_level(level: int):
|
||||
|
||||
@@ -7,6 +7,7 @@ from dataclasses import asdict, dataclass
|
||||
from typing import Iterable, List, Optional, Tuple
|
||||
|
||||
import bitmath
|
||||
import click
|
||||
|
||||
__all__ = ["QueryOptions", "query_options_from_kwargs", "IncompatibleQueryOptions"]
|
||||
|
||||
@@ -249,6 +250,8 @@ def query_options_from_kwargs(**kwargs) -> QueryOptions:
|
||||
("slow_mo", "not_slow_mo"),
|
||||
("time_lapse", "not_time_lapse"),
|
||||
("deleted", "not_deleted"),
|
||||
("deleted", "deleted_only"),
|
||||
("deleted_only", "not_deleted"),
|
||||
]
|
||||
# TODO: add option to validate requiring at least one query arg
|
||||
for arg, not_arg in exclusive:
|
||||
@@ -256,7 +259,7 @@ def query_options_from_kwargs(**kwargs) -> QueryOptions:
|
||||
arg = arg.replace("_", "-")
|
||||
not_arg = not_arg.replace("_", "-")
|
||||
raise IncompatibleQueryOptions(
|
||||
f"--{arg} and --{not_arg} are mutually exclusive"
|
||||
f"Incompatible query options: --{arg} and --{not_arg} are mutually exclusive"
|
||||
)
|
||||
|
||||
# some options like title can be specified multiple times
|
||||
@@ -276,17 +279,18 @@ def query_options_from_kwargs(**kwargs) -> QueryOptions:
|
||||
include_movies = False
|
||||
|
||||
# load UUIDs if necessary and append to any uuids passed with --uuid
|
||||
uuid = None
|
||||
uuids = list(kwargs.get("uuid", [])) # Click option is a tuple
|
||||
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)
|
||||
uuids.extend(load_uuid_from_file(uuid_from_file))
|
||||
uuids = tuple(uuids)
|
||||
|
||||
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
|
||||
query_dict["uuid"] = uuids
|
||||
query_dict["function"] = kwargs.get("query_function")
|
||||
|
||||
return QueryOptions(**query_dict)
|
||||
|
||||
|
||||
|
||||
@@ -176,6 +176,17 @@ class SQLiteKVStore:
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
@property
|
||||
def path(self) -> str:
|
||||
"""Return path to the database"""
|
||||
return self._dbpath
|
||||
|
||||
def wipe(self):
|
||||
"""Wipe the database"""
|
||||
self.connection().execute("DELETE FROM data;")
|
||||
self.connection().commit()
|
||||
self.vacuum()
|
||||
|
||||
def vacuum(self):
|
||||
"""Vacuum the database, ref: https://www.sqlite.org/matrix/lang_vacuum.html"""
|
||||
self.connection().execute("VACUUM;")
|
||||
|
||||
Reference in New Issue
Block a user