This commit is contained in:
nose
2025-12-03 15:18:57 -08:00
parent 89aa24961b
commit 5e4df11dbf
12 changed files with 1953 additions and 346 deletions

View File

@@ -68,8 +68,7 @@ def _load_sidecar_bundle(media_path: Path, origin: Optional[str] = None, config:
if db_root:
try:
db = LocalLibraryDB(Path(db_root))
try:
with LocalLibraryDB(Path(db_root)) as db:
# Get tags and metadata from database
tags = db.get_tags(media_path) or []
metadata = db.get_metadata(media_path) or {}
@@ -79,8 +78,6 @@ def _load_sidecar_bundle(media_path: Path, origin: Optional[str] = None, config:
if tags or known_urls or file_hash:
debug(f"Found metadata in local database: {len(tags)} tag(s), {len(known_urls)} URL(s)")
return None, file_hash, tags, known_urls
finally:
db.close()
except Exception as exc:
log(f"⚠️ Could not query local database: {exc}", file=sys.stderr)
except Exception:

View File

@@ -14,22 +14,25 @@ from . import register
import models
import pipeline as ctx
from helper import hydrus as hydrus_wrapper
from ._shared import Cmdlet, CmdletArg, parse_cmdlet_args
from ._shared import Cmdlet, CmdletArg, parse_cmdlet_args, normalize_result_input
from helper.local_library import read_sidecar, find_sidecar
CMDLET = Cmdlet(
name="add-relationship",
summary="Associate file relationships (king/alt/related) in Hydrus based on relationship tags in sidecar.",
usage="add-relationship OR add-relationship -path <file>",
usage="@1-3 | add-relationship -king @4 OR add-relationship -path <file> OR @1,@2,@3 | add-relationship",
args=[
CmdletArg("path", type="string", description="Specify the local file path (if not piping a result)."),
CmdletArg("-king", type="string", description="Explicitly set the king hash/file for relationships (e.g., -king @4 or -king hash)"),
CmdletArg("-type", type="string", description="Relationship type for piped items (default: 'alt', options: 'king', 'alt', 'related')"),
],
details=[
"- Reads relationship tags from sidecar (format: 'relationship: hash(king)<HASH>,hash(alt)<HASH>,hash(related)<HASH>')",
"- Calls Hydrus API to associate the hashes as relationships",
"- Mode 1: Pipe multiple items, first becomes king, rest become alts (default)",
"- Mode 2: Use -king to explicitly set which item/hash is the king: @1-3 | add-relationship -king @4",
"- Mode 3: Read relationships from sidecar (format: 'relationship: hash(king)<HASH>,hash(alt)<HASH>...')",
"- Supports three relationship types: king (primary), alt (alternative), related (other versions)",
"- Works with piped file results or -path argument for direct invocation",
"- When using -king, all piped items become the specified relationship type to the king",
],
)
@@ -67,6 +70,81 @@ def _extract_relationships_from_tag(tag_value: str) -> Dict[str, list[str]]:
return result
def _resolve_king_reference(king_arg: str) -> Optional[str]:
"""Resolve a king reference like '@4' to its actual hash or path.
Supports:
- Direct hash: '0123456789abcdef...' (64 chars)
- Selection reference: '@4' (resolves from pipeline context)
Returns:
- For Hydrus items: normalized hash
- For local storage items: file path
- None if not found
"""
if not king_arg:
return None
# Check if it's already a valid hash
normalized = _normalise_hash_hex(king_arg)
if normalized:
return normalized
# Try to resolve as @N selection from pipeline context
if king_arg.startswith('@'):
try:
# Get the result items from the pipeline context
from pipeline import get_last_result_items
items = get_last_result_items()
if not items:
log(f"Cannot resolve {king_arg}: no search results in context", file=sys.stderr)
return None
# Parse @N to get the index (1-based)
index_str = king_arg[1:] # Remove '@'
index = int(index_str) - 1 # Convert to 0-based
if 0 <= index < len(items):
item = items[index]
# Try to extract hash from the item (could be dict or object)
item_hash = None
if isinstance(item, dict):
# Dictionary: try common hash field names
item_hash = item.get('hash_hex') or item.get('hash') or item.get('file_hash')
else:
# Object: use getattr
item_hash = getattr(item, 'hash_hex', None) or getattr(item, 'hash', None)
if item_hash:
normalized = _normalise_hash_hex(item_hash)
if normalized:
return normalized
# If no hash, try to get file path (for local storage)
file_path = None
if isinstance(item, dict):
# Dictionary: try common path field names
file_path = item.get('file_path') or item.get('path') or item.get('target')
else:
# Object: use getattr
file_path = getattr(item, 'file_path', None) or getattr(item, 'path', None) or getattr(item, 'target', None)
if file_path:
return str(file_path)
log(f"Item {king_arg} has no hash or path information", file=sys.stderr)
return None
else:
log(f"Index {king_arg} out of range", file=sys.stderr)
return None
except (ValueError, IndexError) as e:
log(f"Cannot resolve {king_arg}: {e}", file=sys.stderr)
return None
return None
@register(["add-relationship", "add-rel"]) # primary name and alias
def _run(result: Any, _args: Sequence[str], config: Dict[str, Any]) -> int:
"""Associate file relationships in Hydrus.
@@ -88,6 +166,9 @@ 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
king_arg = parsed.get("king") # New: explicit king argument
rel_type = parsed.get("type", "alt") # New: relationship type (default: alt)
if parsed:
# Get the first arg value (e.g., -path)
first_arg_name = CMDLET.get("args", [{}])[0].get("name") if CMDLET.get("args") else None
@@ -98,62 +179,160 @@ def _run(result: Any, _args: Sequence[str], config: Dict[str, Any]) -> int:
except Exception:
arg_path = Path(str(arg_value))
# Get Hydrus client
try:
client = hydrus_wrapper.get_client(config)
except Exception as exc:
log(f"Hydrus client unavailable: {exc}", file=sys.stderr)
# Handle @N selection which creates a list
# Use normalize_result_input to handle both single items and lists
items_to_process = normalize_result_input(result)
if not items_to_process and not arg_path:
log("No items provided to add-relationship (no piped result and no -path)", file=sys.stderr)
return 1
if client is None:
log("Hydrus client unavailable", file=sys.stderr)
return 1
# If no items from pipeline, just process the -path arg
if not items_to_process and arg_path:
items_to_process = [{"file_path": arg_path}]
# Handle @N selection which creates a list - extract the first item
if isinstance(result, list) and len(result) > 0:
result = result[0]
# Check if we're in pipeline mode (have a hash) or file mode
file_hash = getattr(result, "hash_hex", None)
# Import local storage utilities
from helper.local_library import LocalLibrarySearchOptimizer
from config import get_local_storage_path
# PIPELINE MODE: Track relationships across multiple items
if file_hash:
file_hash = _normalise_hash_hex(file_hash)
if not file_hash:
log("Invalid file hash format", file=sys.stderr)
return 1
# Load or initialize king hash from pipeline context
local_storage_path = get_local_storage_path(config) if config else None
# Check if any items have Hydrus hashes (file_hash or hash_hex fields)
has_hydrus_hashes = any(
(isinstance(item, dict) and (item.get('hash_hex') or item.get('hash')))
or (hasattr(item, 'hash_hex') or hasattr(item, 'hash'))
for item in items_to_process
)
# Only try to initialize Hydrus if we actually have Hydrus hashes to work with
hydrus_client = None
if has_hydrus_hashes:
try:
king_hash = ctx.load_value("relationship_king")
except Exception:
king_hash = None
# If this is the first item, make it the king
if not king_hash:
try:
ctx.store_value("relationship_king", file_hash)
log(f"Established king hash: {file_hash}", file=sys.stderr)
return 0 # First item just becomes the king, no relationships yet
except Exception:
pass
# If we already have a king and this is a different hash, link them
if king_hash and king_hash != file_hash:
try:
client.set_relationship(file_hash, king_hash, "alt")
log(
f"[add-relationship] Set alt relationship: {file_hash} <-> {king_hash}",
file=sys.stderr
)
return 0
except Exception as exc:
log(f"Failed to set relationship: {exc}", file=sys.stderr)
return 1
return 0
hydrus_client = hydrus_wrapper.get_client(config)
except Exception as exc:
log(f"Hydrus unavailable, will use local storage: {exc}", file=sys.stderr)
# FILE MODE: Read relationships from sidecar
# Use local storage if it's available and either Hydrus is not available or items are local files
use_local_storage = local_storage_path and (not has_hydrus_hashes or (arg_path and arg_path.exists()))
# Resolve the king reference once (if provided)
king_hash = None
if king_arg:
# Resolve the king reference (could be @4 or a direct hash)
king_hash = _resolve_king_reference(king_arg)
if not king_hash:
log(f"Failed to resolve king argument: {king_arg}", file=sys.stderr)
return 1
log(f"Using king hash: {king_hash}", file=sys.stderr)
# Process each item in the list
for item_idx, item in enumerate(items_to_process):
# Extract hash and path from current item
file_hash = None
file_path_from_result = None
if isinstance(item, dict):
file_hash = item.get("hash_hex") or item.get("hash")
file_path_from_result = item.get("file_path") or item.get("path") or item.get("target")
else:
file_hash = getattr(item, "hash_hex", None) or getattr(item, "hash", None)
file_path_from_result = getattr(item, "file_path", None) or getattr(item, "path", None)
# PIPELINE MODE with Hydrus: Track relationships using hash
if file_hash and hydrus_client:
file_hash = _normalise_hash_hex(file_hash)
if not file_hash:
log("Invalid file hash format", file=sys.stderr)
return 1
# If explicit -king provided, use it
if king_hash:
try:
hydrus_client.set_relationship(file_hash, king_hash, rel_type)
log(
f"[add-relationship] Set {rel_type} relationship: {file_hash} <-> {king_hash}",
file=sys.stderr
)
except Exception as exc:
log(f"Failed to set relationship: {exc}", file=sys.stderr)
return 1
else:
# Original behavior: no explicit king, first becomes king, rest become alts
try:
existing_king = ctx.load_value("relationship_king")
except Exception:
existing_king = None
# If this is the first item, make it the king
if not existing_king:
try:
ctx.store_value("relationship_king", file_hash)
log(f"Established king hash: {file_hash}", file=sys.stderr)
continue # Move to next item
except Exception:
pass
# If we already have a king and this is a different hash, link them
if existing_king and existing_king != file_hash:
try:
hydrus_client.set_relationship(file_hash, existing_king, rel_type)
log(
f"[add-relationship] Set {rel_type} relationship: {file_hash} <-> {existing_king}",
file=sys.stderr
)
except Exception as exc:
log(f"Failed to set relationship: {exc}", file=sys.stderr)
return 1
# LOCAL STORAGE MODE: Handle relationships for local files
elif use_local_storage and file_path_from_result:
try:
file_path_obj = Path(str(file_path_from_result))
if not file_path_obj.exists():
log(f"File not found: {file_path_obj}", file=sys.stderr)
return 1
if king_hash:
# king_hash is a file path from _resolve_king_reference (or a Hydrus hash)
king_file_path = Path(str(king_hash)) if king_hash else None
if king_file_path and king_file_path.exists():
with LocalLibrarySearchOptimizer(local_storage_path) as db:
db.set_relationship(file_path_obj, king_file_path, rel_type)
log(f"Set {rel_type} relationship: {file_path_obj.name} -> {king_file_path.name}", file=sys.stderr)
else:
log(f"King file not found or invalid: {king_hash}", file=sys.stderr)
return 1
else:
# Original behavior: first becomes king, rest become alts
try:
king_path = ctx.load_value("relationship_king_path")
except Exception:
king_path = None
if not king_path:
try:
ctx.store_value("relationship_king_path", str(file_path_obj))
log(f"Established king file: {file_path_obj.name}", file=sys.stderr)
continue # Move to next item
except Exception:
pass
if king_path and king_path != str(file_path_obj):
try:
with LocalLibrarySearchOptimizer(local_storage_path) as db:
db.set_relationship(file_path_obj, Path(king_path), rel_type)
log(f"Set {rel_type} relationship: {file_path_obj.name} -> {Path(king_path).name}", file=sys.stderr)
except Exception as exc:
log(f"Failed to set relationship: {exc}", file=sys.stderr)
return 1
except Exception as exc:
log(f"Local storage error: {exc}", file=sys.stderr)
return 1
return 0
# FILE MODE: Read relationships from sidecar (legacy mode - for -path arg only)
log("Note: Use piping mode for easier relationships. Example: 1,2,3 | add-relationship", file=sys.stderr)
# Resolve media path from -path arg or result target
@@ -235,7 +414,7 @@ def _run(result: Any, _args: Sequence[str], config: Dict[str, Any]) -> int:
continue
try:
client.set_relationship(file_hash, related_hash, rel_type)
hydrus_client.set_relationship(file_hash, related_hash, rel_type)
log(
f"[add-relationship] Set {rel_type} relationship: "
f"{file_hash} <-> {related_hash}",

View File

@@ -0,0 +1,168 @@
"""Delete file relationships."""
from __future__ import annotations
from typing import Any, Dict, Optional, Sequence
import json
from pathlib import Path
import sys
from helper.logger import log
import pipeline as ctx
from ._shared import Cmdlet, CmdletArg, parse_cmdlet_args, normalize_result_input
from helper.local_library import LocalLibrarySearchOptimizer
from config import get_local_storage_path
def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
"""Delete relationships from files.
Args:
result: Input result(s) from previous cmdlet
args: Command arguments
config: CLI configuration
Returns:
Exit code (0 = success)
"""
try:
# Parse arguments
parsed_args = parse_cmdlet_args(args, CMDLET)
delete_all_flag = parsed_args.get("all", False)
rel_type_filter = parsed_args.get("type")
# Get storage path
local_storage_path = get_local_storage_path(config)
if not local_storage_path:
log("Local storage path not configured", file=sys.stderr)
return 1
# Normalize input
results = normalize_result_input(result)
if not results:
log("No results to process", file=sys.stderr)
return 1
deleted_count = 0
for single_result in results:
try:
# Get file path from result
file_path_from_result = None
if isinstance(single_result, dict):
file_path_from_result = (
single_result.get("file_path") or
single_result.get("path") or
single_result.get("target")
)
else:
file_path_from_result = (
getattr(single_result, "file_path", None) or
getattr(single_result, "path", None) or
getattr(single_result, "target", None) or
str(single_result)
)
if not file_path_from_result:
log("Could not extract file path from result", file=sys.stderr)
return 1
file_path_obj = Path(str(file_path_from_result))
if not file_path_obj.exists():
log(f"File not found: {file_path_obj}", file=sys.stderr)
return 1
with LocalLibrarySearchOptimizer(local_storage_path) as db:
file_id = db.db.get_file_id(file_path_obj)
if not file_id:
log(f"File not in database: {file_path_obj.name}", file=sys.stderr)
continue
# Get current relationships
cursor = db.db.connection.cursor()
cursor.execute("""
SELECT relationships FROM metadata WHERE file_id = ?
""", (file_id,))
row = cursor.fetchone()
if not row:
log(f"No relationships found for: {file_path_obj.name}", file=sys.stderr)
continue
relationships_str = row[0]
if not relationships_str:
log(f"No relationships found for: {file_path_obj.name}", file=sys.stderr)
continue
try:
relationships = json.loads(relationships_str)
except json.JSONDecodeError:
log(f"Invalid relationship data for: {file_path_obj.name}", file=sys.stderr)
continue
if not isinstance(relationships, dict):
relationships = {}
# Determine what to delete
if delete_all_flag:
# Delete all relationships
deleted_types = list(relationships.keys())
relationships = {}
log(f"Deleted all relationships ({len(deleted_types)} types) from: {file_path_obj.name}", file=sys.stderr)
elif rel_type_filter:
# Delete specific type
if rel_type_filter in relationships:
deleted_count_for_type = len(relationships[rel_type_filter])
del relationships[rel_type_filter]
log(f"Deleted {deleted_count_for_type} {rel_type_filter} relationship(s) from: {file_path_obj.name}", file=sys.stderr)
else:
log(f"No {rel_type_filter} relationships found for: {file_path_obj.name}", file=sys.stderr)
continue
else:
log("Specify --all to delete all relationships or -type <type> to delete specific type", file=sys.stderr)
return 1
# Save updated relationships
cursor.execute("""
INSERT INTO metadata (file_id, relationships)
VALUES (?, ?)
ON CONFLICT(file_id) DO UPDATE SET
relationships = excluded.relationships,
time_modified = CURRENT_TIMESTAMP
""", (file_id, json.dumps(relationships) if relationships else None))
db.db.connection.commit()
deleted_count += 1
except Exception as exc:
log(f"Error deleting relationship: {exc}", file=sys.stderr)
return 1
log(f"Successfully deleted relationships from {deleted_count} file(s)", file=sys.stderr)
return 0
except Exception as exc:
log(f"Error in delete-relationship: {exc}", file=sys.stderr)
return 1
CMDLET = Cmdlet(
name="delete-relationship",
summary="Remove relationships from files.",
usage="@1 | delete-relationship --all OR delete-relationship -path <file> --all OR @1-3 | delete-relationship -type alt",
args=[
CmdletArg("path", type="string", description="Specify the local file path (if not piping a result)."),
CmdletArg("all", type="flag", description="Delete all relationships for the file(s)."),
CmdletArg("type", type="string", description="Delete specific relationship type ('alt', 'king', 'related'). Default: delete all types."),
],
details=[
"- Delete all relationships: pipe files | delete-relationship --all",
"- Delete specific type: pipe files | delete-relationship -type alt",
"- Delete all from file: delete-relationship -path <file> --all",
],
)

View File

@@ -243,8 +243,8 @@ def _process_deletion(tags: list[str], hash_hex: str | None, file_path: str | No
# Fallback: assume file is in a library root or use its parent
local_root = path_obj.parent
db = LocalLibraryDB(local_root)
db.remove_tags(path_obj, tags)
with LocalLibraryDB(local_root) as db:
db.remove_tags(path_obj, tags)
debug(f"Removed {len(tags)} tag(s) from {path_obj.name} (local)")
return True

View File

@@ -1224,12 +1224,31 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any], emit_results:
from helper.local_library import LocalLibraryDB
from config import get_local_storage_path
# Define LazyDB proxy to avoid keeping DB connection open for long duration
class LazyDB:
def __init__(self, root):
self.root = root
def _op(self, func_name, *args, **kwargs):
try:
with LocalLibraryDB(self.root) as db:
func = getattr(db, func_name)
return func(*args, **kwargs)
except Exception as e:
# Log error but don't crash
pass
def insert_worker(self, *args, **kwargs): self._op('insert_worker', *args, **kwargs)
def update_worker_status(self, *args, **kwargs): self._op('update_worker_status', *args, **kwargs)
def append_worker_stdout(self, *args, **kwargs): self._op('append_worker_stdout', *args, **kwargs)
def close(self): pass
worker_id = str(uuid.uuid4())
library_root = get_local_storage_path(config or {})
db = None
if library_root:
try:
db = LocalLibraryDB(library_root)
db = LazyDB(library_root)
db.insert_worker(
worker_id,
"download",
@@ -1812,12 +1831,18 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any], emit_results:
current_format_selector = format_id
# If it's a video-only format (has vcodec but no acodec), add bestaudio
# BUT: Skip this for -section downloads because combining formats causes re-encoding
# For -section, use formats that already have audio (muxed) to avoid FFmpeg re-encoding
vcodec = url.get('vcodec', '')
acodec = url.get('acodec', '')
if vcodec and vcodec != "none" and (not acodec or acodec == "none"):
# Video-only format, add bestaudio automatically
current_format_selector = f"{format_id}+bestaudio"
debug(f" Video-only format detected, automatically adding bestaudio")
if not clip_range and not section_ranges:
# Only add bestaudio if NOT doing -section or -clip
# For section downloads, we need muxed formats to avoid re-encoding
current_format_selector = f"{format_id}+bestaudio"
debug(f" Video-only format detected, automatically adding bestaudio")
else:
debug(f" Section/clip download: using video-only format as-is (no bestaudio to avoid re-encoding)")
actual_url = url.get('source_url')
url = actual_url # Use the actual URL for further processing
@@ -2488,11 +2513,16 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any], emit_results:
current_format_selector = fmt.get("format_id")
# If video-only format is selected, append +bestaudio to merge with best audio
# BUT: Skip this for -section downloads because combining formats causes re-encoding
vcodec = fmt.get("vcodec")
acodec = fmt.get("acodec")
if vcodec and vcodec != "none" and (not acodec or acodec == "none"):
current_format_selector = f"{current_format_selector}+bestaudio"
debug(f"Video-only format selected, appending bestaudio: {current_format_selector}")
if not clip_range and not section_ranges:
# Only add bestaudio if NOT doing -section or -clip
current_format_selector = f"{current_format_selector}+bestaudio"
debug(f"Video-only format selected, appending bestaudio: {current_format_selector}")
else:
debug(f"Section/clip download: using video-only format as-is (no bestaudio to avoid re-encoding)")
debug(f"Selected format #{idx}: {current_format_selector}")
playlist_items = None # Clear so it doesn't affect download options

View File

@@ -74,24 +74,33 @@ def _run(result: Any, _args: Sequence[str], config: Dict[str, Any]) -> int:
path_obj = Path(file_path)
if not source_title or source_title == "Unknown":
source_title = path_obj.name
print(f"\n[DEBUG] Starting get-relationship for: {path_obj.name}", file=sys.stderr)
print(f"[DEBUG] Path exists: {path_obj.exists()}", file=sys.stderr)
if path_obj.exists():
storage_path = get_local_storage_path(config)
print(f"[DEBUG] Storage path: {storage_path}", file=sys.stderr)
if storage_path:
with LocalLibraryDB(storage_path) as db:
metadata = db.get_metadata(path_obj)
print(f"[DEBUG] Metadata found: {metadata is not None}", file=sys.stderr)
if metadata and metadata.get("relationships"):
local_db_checked = True
rels = metadata["relationships"]
print(f"[DEBUG] Relationships dict: {rels}", file=sys.stderr)
if isinstance(rels, dict):
for rel_type, hashes in rels.items():
print(f"[DEBUG] Processing rel_type: {rel_type}, hashes: {hashes}", file=sys.stderr)
if hashes:
for h in hashes:
# Try to resolve hash to filename if possible
# h is now a file hash (not a path)
print(f"[DEBUG] Processing relationship hash: h={h}", file=sys.stderr)
# Resolve hash to file path
resolved_path = db.search_by_hash(h)
title = h
title = h[:16] + "..."
path = None
if resolved_path:
if resolved_path and resolved_path.exists():
path = str(resolved_path)
# Try to get title from tags
try:
@@ -114,6 +123,177 @@ def _run(result: Any, _args: Sequence[str], config: Dict[str, Any]) -> int:
"path": path,
"origin": "local"
})
# RECURSIVE LOOKUP: If this is an "alt" relationship (meaning we're an alt pointing to a king),
# then we should look up the king's other alts to show siblings.
# NOTE: We only do this for "alt" relationships, not "king", to avoid duplicating
# the king's direct relationships with its alts.
print(f"[DEBUG] Checking if recursive lookup needed: rel_type={rel_type}, path={path}", file=sys.stderr)
if rel_type.lower() == "alt" and path:
print(f"[DEBUG] 🔍 RECURSIVE LOOKUP TRIGGERED for parent: {path}", file=sys.stderr)
try:
parent_path_obj = Path(path)
print(f"[DEBUG] Parent path obj: {parent_path_obj}", file=sys.stderr)
# Also add the king/parent itself if not already in results
if not any(str(r['hash']).lower() == str(path).lower() for r in found_relationships):
parent_title = parent_path_obj.stem
try:
parent_tags = db.get_tags(parent_path_obj)
for t in parent_tags:
if t.lower().startswith('title:'):
parent_title = t[6:].strip()
break
except Exception:
pass
print(f"[DEBUG] Adding king/parent to results: {parent_title}", file=sys.stderr)
found_relationships.append({
"hash": str(path),
"type": "king" if rel_type.lower() == "alt" else rel_type,
"title": parent_title,
"path": str(path),
"origin": "local"
})
else:
# If already in results, ensure it's marked as king if appropriate
for r in found_relationships:
if str(r['hash']).lower() == str(path).lower():
if rel_type.lower() == "alt":
r['type'] = "king"
break
# 1. Check forward relationships from parent (siblings)
parent_metadata = db.get_metadata(parent_path_obj)
print(f"[DEBUG] 📖 Parent metadata: {parent_metadata is not None}", file=sys.stderr)
if parent_metadata:
print(f"[DEBUG] Parent metadata keys: {parent_metadata.keys()}", file=sys.stderr)
if parent_metadata and parent_metadata.get("relationships"):
parent_rels = parent_metadata["relationships"]
print(f"[DEBUG] 👑 Parent has relationships: {list(parent_rels.keys())}", file=sys.stderr)
if isinstance(parent_rels, dict):
for child_type, child_hashes in parent_rels.items():
print(f"[DEBUG] Type '{child_type}': {len(child_hashes) if child_hashes else 0} children", file=sys.stderr)
if child_hashes:
for child_h in child_hashes:
# child_h is now a HASH, not a path - resolve it
child_path_obj = db.search_by_hash(child_h)
print(f"[DEBUG] Resolved hash {child_h[:16]}... to: {child_path_obj}", file=sys.stderr)
if not child_path_obj:
# Hash doesn't resolve - skip it
print(f"[DEBUG] ⏭️ Hash doesn't resolve, skipping: {child_h}", file=sys.stderr)
continue
# Skip the current file we're querying
if str(child_path_obj).lower() == str(path_obj).lower():
print(f"[DEBUG] ⏭️ Skipping current file: {child_path_obj}", file=sys.stderr)
continue
# Check if already added (case-insensitive hash check)
if any(str(r['hash']).lower() == str(child_h).lower() for r in found_relationships):
print(f"[DEBUG] ⏭️ Already in results: {child_h}", file=sys.stderr)
continue
# Now child_path_obj is a Path, so we can get tags
child_title = child_path_obj.stem
try:
child_tags = db.get_tags(child_path_obj)
for t in child_tags:
if t.lower().startswith('title:'):
child_title = t[6:].strip()
break
except Exception:
pass
print(f"[DEBUG] Adding sibling: {child_title}", file=sys.stderr)
found_relationships.append({
"hash": child_h,
"type": f"alt" if child_type == "alt" else f"sibling ({child_type})",
"title": child_title,
"path": str(child_path_obj),
"origin": "local"
})
else:
print(f"[DEBUG] ⚠️ Parent has no relationships metadata", file=sys.stderr)
# 2. Check reverse relationships pointing TO parent (siblings via reverse lookup)
# This handles the case where siblings point to parent but parent doesn't point to siblings
reverse_children = db.find_files_pointing_to(parent_path_obj)
print(f"[DEBUG] 🔄 Reverse lookup found {len(reverse_children)} children", file=sys.stderr)
for child in reverse_children:
child_path = child['path']
child_type = child['type']
print(f"[DEBUG] Reverse child: {child_path}, type: {child_type}", file=sys.stderr)
# Skip the current file
if str(child_path).lower() == str(path_obj).lower():
print(f"[DEBUG] ⏭️ Skipping self", file=sys.stderr)
continue
# Skip if already added (check by path, case-insensitive)
if any(str(r.get('path', '')).lower() == str(child_path).lower() for r in found_relationships):
print(f"[DEBUG] ⏭️ Already in results: {child_path}", file=sys.stderr)
continue
child_path_obj = Path(child_path)
child_title = child_path_obj.stem
try:
child_tags = db.get_tags(child_path_obj)
for t in child_tags:
if t.lower().startswith('title:'):
child_title = t[6:].strip()
break
except Exception:
pass
print(f"[DEBUG] Adding reverse sibling: {child_title}", file=sys.stderr)
found_relationships.append({
"hash": child_path,
"type": f"alt" if child_type == "alt" else f"sibling ({child_type})",
"title": child_title,
"path": child_path,
"origin": "local"
})
except Exception as e:
print(f"[DEBUG] ❌ Recursive lookup error: {e}", file=sys.stderr)
import traceback
traceback.print_exc(file=sys.stderr)
except Exception as e:
log(f"Recursive lookup error: {e}", file=sys.stderr)
# ALSO CHECK REVERSE RELATIONSHIPS FOR THE CURRENT FILE
# NOTE: This is now handled via recursive lookup above, which finds siblings through the parent.
# We keep this disabled to avoid adding the same relationships twice.
# If needed in future, can be re-enabled with better deduplication.
# for rev in reverse_rels:
# rev_path = rev['path']
# rev_type = rev['type']
#
# if any(r['hash'] == rev_path for r in found_relationships): continue
#
# rev_path_obj = Path(rev_path)
# rev_title = rev_path_obj.stem
# try:
# rev_tags = db.get_tags(rev_path_obj)
# for t in rev_tags:
# if t.lower().startswith('title:'):
# rev_title = t[6:].strip(); break
# except Exception: pass
#
# # If someone points to us as 'alt' or 'king', they are our 'child' or 'subject'
# # But we'll just list them with the relationship type they used
# found_relationships.append({
# "hash": rev_path,
# "type": f"reverse-{rev_type}", # e.g. reverse-alt
# "title": rev_title,
# "path": rev_path,
# "origin": "local"
# })
except Exception as e:
log(f"Error checking local relationships: {e}", file=sys.stderr)

View File

@@ -223,6 +223,42 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
pause_mode = parsed.get("pause")
save_mode = parsed.get("save")
load_mode = parsed.get("load")
current_mode = parsed.get("current")
# Handle --current flag: emit currently playing item to pipeline
if current_mode:
items = _get_playlist()
if items is None:
debug("MPV is not running or not accessible.", file=sys.stderr)
return 1
# Find the currently playing item
current_item = None
for item in items:
if item.get("current", False):
current_item = item
break
if current_item is None:
debug("No item is currently playing.", file=sys.stderr)
return 1
# Build result object with file info
title = _extract_title_from_item(current_item)
filename = current_item.get("filename", "")
# Emit the current item to pipeline
result_obj = {
'file_path': filename,
'title': title,
'cmdlet_name': '.pipe',
'source': 'pipe',
'__pipe_index': items.index(current_item),
}
ctx.emit(result_obj)
debug(f"Emitted current item: {title}")
return 0
# Handle URL queuing
mpv_started = False
@@ -599,7 +635,7 @@ CMDLET = Cmdlet(
name=".pipe",
aliases=["pipe", "playlist", "queue", "ls-pipe"],
summary="Manage and play items in the MPV playlist via IPC",
usage=".pipe [index|url] [-clear] [-url URL]",
usage=".pipe [index|url] [-current] [-clear] [-list] [-url URL]",
args=[
CmdletArg(
name="index",
@@ -643,6 +679,11 @@ CMDLET = Cmdlet(
type="flag",
description="List saved playlists"
),
CmdletArg(
name="current",
type="flag",
description="Emit the currently playing item to pipeline for further processing"
),
],
exec=_run
)

View File

@@ -88,9 +88,7 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
try:
from helper.local_library import LocalLibraryDB
db: LocalLibraryDB | None = None
try:
db = LocalLibraryDB(library_root)
with LocalLibraryDB(library_root) as db:
if clear_requested:
count = db.clear_finished_workers()
log(f"Cleared {count} finished workers.")
@@ -115,9 +113,6 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
if selection_requested:
return _render_worker_selection(db, result)
return _render_worker_list(db, status_filter, limit)
finally:
if db:
db.close()
except Exception as exc:
log(f"Workers query failed: {exc}", file=sys.stderr)
import traceback