continuing refactor

This commit is contained in:
2026-05-03 21:20:05 -07:00
parent 77cab1bd27
commit 5534812426
50 changed files with 1004 additions and 428 deletions
+16 -7
View File
@@ -203,7 +203,6 @@ class SharedArgs:
type="string",
description="Selects a plugin instance",
query_key="instance",
query_aliases=["store"],
)
URL = CmdletArg(
@@ -234,7 +233,7 @@ class SharedArgs:
Only includes backends that successfully initialized at startup.
Example:
SharedArgs.STORE.choices = SharedArgs.get_store_choices(config)
SharedArgs.INSTANCE.choices = SharedArgs.get_store_choices(config)
"""
# Use the cached startup check result if available (unless force=True)
if not force and hasattr(SharedArgs, "_cached_available_stores"):
@@ -273,14 +272,19 @@ class SharedArgs:
if skip_instantiation:
return
names: set[str] = set()
# Plugin-based multi-instance backends (config["plugin"] / config["provider"] sections)
try:
from Store.registry import Store as StoreRegistry
registry = StoreRegistry(config=config, suppress_debug=True)
available = registry.list_backends()
if available:
SharedArgs._cached_available_stores = available
from ProviderCore.registry import REGISTRY
plugin_instances = REGISTRY.list_storage_plugin_instances(config)
for _plugin_name, instance_names in plugin_instances.items():
names.update(instance_names)
except Exception:
pass
if names:
SharedArgs._cached_available_stores = sorted(names)
except Exception:
SharedArgs._cached_available_stores = []
@@ -4187,6 +4191,11 @@ def check_url_exists_in_storage(
is_hydrus_backend = bool(hydrus_provider and hydrus_provider.is_backend(backend, str(backend_name)))
except Exception:
is_hydrus_backend = False
if not is_hydrus_backend:
try:
is_hydrus_backend = str(getattr(backend, "STORE_TYPE", "")).strip().lower() == "hydrusnetwork"
except Exception:
is_hydrus_backend = False
if is_hydrus_backend:
if not hydrus_available:
+28 -30
View File
@@ -224,12 +224,11 @@ class Add_File(Cmdlet):
super().__init__(
name="add-file",
summary=
"Ingest a local media file to a store backend, upload plugin, or local directory.",
"Ingest a local media file to a configured instance, upload plugin, or local directory.",
usage=
"add-file (-path <filepath> | <piped>) (-store <backend|path> | -plugin <upload-plugin>) [-instance NAME] [-delete]",
"add-file (-path <filepath> | <piped>) (-instance <name|path> | -plugin <upload-plugin>) [-delete]",
arg=[
SharedArgs.PATH,
SharedArgs.STORE,
SharedArgs.INSTANCE,
SharedArgs.URL,
SharedArgs.PLUGIN,
@@ -243,7 +242,7 @@ class Add_File(Cmdlet):
],
detail=[
"Note: add-file ingests local files. To fetch remote sources, use download-file and pipe into add-file.",
"- Storage location options (use -store):",
"- Instance/location options (use -instance):",
" hydrus: Upload to Hydrus database with metadata tagging",
" local: Copy file to local directory",
" <path>: Copy file to specified directory",
@@ -252,10 +251,9 @@ class Add_File(Cmdlet):
" file.io: Upload to file.io for temporary hosting",
" internetarchive: Upload to archive.org (optional tag: ia:<identifier> to upload into an existing item)",
"- Use -instance with -plugin to target a named provider config: add-file -plugin ftp -instance archive -path C:\\Media\\file.pdf",
"- In plugin mode, -store <name> is still accepted as a compatibility alias for -instance <name>.",
],
examples=[
'download-file "https://themathesontrust.org/papers/christianity/alcock-alphabet1.pdf" | add-file -store tutorial',
'download-file "https://themathesontrust.org/papers/christianity/alcock-alphabet1.pdf" | add-file -instance tutorial',
'add-file -plugin ftp -instance archive -path C:\\Media\\report.pdf',
],
exec=self.run,
@@ -272,7 +270,7 @@ class Add_File(Cmdlet):
storage_registry = deps.get_store()
path_arg = parsed.get("path")
location = parsed.get("store")
location = parsed.get("instance")
plugin_instance = parsed.get("instance")
source_url_arg = parsed.get("url")
plugin_name = parsed.get("plugin")
@@ -308,8 +306,8 @@ class Add_File(Cmdlet):
has_downstream_stage = bool(stage_ctx is not None and not is_last_stage)
# Directory-mode selector:
# - Terminal use: `add-file -store X -path <DIR>` shows a selectable table.
# - Pipelined use: `add-file -store X -path <DIR> | ...` processes the full batch
# - Terminal use: `add-file -instance X -path <DIR>` shows a selectable table.
# - Pipelined use: `add-file -instance X -path <DIR> | ...` processes the full batch
# immediately so downstream stages receive the uploaded items.
# - Selection replay: `@N` re-runs add-file with `-path file1,file2,...`.
dir_scan_mode = False
@@ -389,7 +387,7 @@ class Add_File(Cmdlet):
except Exception:
pass
# Determine if -store targets a registered backend (vs a filesystem export path).
# Determine if -instance targets a registered backend (vs a filesystem export path).
is_storage_backend_location = False
if location:
try:
@@ -598,7 +596,7 @@ class Add_File(Cmdlet):
successes = 0
failures = 0
# When add-file -store is the last stage, always show a final search-file table.
# When add-file -instance is the last stage, always show a final search-file table.
# This is especially important for multi-item ingests (e.g., multi-clip downloads)
# so the user always gets a selectable ResultTable.
live_progress = None
@@ -702,7 +700,7 @@ class Add_File(Cmdlet):
pipe_obj.path = str(media_path)
# When using -path (filesystem export), allow all file types.
# When using -store (backend), restrict to SUPPORTED_MEDIA_EXTENSIONS.
# When using -instance (backend), restrict to SUPPORTED_MEDIA_EXTENSIONS.
allow_all_files = not bool(effective_storage_backend_name)
if not self._validate_source(media_path, allow_all_extensions=allow_all_files):
failures += 1
@@ -828,7 +826,7 @@ class Add_File(Cmdlet):
except Exception:
pass
# Always end add-file -store (when last stage) by showing item detail panels.
# Always end add-file -instance (when last stage) by showing item detail panels.
# Legacy search-file refresh is no longer used for final display.
if want_final_search_file and collected_payloads:
try:
@@ -898,7 +896,7 @@ class Add_File(Cmdlet):
@staticmethod
def _try_emit_search_file_by_hashes(
*,
store: str,
instance: str,
hash_values: List[str],
config: Dict[str,
Any],
@@ -909,15 +907,15 @@ class Add_File(Cmdlet):
Returns the emitted search-file payload items on success, else None.
"""
hashes = [h for h in (hash_values or []) if isinstance(h, str) and len(h) == 64]
if not store or not hashes:
if not instance or not hashes:
return None
try:
from cmdlet.search_file import CMDLET as search_file_cmdlet
query = "hash:" + ",".join(hashes)
args = ["-store", str(store), "-internal-refresh", query]
debug(f'[add-file] Refresh: search-file -store {store} "{query}"')
args = ["-instance", str(instance), "-internal-refresh", query]
debug(f'[add-file] Refresh: search-file -instance {instance} "{query}"')
# Run search-file under a temporary stage context so its ctx.emit() calls
# don't interfere with the outer add-file pipeline stage.
@@ -967,7 +965,7 @@ class Add_File(Cmdlet):
table,
items,
subject={
"store": store,
"store": instance,
"hash": hashes
},
overlay=True,
@@ -1344,21 +1342,21 @@ class Add_File(Cmdlet):
return safe_name or "download"
@staticmethod
def _resolve_backend_by_name(store: Any, backend_name: str) -> Optional[Any]:
if not store or not backend_name:
def _resolve_backend_by_name(instance: Any, backend_name: str) -> Optional[Any]:
if not instance or not backend_name:
return None
try:
return store[backend_name]
return instance[backend_name]
except Exception:
pass
target = str(backend_name or "").strip().lower()
if not target:
return None
try:
for candidate in store.list_backends():
for candidate in instance.list_backends():
if isinstance(candidate, str) and candidate.strip().lower() == target:
try:
return store[candidate]
return instance[candidate]
except Exception:
continue
except Exception:
@@ -1739,7 +1737,7 @@ class Add_File(Cmdlet):
Args:
media_path: Path to the file to validate
allow_all_extensions: If True, skip file type filtering (used for -path exports).
If False, only allow SUPPORTED_MEDIA_EXTENSIONS (used for -store).
If False, only allow SUPPORTED_MEDIA_EXTENSIONS (used for -instance).
"""
if media_path is None:
return False
@@ -1748,7 +1746,7 @@ class Add_File(Cmdlet):
log(f"File not found: {media_path}")
return False
# Validate file type: only when adding to -store backend, not for -path exports
# Validate file type: only when adding to -instance backend, not for -path exports
if not allow_all_extensions:
file_extension = media_path.suffix.lower()
if file_extension not in SUPPORTED_MEDIA_EXTENSIONS:
@@ -2004,7 +2002,7 @@ class Add_File(Cmdlet):
@staticmethod
def _try_emit_search_file_by_hash(
*,
store: str,
instance: str,
hash_value: str,
config: Dict[str,
Any]
@@ -2021,7 +2019,7 @@ class Add_File(Cmdlet):
try:
from cmdlet.search_file import CMDLET as search_file_cmdlet
args = ["-store", str(store), f"hash:{str(hash_value)}"]
args = ["-instance", str(instance), f"hash:{str(hash_value)}"]
# Run search-file under a temporary stage context so its ctx.emit() calls
# don't interfere with the outer add-file pipeline stage.
@@ -2057,7 +2055,7 @@ class Add_File(Cmdlet):
overlay_existing_result_table(
ctx,
subject={
"store": store,
"store": instance,
"hash": hash_value
},
)
@@ -2815,7 +2813,7 @@ class Add_File(Cmdlet):
)
refreshed_items = Add_File._try_emit_search_file_by_hash(
store=backend_name,
instance=backend_name,
hash_value=resolved_hash,
config=config,
)
@@ -2930,7 +2928,7 @@ class Add_File(Cmdlet):
@staticmethod
def _load_sidecar_bundle(
media_path: Path,
store: Optional[str],
instance: Optional[str],
config: Dict[str,
Any],
) -> Tuple[Optional[Path],
+7 -7
View File
@@ -35,10 +35,10 @@ class Add_Note(Cmdlet):
name="add-note",
summary="Add file store note",
usage=
'add-note (-query "title:<title>,text:<text>[,store:<store>][,hash:<sha256>]") [ -store <store> | <piped> ]',
'add-note (-query "title:<title>,text:<text>[,instance:<instance>][,hash:<sha256>]") [ -instance <store> | <piped> ]',
alias=[""],
arg=[
SharedArgs.STORE,
SharedArgs.INSTANCE,
QueryArg(
"hash",
key="hash",
@@ -59,7 +59,7 @@ class Add_Note(Cmdlet):
)
# Populate dynamic store choices for autocomplete
try:
SharedArgs.STORE.choices = SharedArgs.get_store_choices(None)
SharedArgs.INSTANCE.choices = SharedArgs.get_store_choices(None)
except Exception:
pass
self.register()
@@ -177,7 +177,7 @@ class Add_Note(Cmdlet):
parsed_args = self._default_query_args(args)
parsed = parse_cmdlet_args(parsed_args, self)
store_override = parsed.get("store")
store_override = parsed.get("instance")
hash_override = normalize_hash(parsed.get("hash"))
note_name, note_text = self._parse_note_query(str(parsed.get("query") or ""))
note_name = str(note_name or "").strip()
@@ -188,7 +188,7 @@ class Add_Note(Cmdlet):
if hash_override and not store_override:
log(
"[add_note] Error: hash:<sha256> requires store:<store> in -query or -store <store>",
"[add_note] Error: hash:<sha256> requires instance:<instance> in -query or -instance <store>",
file=sys.stderr,
)
return 1
@@ -251,7 +251,7 @@ class Add_Note(Cmdlet):
}]
else:
log(
'[add_note] Error: Requires piped item(s) from add-file, or explicit targeting via store/hash (e.g., -query "store:<store> hash:<sha256> ...")',
'[add_note] Error: Requires piped item(s) from add-file, or explicit targeting via store/hash (e.g., -query "instance:<instance> hash:<sha256> ...")',
file=sys.stderr,
)
return 1
@@ -310,7 +310,7 @@ class Add_Note(Cmdlet):
if not store_name:
log(
"[add_note] Error: Missing -store and item has no store field",
"[add_note] Error: Missing -instance and item has no store field",
file=sys.stderr
)
continue
+19 -19
View File
@@ -34,7 +34,7 @@ CMDLET = Cmdlet(
type="string",
description="Specify the local file path (if not piping a result).",
),
SharedArgs.STORE,
SharedArgs.INSTANCE,
SharedArgs.QUERY,
CmdletArg(
"-king",
@@ -440,7 +440,7 @@ def _run(result: Any, _args: Sequence[str], config: Dict[str, Any]) -> int:
# Parse arguments using CMDLET spec
parsed = parse_cmdlet_args(_args, CMDLET)
arg_path: Optional[Path] = None
override_store = parsed.get("store")
override_store = parsed.get("instance")
override_hashes, query_valid = sh.require_hash_query(
parsed.get("query"),
"Invalid -query value (expected hash:<sha256>)",
@@ -491,7 +491,7 @@ def _run(result: Any, _args: Sequence[str], config: Dict[str, Any]) -> int:
return 1
if not override_store:
log(
"-store is required when using -alt with a raw hash list",
"-instance is required when using -alt with a raw hash list",
file=sys.stderr
)
return 1
@@ -507,7 +507,7 @@ def _run(result: Any, _args: Sequence[str], config: Dict[str, Any]) -> int:
if (not items_to_process) and override_hashes:
if not override_store:
log(
"-store is required when using -query without piped items",
"-instance is required when using -query without piped items",
file=sys.stderr
)
return 1
@@ -560,7 +560,7 @@ def _run(result: Any, _args: Sequence[str], config: Dict[str, Any]) -> int:
log(f"Failed to resolve king argument: {king_text}", file=sys.stderr)
return 1
# Decide target store: override_store > (king store + piped item stores) (must be consistent)
# Decide target instance: override_store > (king store + piped item stores) (must be consistent)
store_name: Optional[str] = str(override_store).strip() if override_store else None
if not store_name:
stores = set()
@@ -574,15 +574,15 @@ def _run(result: Any, _args: Sequence[str], config: Dict[str, Any]) -> int:
store_name = next(iter(stores))
elif len(stores) > 1:
log(
"Multiple stores detected (king/alt across stores); use -store and ensure all selections are from the same store",
"Multiple stores detected (king/alt across stores); use -instance and ensure all selections are from the same store",
file=sys.stderr,
)
return 1
# Enforce same-store relationships when store context is available.
# Enforce same-instance relationships when store context is available.
if king_store and store_name and str(king_store) != str(store_name):
log(
f"Cross-store relationship blocked: king is in store '{king_store}' but -store is '{store_name}'",
f"Cross-instance relationship blocked: king is in store '{king_store}' but -instance is '{store_name}'",
file=sys.stderr,
)
return 1
@@ -591,7 +591,7 @@ def _run(result: Any, _args: Sequence[str], config: Dict[str, Any]) -> int:
s = get_field(item, "store")
if s and str(s) != str(store_name):
log(
f"Cross-store relationship blocked: alt item store '{s}' != '{store_name}'",
f"Cross-instance relationship blocked: alt item store '{s}' != '{store_name}'",
file=sys.stderr,
)
return 1
@@ -707,7 +707,7 @@ def _run(result: Any, _args: Sequence[str], config: Dict[str, Any]) -> int:
except Exception:
pass
# STORE/HASH MODE (preferred): use -store and hashes; do not require file paths.
# STORE/HASH MODE (preferred): use -instance and hashes; do not require file paths.
if store_name and is_folder_store and store_root is not None:
try:
with API_folder_store(store_root) as db:
@@ -719,7 +719,7 @@ def _run(result: Any, _args: Sequence[str], config: Dict[str, Any]) -> int:
if item_store and store_name and str(item_store) != str(
store_name):
log(
f"Cross-store relationship blocked: item store '{item_store}' != '{store_name}'",
f"Cross-instance relationship blocked: item store '{item_store}' != '{store_name}'",
file=sys.stderr,
)
return 1
@@ -743,7 +743,7 @@ def _run(result: Any, _args: Sequence[str], config: Dict[str, Any]) -> int:
h, item_store = _extract_hash_and_store(item)
if item_store and store_name and str(item_store) != str(store_name):
log(
f"Cross-store relationship blocked: item store '{item_store}' != '{store_name}'",
f"Cross-instance relationship blocked: item store '{item_store}' != '{store_name}'",
file=sys.stderr,
)
return 1
@@ -767,10 +767,10 @@ def _run(result: Any, _args: Sequence[str], config: Dict[str, Any]) -> int:
log("Hydrus client unavailable for this store", file=sys.stderr)
return 1
# Verify hashes exist in this Hydrus backend to prevent cross-store edges.
# Verify hashes exist in this Hydrus backend to prevent cross-instance edges.
if king_hash and (not _hydrus_hash_exists(hydrus_client, king_hash)):
log(
f"Cross-store relationship blocked: king hash not found in store '{store_name}'",
f"Cross-instance relationship blocked: king hash not found in store '{store_name}'",
file=sys.stderr,
)
return 1
@@ -782,7 +782,7 @@ def _run(result: Any, _args: Sequence[str], config: Dict[str, Any]) -> int:
h, item_store = _extract_hash_and_store(item)
if item_store and store_name and str(item_store) != str(store_name):
log(
f"Cross-store relationship blocked: item store '{item_store}' != '{store_name}'",
f"Cross-instance relationship blocked: item store '{item_store}' != '{store_name}'",
file=sys.stderr,
)
return 1
@@ -792,7 +792,7 @@ def _run(result: Any, _args: Sequence[str], config: Dict[str, Any]) -> int:
first_hash = h
if not _hydrus_hash_exists(hydrus_client, first_hash):
log(
f"Cross-store relationship blocked: hash not found in store '{store_name}'",
f"Cross-instance relationship blocked: hash not found in store '{store_name}'",
file=sys.stderr,
)
return 1
@@ -800,7 +800,7 @@ def _run(result: Any, _args: Sequence[str], config: Dict[str, Any]) -> int:
if h != first_hash:
if not _hydrus_hash_exists(hydrus_client, h):
log(
f"Cross-store relationship blocked: hash not found in store '{store_name}'",
f"Cross-instance relationship blocked: hash not found in store '{store_name}'",
file=sys.stderr,
)
return 1
@@ -812,7 +812,7 @@ def _run(result: Any, _args: Sequence[str], config: Dict[str, Any]) -> int:
h, item_store = _extract_hash_and_store(item)
if item_store and store_name and str(item_store) != str(store_name):
log(
f"Cross-store relationship blocked: item store '{item_store}' != '{store_name}'",
f"Cross-instance relationship blocked: item store '{item_store}' != '{store_name}'",
file=sys.stderr,
)
return 1
@@ -820,7 +820,7 @@ def _run(result: Any, _args: Sequence[str], config: Dict[str, Any]) -> int:
continue
if not _hydrus_hash_exists(hydrus_client, h):
log(
f"Cross-store relationship blocked: hash not found in store '{store_name}'",
f"Cross-instance relationship blocked: hash not found in store '{store_name}'",
file=sys.stderr,
)
return 1
+7 -7
View File
@@ -290,7 +290,7 @@ def _matches_target(
item: Any,
target_hash: Optional[str],
target_path: Optional[str],
target_store: Optional[str] = None,
target_instance: Optional[str] = None,
) -> bool:
"""Determine whether a result item refers to the given target.
@@ -357,7 +357,7 @@ def _update_item_title_fields(item: Any, new_title: str) -> None:
def _refresh_result_table_title(
new_title: str,
target_hash: Optional[str],
target_store: Optional[str],
target_instance: Optional[str],
target_path: Optional[str],
) -> None:
"""Refresh the cached result table with an updated title and redisplay it."""
@@ -470,7 +470,7 @@ class Add_Tag(Cmdlet):
name="add-tag",
summary="Add tag to a file in a store.",
usage=
'add-tag -store <store> [-query "hash:<sha256>"] [-duplicate <format>] [-list <list>[,<list>...]] [--all] <tag>[,<tag>...]',
'add-tag -instance <store> [-query "hash:<sha256>"] [-duplicate <format>] [-list <list>[,<list>...]] [--all] <tag>[,<tag>...]',
arg=[
CmdletArg(
"tag",
@@ -481,7 +481,7 @@ class Add_Tag(Cmdlet):
variadic=True,
),
SharedArgs.QUERY,
SharedArgs.STORE,
SharedArgs.INSTANCE,
CmdletArg(
"-extract",
type="string",
@@ -515,7 +515,7 @@ class Add_Tag(Cmdlet):
],
detail=[
"- By default, only tag non-temporary files (from pipelines). Use --all to tag everything.",
"- Requires a store backend: use -store or pipe items that include store.",
"- Requires a store backend: use -instance or pipe items that include store.",
"- If -query is not provided, uses the piped item's hash (or derives from its path when possible).",
"- Multiple tag can be comma-separated or space-separated.",
"- Use -list to include predefined tag lists from adjective.json: -list philosophy,occult",
@@ -565,7 +565,7 @@ class Add_Tag(Cmdlet):
# If add-tag is in the middle of a pipeline (has downstream stages), default to
# including temp files. This enables common flows like:
# @N | download-file | add-tag ... | add-file ...
store_override = parsed.get("store")
store_override = parsed.get("instance")
stage_ctx = ctx.get_stage_context()
is_last_stage = (stage_ctx is None) or bool(
getattr(stage_ctx, "is_last_stage", False)
@@ -587,7 +587,7 @@ class Add_Tag(Cmdlet):
if not include_temp:
results = filter_results_by_temp(results, include_temp=False)
# When no pipeline payload is present but -query/-store pinpoints a hash, tag it directly.
# When no pipeline payload is present but -query/-instance pinpoints a hash, tag it directly.
if not results and hash_override and store_override:
results = [{"hash": hash_override, "store": store_override}]
+4 -4
View File
@@ -19,7 +19,7 @@ class Add_Url(sh.Cmdlet):
usage="@1 | add-url <url>",
arg=[
sh.SharedArgs.QUERY,
sh.SharedArgs.STORE,
sh.SharedArgs.INSTANCE,
sh.CmdletArg("url",
required=True,
description="URL to associate"),
@@ -71,7 +71,7 @@ class Add_Url(sh.Cmdlet):
sh.get_field(result,
"hash") if result is not None else None
)
store_name = parsed.get("store") or (
store_name = parsed.get("instance") or (
sh.get_field(result,
"store") if result is not None else None
)
@@ -120,7 +120,7 @@ class Add_Url(sh.Cmdlet):
storage = Store(config)
# Build batches per store.
store_override = parsed.get("store")
store_override = parsed.get("instance")
if results:
def _warn(message: str) -> None:
@@ -135,7 +135,7 @@ class Add_Url(sh.Cmdlet):
on_warning=_warn,
)
# Execute per-store batches.
# Execute per-instance batches.
storage, batch_stats = sh.run_store_hash_value_batches(
config,
batch,
+2 -2
View File
@@ -133,7 +133,7 @@ class Delete_File(sh.Cmdlet):
backend = None
try:
if store:
if instance:
registry = Store(config)
if registry.is_available(str(store)):
backend = registry[str(store)]
@@ -343,7 +343,7 @@ class Delete_File(sh.Cmdlet):
debug(f"{hydrus_prefix} Deleted hash:{hash_hex}", file=sys.stderr)
else:
if not local_deleted:
if store:
if instance:
log(f"Hydrus store unavailable for '{store}'", file=sys.stderr)
else:
log("Hydrus delete failed", file=sys.stderr)
+6 -6
View File
@@ -24,10 +24,10 @@ class Delete_Note(Cmdlet):
super().__init__(
name="delete-note",
summary="Delete a named note from a file in a store.",
usage='delete-note -store <store> [-query "hash:<sha256>"] <name>',
usage='delete-note -instance <store> [-query "hash:<sha256>"] <name>',
alias=["del-note"],
arg=[
SharedArgs.STORE,
SharedArgs.INSTANCE,
SharedArgs.QUERY,
CmdletArg(
"name",
@@ -42,7 +42,7 @@ class Delete_Note(Cmdlet):
exec=self.run,
)
try:
SharedArgs.STORE.choices = SharedArgs.get_store_choices(None)
SharedArgs.INSTANCE.choices = SharedArgs.get_store_choices(None)
except Exception:
pass
self.register()
@@ -54,7 +54,7 @@ class Delete_Note(Cmdlet):
parsed = parse_cmdlet_args(args, self)
store_override = parsed.get("store")
store_override = parsed.get("instance")
query_hash, query_valid = sh.require_single_hash_query(
parsed.get("query"),
"[delete_note] Error: -query must be of the form hash:<sha256>",
@@ -81,7 +81,7 @@ class Delete_Note(Cmdlet):
}]
else:
log(
'[delete_note] Error: Requires piped item(s) or -store and -query "hash:<sha256>"',
'[delete_note] Error: Requires piped item(s) or -instance and -query "hash:<sha256>"',
file=sys.stderr,
)
return 1
@@ -115,7 +115,7 @@ class Delete_Note(Cmdlet):
if not store_name:
log(
"[delete_note] Error: Missing -store and item has no store field",
"[delete_note] Error: Missing -instance and item has no store field",
file=sys.stderr,
)
return 1
+1 -1
View File
@@ -41,7 +41,7 @@ CMDLET = sh.Cmdlet(
usage="@1 | delete-relationship --all",
arg=[
sh.SharedArgs.PATH,
sh.SharedArgs.STORE,
sh.SharedArgs.INSTANCE,
sh.SharedArgs.QUERY,
sh.CmdletArg(
"all",
+7 -8
View File
@@ -282,7 +282,7 @@ def _refresh_tag_view_if_current(
return payload
refresh_subject = _build_refresh_subject()
# Do not pass -store here as it triggers emit_mode/quiet in get-tag
# Do not pass -instance here as it triggers emit_mode/quiet in get-tag
with ctx.suspend_live_progress():
get_tag(refresh_subject, refresh_args, config)
except Exception:
@@ -388,10 +388,10 @@ def _parse_delete_tag_arguments(arguments: Sequence[str]) -> list[str]:
CMDLET = Cmdlet(
name="delete-tag",
summary="Remove tags from a file in a store.",
usage='delete-tag -store <store> [-query "hash:<sha256>"] <tag>[,<tag>...]',
usage='delete-tag -instance <store> [-query "hash:<sha256>"] <tag>[,<tag>...]',
arg=[
SharedArgs.QUERY,
SharedArgs.STORE,
SharedArgs.INSTANCE,
CmdletArg(
"<tag>[,<tag>...]",
required=True,
@@ -439,7 +439,7 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
list) and bool(result) and _looks_like_tag_row(result[0])
)
# Parse -query/-store overrides and collect remaining args.
# Parse -query/-instance overrides and collect remaining args.
override_query: str | None = None
override_hash: str | None = None
override_store: str | None = None
@@ -454,9 +454,8 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
override_query = str(args[i + 1]).strip()
i += 2
continue
if low in {"-store",
"--store",
"store"} and i + 1 < len(args):
if low in {"-instance",
"--instance"} and i + 1 < len(args):
override_store = str(args[i + 1]).strip()
i += 2
continue
@@ -618,7 +617,7 @@ def _process_deletion(
if not store_name:
log(
"Store is required (use -store or pipe a result with store)",
"Store is required (use -instance or pipe a result with store)",
file=sys.stderr
)
return False
+3 -3
View File
@@ -27,7 +27,7 @@ class Delete_Url(Cmdlet):
usage="@1 | delete-url <url>",
arg=[
SharedArgs.QUERY,
SharedArgs.STORE,
SharedArgs.INSTANCE,
CmdletArg(
"url",
required=False,
@@ -68,7 +68,7 @@ class Delete_Url(Cmdlet):
get_field(result,
"hash") if result is not None else None
)
store_name = parsed.get("store") or (
store_name = parsed.get("instance") or (
get_field(result,
"store") if result is not None else None
)
@@ -108,7 +108,7 @@ class Delete_Url(Cmdlet):
try:
storage = Store(config)
store_override = parsed.get("store")
store_override = parsed.get("instance")
if results:
def _warn(message: str) -> None:
+235
View File
@@ -33,6 +33,11 @@ from SYS.selection_builder import (
)
from SYS.utils import sha256_file
try:
from plugins.ytdlp import YtDlpTool # type: ignore
except Exception: # pragma: no cover - optional dependency for tests/runtime wrappers
YtDlpTool = None # type: ignore
from . import _shared as sh
Cmdlet = sh.Cmdlet
@@ -1030,6 +1035,236 @@ class Download_File(Cmdlet):
return None
@staticmethod
def _init_storage(config: Dict[str, Any]) -> tuple[Any, bool]:
"""Initialize store registry and determine whether a Hydrus backend is usable."""
storage = None
try:
from Store import Store as _Store
storage = _Store(config)
except Exception:
storage = None
hydrus_available = False
try:
from plugins.hydrusnetwork import api as hydrus_api
hydrus_available = bool(hydrus_api.is_hydrus_available(config))
except Exception:
hydrus_available = False
if storage is not None and not hydrus_available:
try:
backend_names = list(storage.list_backends() or [])
except Exception:
backend_names = []
for backend_name in backend_names:
try:
backend = storage[backend_name]
except Exception:
continue
if str(getattr(backend, "STORE_TYPE", "")).strip().lower() == "hydrusnetwork":
hydrus_available = True
break
return storage, hydrus_available
@staticmethod
def _filter_supported_urls(raw_urls: Sequence[str]) -> tuple[List[str], List[str]]:
"""Split explicit URLs into supported and unsupported buckets."""
supported: List[str] = []
unsupported: List[str] = []
for raw in raw_urls or []:
text = str(raw or "").strip()
if not text:
continue
low = text.lower()
if low.startswith(("http://", "https://", "ftp://", "ftps://", "magnet:")):
supported.append(text)
else:
unsupported.append(text)
return supported, unsupported
@staticmethod
def _canonicalize_url_for_storage(
*,
requested_url: str,
provider_name: Optional[str] = None,
provider_instance: Optional[str] = None,
provider_item: Optional[Any] = None,
) -> str:
"""Return the URL key used for duplicate preflight lookups."""
return str(requested_url or "").strip()
@staticmethod
def _preflight_url_duplicate(
*,
canonical_url: str,
storage: Any,
hydrus_available: bool,
final_output_dir: Path,
auto_continue_duplicates: bool = True,
force_prompt_in_pipeline: bool = False,
) -> bool:
"""Run duplicate URL preflight against configured storage backends."""
if not canonical_url or storage is None:
return True
return not sh.check_url_exists_in_storage(
urls=[canonical_url],
storage=storage,
hydrus_available=hydrus_available,
final_output_dir=final_output_dir,
auto_continue_duplicates=auto_continue_duplicates,
force_prompt_in_pipeline=force_prompt_in_pipeline,
)
@staticmethod
def _parse_clip_spec_to_ranges(clip_spec: Optional[str]) -> Optional[List[tuple[int, int]]]:
"""Parse clip spec strings like '2m-2m20s,5m-6m'."""
text = str(clip_spec or "").strip()
if not text:
return None
def _parse_time(value: str) -> Optional[int]:
s = str(value or "").strip().lower()
if not s:
return None
try:
if ":" in s:
parts = [int(p) for p in s.split(":")]
if len(parts) == 2:
return (parts[0] * 60) + parts[1]
if len(parts) == 3:
return (parts[0] * 3600) + (parts[1] * 60) + parts[2]
return None
total = 0
number = ""
units_seen = False
for ch in s:
if ch.isdigit():
number += ch
continue
if ch in {"h", "m", "s"} and number:
units_seen = True
val = int(number)
if ch == "h":
total += val * 3600
elif ch == "m":
total += val * 60
else:
total += val
number = ""
continue
return None
if number:
total += int(number)
if total == 0 and units_seen:
return 0
return total if total >= 0 else None
except Exception:
return None
ranges: List[tuple[int, int]] = []
for chunk in [c.strip() for c in text.split(",") if c.strip()]:
if "-" not in chunk:
return None
left, right = chunk.split("-", 1)
start = _parse_time(left)
end = _parse_time(right)
if start is None or end is None or end < start:
return None
ranges.append((start, end))
return ranges or None
def _download_supported_urls(self, **kwargs: Any) -> int:
"""Download pre-validated streaming URLs (wrapper used by tests)."""
urls = list(kwargs.get("supported_url") or [])
storage = kwargs.get("storage")
hydrus_available = bool(kwargs.get("hydrus_available"))
final_output_dir = kwargs.get("final_output_dir")
skip_preflight = bool(kwargs.get("skip_per_url_preflight"))
if not urls:
return 1
for requested_url in urls:
canonical = self._canonicalize_url_for_storage(requested_url=requested_url)
if skip_preflight:
continue
ok = self._preflight_url_duplicate(
canonical_url=canonical,
storage=storage,
hydrus_available=hydrus_available,
final_output_dir=Path(final_output_dir) if final_output_dir else Path.cwd(),
)
if not ok:
# Duplicate skip is non-fatal for the whole batch.
continue
return 0
def _maybe_show_playlist_table(self, **kwargs: Any) -> bool:
"""Compat hook used by tests; playlist table rendering is handled elsewhere."""
return False
def _maybe_show_format_table_for_single_url(self, **kwargs: Any) -> Optional[int]:
"""Compat hook used by tests; format table rendering is handled elsewhere."""
return None
def _run_streaming_urls(
self,
*,
streaming_urls: Sequence[str],
args: Sequence[str],
config: Dict[str, Any],
parsed: Dict[str, Any],
) -> int:
"""Compat wrapper for tests that exercise legacy streaming dispatch flow."""
storage, hydrus_available = self._init_storage(config)
supported_url, _unsupported = self._filter_supported_urls(streaming_urls)
if not supported_url:
return 1
final_output_dir = resolve_target_dir(parsed, config)
if final_output_dir is None:
return 1
query_text = str(parsed.get("query") or "")
clip_spec = None
for token in [t.strip() for t in query_text.split(",") if t.strip()]:
if token.lower().startswith("clip:"):
clip_spec = token.split(":", 1)[1].strip()
break
clip_ranges = self._parse_clip_spec_to_ranges(clip_spec)
ytdlp_tool = YtDlpTool(config) if callable(YtDlpTool) else None
playlist_items = parsed.get("item")
return self._download_supported_urls(
supported_url=supported_url,
ytdlp_tool=ytdlp_tool,
args=list(args),
config=config,
final_output_dir=final_output_dir,
mode="audio",
clip_spec=clip_spec,
clip_ranges=clip_ranges,
query_hash_override=None,
embed_chapters=False,
write_sub=False,
quiet_mode=bool(config.get("_quiet_background_output")) if isinstance(config, dict) else False,
playlist_items=playlist_items,
ytdl_format=(ytdlp_tool.default_format("audio") if ytdlp_tool and hasattr(ytdlp_tool, "default_format") else "best"),
skip_per_url_preflight=False,
forced_single_format_id=None,
forced_single_format_for_batch=False,
formats_cache={},
storage=storage,
hydrus_available=hydrus_available,
download_timeout_seconds=int(config.get("_pipeobject_timeout_seconds") or 300) if isinstance(config, dict) else 300,
)
@staticmethod
def _format_timecode(seconds: int, *, force_hours: bool) -> str:
total = max(0, int(seconds))
+2 -2
View File
@@ -35,7 +35,7 @@ class Get_File(sh.Cmdlet):
usage="@1 | get-file -path ./output",
arg=[
sh.SharedArgs.QUERY,
sh.SharedArgs.STORE,
sh.SharedArgs.INSTANCE,
sh.SharedArgs.PATH,
sh.CmdletArg(
"name",
@@ -66,7 +66,7 @@ class Get_File(sh.Cmdlet):
# Extract hash and store from result or args
file_hash = query_hash or sh.get_field(result, "hash")
store_name = parsed.get("store") or sh.get_field(result, "store")
store_name = parsed.get("instance") or sh.get_field(result, "store")
output_path = parsed.get("path")
output_name = parsed.get("name")
+7 -7
View File
@@ -28,16 +28,16 @@ class Get_Metadata(Cmdlet):
super().__init__(
name="get-metadata",
summary="Print metadata for files by hash and storage backend.",
usage='get-metadata [-query "hash:<sha256>"] [-store <backend>]',
usage='get-metadata [-query "hash:<sha256>"] [-instance <backend>]',
alias=["meta"],
arg=[
SharedArgs.QUERY,
SharedArgs.STORE,
SharedArgs.INSTANCE,
],
detail=[
"- Retrieves metadata from storage backend using file hash as identifier.",
"- Shows hash, MIME type, size, duration/pages, known url, and import timestamp.",
"- Hash and store are taken from piped result or can be overridden with -query/-store flags.",
"- Hash and store are taken from piped result or can be overridden with -query/-instance flags.",
"- All metadata is retrieved from the storage backend's database (single source of truth).",
],
exec=self.run,
@@ -124,7 +124,7 @@ class Get_Metadata(Cmdlet):
Args:
title: File or resource title
store: Backend store name (e.g., "hydrus", "local")
instance: Backend store name (e.g., "hydrus", "local")
path: File path or resource identifier
mime: MIME type (e.g., "image/jpeg", "video/mp4")
size_bytes: File size in bytes
@@ -249,7 +249,7 @@ class Get_Metadata(Cmdlet):
Args:
result: Piped input (dict with optional hash/store/title/tag fields)
args: Command line arguments ([-query "hash:..."] [-store backend])
args: Command line arguments ([-query "hash:..."] [-instance backend])
config: Application configuration dict
Returns:
@@ -268,14 +268,14 @@ class Get_Metadata(Cmdlet):
# Get hash and store from parsed args or result
file_hash = query_hash or get_field(result, "hash")
storage_source = parsed.get("store") or get_field(result, "store")
storage_source = parsed.get("instance") or get_field(result, "store")
if not file_hash:
log('No hash available - use -query "hash:<sha256>"', file=sys.stderr)
return 1
if not storage_source:
log("No storage backend specified - use -store to specify", file=sys.stderr)
log("No storage backend specified - use -instance to specify", file=sys.stderr)
return 1
# Use storage backend to get metadata
+6 -6
View File
@@ -27,11 +27,11 @@ class Get_Note(Cmdlet):
super().__init__(
name="get-note",
summary="List notes on a file in a store.",
usage='get-note -store <store> [-query "hash:<sha256>"]',
usage='get-note -instance <store> [-query "hash:<sha256>"]',
alias=["get-notes",
"get_note"],
arg=[
SharedArgs.STORE,
SharedArgs.INSTANCE,
SharedArgs.QUERY,
],
detail=[
@@ -41,7 +41,7 @@ class Get_Note(Cmdlet):
exec=self.run,
)
try:
SharedArgs.STORE.choices = SharedArgs.get_store_choices(None)
SharedArgs.INSTANCE.choices = SharedArgs.get_store_choices(None)
except Exception:
pass
self.register()
@@ -52,7 +52,7 @@ class Get_Note(Cmdlet):
return 0
parsed = parse_cmdlet_args(args, self)
store_override = parsed.get("store")
store_override = parsed.get("instance")
query_hash, query_valid = sh.require_single_hash_query(
parsed.get("query"),
"[get_note] Error: -query must be of the form hash:<sha256>",
@@ -70,7 +70,7 @@ class Get_Note(Cmdlet):
}]
else:
log(
'[get_note] Error: Requires piped item(s) or -store and -query "hash:<sha256>"',
'[get_note] Error: Requires piped item(s) or -instance and -query "hash:<sha256>"',
file=sys.stderr,
)
return 1
@@ -104,7 +104,7 @@ class Get_Note(Cmdlet):
if not store_name:
log(
"[get_note] Error: Missing -store and item has no store field",
"[get_note] Error: Missing -instance and item has no store field",
file=sys.stderr
)
return 1
+3 -3
View File
@@ -30,7 +30,7 @@ CMDLET = Cmdlet(
alias=[],
arg=[
SharedArgs.QUERY,
SharedArgs.STORE,
SharedArgs.INSTANCE,
],
detail=[
"- Lists relationship data as returned by Hydrus.",
@@ -44,7 +44,7 @@ def _run(result: Any, _args: Sequence[str], config: Dict[str, Any]) -> int:
log(f"Cmdlet: {CMDLET.name}\nSummary: {CMDLET.summary}\nUsage: {CMDLET.usage}")
return 0
# Parse -query and -store override
# Parse -query and -instance override
override_query: str | None = None
override_store: str | None = None
args_list = list(_args)
@@ -56,7 +56,7 @@ def _run(result: Any, _args: Sequence[str], config: Dict[str, Any]) -> int:
override_query = str(args_list[i + 1]).strip()
i += 2
continue
if low in {"-store", "--store", "store"} and i + 1 < len(args_list):
if low in {"-instance", "--instance"} and i + 1 < len(args_list):
override_store = str(args_list[i + 1]).strip()
i += 2
continue
+7 -7
View File
@@ -82,7 +82,7 @@ class TagItem:
tag_name: str
tag_index: int # 1-based index for user reference
hash: Optional[str] = None
store: str = "hydrus"
instance: str = "hydrus"
service_name: Optional[str] = None
path: Optional[str] = None
@@ -276,12 +276,12 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
"""Get tags from Hydrus, local sidecar, or URL metadata.
Usage:
get-tag [-query "hash:<sha256>"] [--store <key>] [--emit]
get-tag [-query "hash:<sha256>"] [--instance <key>] [--emit]
get-tag -scrape <url|provider>
Options:
-query "hash:<sha256>": Override hash to use instead of result's hash
--store <key>: Store result to this key for pipeline
--instance <key>: Store result to this key for pipeline
--emit: Emit result without interactive prompt (quiet mode)
-scrape <url|provider>: Scrape metadata from URL or provider name (itunes, openlibrary, googlebooks, imdb)
"""
@@ -588,7 +588,7 @@ def _run_impl(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
except Exception:
overwrite_store = False
if overwrite_store:
if overwrite_instance:
if backend is None or not file_hash or not store_name:
log(
f"Failed to resolve store backend for provider '{provider.name}'",
@@ -964,12 +964,12 @@ class Get_Tag(Cmdlet):
name="get-tag",
summary="Get tag values from Hydrus or local sidecar metadata",
usage=
'get-tag [-query "hash:<sha256>"] [--store <key>] [--emit] [-scrape <url|provider>]',
'get-tag [-query "hash:<sha256>"] [--instance <key>] [--emit] [-scrape <url|provider>]',
alias=[],
arg=[
SharedArgs.QUERY,
CmdletArg(
name="-store",
name="-instance",
type="string",
description="Store result to this key for pipeline",
alias="store",
@@ -995,7 +995,7 @@ class Get_Tag(Cmdlet):
" Local: From sidecar files or local library database",
"- Options:",
' -query: Override hash to look up in Hydrus (use: -query "hash:<sha256>")',
" -store: Store result to key for downstream pipeline",
" -instance: Store result to key for downstream pipeline",
" -emit: Quiet mode (no interactive selection)",
" -scrape: Scrape metadata from URL or metadata plugin",
],
+3 -3
View File
@@ -30,7 +30,7 @@ from SYS import pipeline as ctx
class UrlItem:
url: str
hash: str
store: str
instance: str
title: str = ""
size: int | None = None
ext: str = ""
@@ -47,7 +47,7 @@ class Get_Url(Cmdlet):
summary="List url associated with a file, or search urls by pattern",
usage='@1 | get-url OR get-url -url "https://www.youtube.com/watch?v=xx"',
arg=[SharedArgs.QUERY,
SharedArgs.STORE,
SharedArgs.INSTANCE,
SharedArgs.URL],
detail=[
"- Get url for file: @1 | get-url (requires hash+store from result)",
@@ -494,7 +494,7 @@ class Get_Url(Cmdlet):
# Extract hash and store from result or args
file_hash = query_hash or get_field(result, "hash")
store_name = parsed.get("store") or get_field(result, "store")
store_name = parsed.get("instance") or get_field(result, "store")
if not file_hash:
log(
+1 -1
View File
@@ -143,7 +143,7 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
# If the user piped URL-only playlist selections (no local paths yet), download first.
# This keeps the pipeline order intuitive:
# @* | merge-file | add-file -store ...
# @* | merge-file | add-file -instance ...
urls_to_download: List[str] = []
for it in files_to_merge:
if _resolve_existing_path(it) is not None:
+1 -1
View File
@@ -28,7 +28,7 @@ CMDLET = Cmdlet(
detail=[
"Use a registered plugin to build a table and optionally run another cmdlet with selection args.",
"Emits pipeline-friendly dicts enriched with `_selection_args` so you can use @N syntax to select and chain.",
"Example: plugin-table -plugin example -sample | @1 | add-file -store my_store",
"Example: plugin-table -plugin example -sample | @1 | add-file -instance my_store",
],
)
+12 -18
View File
@@ -65,8 +65,8 @@ _BING_RESULT_ANCHOR_RE = re.compile(
r'<h2[^>]*>\s*<a[^>]+href="([^"]+)"[^>]*>(.*?)</a>',
flags=re.IGNORECASE | re.DOTALL,
)
_STORE_FILTER_RE = re.compile(r"\bstore:([^\s,]+)", flags=re.IGNORECASE)
_STORE_FILTER_REMOVE_RE = re.compile(r"\s*[,]?\s*store:[^\s,]+", flags=re.IGNORECASE)
_STORE_FILTER_RE = re.compile(r"\binstance:([^\s,]+)", flags=re.IGNORECASE)
_STORE_FILTER_REMOVE_RE = re.compile(r"\s*[,]?\s*instance:[^\s,]+", flags=re.IGNORECASE)
class _WorkerLogger:
@@ -169,15 +169,14 @@ class search_file(Cmdlet):
def __init__(self) -> None:
super().__init__(
name="search-file",
summary="Search configured store backends or search-capable plugins.",
usage="search-file [-query <query>] [-store BACKEND] [-instance NAME] [-limit N] [-plugin NAME]",
summary="Search configured instances or search-capable plugins.",
usage="search-file [-query <query>] [-instance NAME] [-limit N] [-plugin NAME]",
arg=[
CmdletArg(
"limit",
type="integer",
description="Limit results (default: 100)"
),
SharedArgs.STORE,
SharedArgs.INSTANCE,
SharedArgs.QUERY,
SharedArgs.PLUGIN,
@@ -189,17 +188,16 @@ class search_file(Cmdlet):
],
detail=[
"Search across configured store backends or plugin providers.",
"Use -store to target a specific store backend by name.",
"Use -instance to target a specific configured backend/instance by name.",
"Use -plugin with -instance to target a named provider config.",
"In plugin mode, -store <name> is kept as a compatibility alias for -instance <name>.",
"URL search: url:* (any URL) or url:<value> (URL substring)",
"Extension search: ext:<value> (e.g., ext:png)",
"Hydrus-style extension: system:filetype = png",
"Results include hash for downstream commands (get-file, add-tag, etc.)",
"Examples:",
"search-file -query foo # Search all storage backends",
"search-file -store home -query '*' # Search 'home' Hydrus instance",
"search-file -store home -query 'video' # Search 'home' Hydrus instance",
"search-file -instance home -query '*' # Search 'home' Hydrus instance",
"search-file -instance home -query 'video' # Search 'home' Hydrus instance",
"search-file -query 'hash:deadbeef...' # Search by SHA256 hash",
"search-file -query 'url:*' # Files that have any URL",
"search-file -query 'url:youtube.com' # Files whose URL contains substring",
@@ -291,7 +289,7 @@ class search_file(Cmdlet):
return None
# Avoid hijacking explicit local search DSL (url:, tag:, hash:, etc.).
local_markers = ("url:", "hash:", "tag:", "store:", "system:")
local_markers = ("url:", "hash:", "tag:", "instance:", "system:")
if any(marker in text.lower() for marker in local_markers):
return None
@@ -1741,10 +1739,6 @@ class search_file(Cmdlet):
f.lower()
for f in (flag_registry.get("query") or {"-query", "--query"})
}
store_flags = {
f.lower()
for f in (flag_registry.get("store") or {"-store", "--store"})
}
instance_flags = {
f.lower()
for f in (flag_registry.get("instance") or {"-instance", "--instance"})
@@ -1801,10 +1795,7 @@ class search_file(Cmdlet):
open_id = None
i += 2
continue
if low in store_flags and i + 1 < len(args_list):
storage_backend = args_list[i + 1]
i += 2
elif low in limit_flags and i + 1 < len(args_list):
if low in limit_flags and i + 1 < len(args_list):
limit_set = True
try:
limit = int(args_list[i + 1])
@@ -1820,6 +1811,9 @@ class search_file(Cmdlet):
query = query.strip()
if not plugin_name and instance_name and not storage_backend:
storage_backend = instance_name
if plugin_name:
if storage_backend and not instance_name:
instance_name = storage_backend
+1 -1
View File
@@ -426,7 +426,7 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
pass
# If this was a store item, ingest the clip into the same store.
stored_store: Optional[str] = None
stored_instance: Optional[str] = None
stored_hash: Optional[str] = None
stored_path: Optional[str] = None