Add YAPF style + ignore, and format tracked Python files
This commit is contained in:
@@ -32,9 +32,8 @@ def register_native_commands(registry: Dict[str, CmdletFn]) -> None:
|
||||
"""Import native command modules and register their CMDLET exec functions."""
|
||||
base_dir = os.path.dirname(__file__)
|
||||
for filename in os.listdir(base_dir):
|
||||
if not (
|
||||
filename.endswith(".py") and not filename.startswith("_") and filename != "__init__.py"
|
||||
):
|
||||
if not (filename.endswith(".py") and not filename.startswith("_")
|
||||
and filename != "__init__.py"):
|
||||
continue
|
||||
|
||||
mod_name = filename[:-3]
|
||||
@@ -46,5 +45,8 @@ def register_native_commands(registry: Dict[str, CmdletFn]) -> None:
|
||||
except Exception as exc:
|
||||
import sys
|
||||
|
||||
print(f"Error importing native command '{mod_name}': {exc}", file=sys.stderr)
|
||||
print(
|
||||
f"Error importing native command '{mod_name}': {exc}",
|
||||
file=sys.stderr
|
||||
)
|
||||
continue
|
||||
|
||||
@@ -8,7 +8,9 @@ from result_table import ResultTable
|
||||
import pipeline as ctx
|
||||
|
||||
ADJECTIVE_FILE = os.path.join(
|
||||
os.path.dirname(os.path.dirname(__file__)), "cmdnat", "adjective.json"
|
||||
os.path.dirname(os.path.dirname(__file__)),
|
||||
"cmdnat",
|
||||
"adjective.json"
|
||||
)
|
||||
|
||||
|
||||
@@ -149,10 +151,22 @@ CMDLET = Cmdlet(
|
||||
summary="Manage adjective categories and tags",
|
||||
usage=".adjective [category] [-add tag] [-delete tag]",
|
||||
arg=[
|
||||
CmdletArg(name="category", type="string", description="Category name", required=False),
|
||||
CmdletArg(name="tag", type="string", description="Tag name", required=False),
|
||||
CmdletArg(name="add", type="flag", description="Add tag"),
|
||||
CmdletArg(name="delete", type="flag", description="Delete tag"),
|
||||
CmdletArg(
|
||||
name="category",
|
||||
type="string",
|
||||
description="Category name",
|
||||
required=False
|
||||
),
|
||||
CmdletArg(name="tag",
|
||||
type="string",
|
||||
description="Tag name",
|
||||
required=False),
|
||||
CmdletArg(name="add",
|
||||
type="flag",
|
||||
description="Add tag"),
|
||||
CmdletArg(name="delete",
|
||||
type="flag",
|
||||
description="Delete tag"),
|
||||
],
|
||||
exec=_run,
|
||||
)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from typing import List, Dict, Any
|
||||
|
||||
from cmdlet._shared import Cmdlet, CmdletArg
|
||||
from config import load_config, save_config
|
||||
from SYS.config import load_config, save_config
|
||||
|
||||
CMDLET = Cmdlet(
|
||||
name=".config",
|
||||
@@ -9,16 +9,24 @@ CMDLET = Cmdlet(
|
||||
usage=".config [key] [value]",
|
||||
arg=[
|
||||
CmdletArg(
|
||||
name="key", description="Configuration key to update (dot-separated)", required=False
|
||||
name="key",
|
||||
description="Configuration key to update (dot-separated)",
|
||||
required=False
|
||||
),
|
||||
CmdletArg(
|
||||
name="value",
|
||||
description="New value for the configuration key",
|
||||
required=False
|
||||
),
|
||||
CmdletArg(name="value", description="New value for the configuration key", required=False),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def flatten_config(
|
||||
config: Dict[str, Any], parent_key: str = "", sep: str = "."
|
||||
) -> List[Dict[str, Any]]:
|
||||
def flatten_config(config: Dict[str,
|
||||
Any],
|
||||
parent_key: str = "",
|
||||
sep: str = ".") -> List[Dict[str,
|
||||
Any]]:
|
||||
items = []
|
||||
for k, v in config.items():
|
||||
if k.startswith("_"): # Skip internal keys
|
||||
@@ -126,9 +134,8 @@ def _run(piped_result: Any, args: List[str], config: Dict[str, Any]) -> int:
|
||||
value = " ".join(args[1:])
|
||||
|
||||
# Remove quotes if present
|
||||
if (value.startswith('"') and value.endswith('"')) or (
|
||||
value.startswith("'") and value.endswith("'")
|
||||
):
|
||||
if (value.startswith('"') and value.endswith('"')) or (value.startswith("'")
|
||||
and value.endswith("'")):
|
||||
value = value[1:-1]
|
||||
|
||||
try:
|
||||
|
||||
@@ -27,7 +27,11 @@ def _examples_for_cmd(name: str) -> List[str]:
|
||||
return lookup.get(key, [])
|
||||
|
||||
|
||||
def _find_cmd_metadata(name: str, metadata: Dict[str, Dict[str, Any]]) -> Optional[Dict[str, Any]]:
|
||||
def _find_cmd_metadata(name: str,
|
||||
metadata: Dict[str,
|
||||
Dict[str,
|
||||
Any]]) -> Optional[Dict[str,
|
||||
Any]]:
|
||||
target = name.replace("_", "-").lower()
|
||||
for cmd_name, meta in metadata.items():
|
||||
if target == cmd_name:
|
||||
@@ -39,7 +43,11 @@ def _find_cmd_metadata(name: str, metadata: Dict[str, Dict[str, Any]]) -> Option
|
||||
|
||||
|
||||
def _render_list(
|
||||
metadata: Dict[str, Dict[str, Any]], filter_text: Optional[str], args: Sequence[str]
|
||||
metadata: Dict[str,
|
||||
Dict[str,
|
||||
Any]],
|
||||
filter_text: Optional[str],
|
||||
args: Sequence[str]
|
||||
) -> None:
|
||||
table = ResultTable("Help")
|
||||
table.set_source_command(".help", list(args))
|
||||
@@ -143,7 +151,9 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
|
||||
try:
|
||||
import cmdlet_catalog as _catalog
|
||||
|
||||
CMDLET.arg[0].choices = _normalize_choice_list(_catalog.list_cmdlet_names(config=config))
|
||||
CMDLET.arg[0].choices = _normalize_choice_list(
|
||||
_catalog.list_cmdlet_names(config=config)
|
||||
)
|
||||
metadata = _catalog.list_cmdlet_metadata(config=config)
|
||||
except Exception:
|
||||
CMDLET.arg[0].choices = []
|
||||
@@ -168,7 +178,8 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
|
||||
|
||||
CMDLET = Cmdlet(
|
||||
name=".help",
|
||||
alias=["help", "?"],
|
||||
alias=["help",
|
||||
"?"],
|
||||
summary="Show cmdlet or detailed help",
|
||||
usage=".help [cmd] [-filter text]",
|
||||
arg=[
|
||||
|
||||
106
cmdnat/matrix.py
106
cmdnat/matrix.py
@@ -13,7 +13,6 @@ from SYS.logger import log, debug
|
||||
from result_table import ResultTable
|
||||
import pipeline as ctx
|
||||
|
||||
|
||||
_MATRIX_PENDING_ITEMS_KEY = "matrix_pending_items"
|
||||
_MATRIX_PENDING_TEXT_KEY = "matrix_pending_text"
|
||||
|
||||
@@ -226,11 +225,10 @@ def _extract_url(item: Any) -> Optional[str]:
|
||||
if isinstance(item, dict):
|
||||
for key in ("url", "source_url", "path", "target"):
|
||||
raw = item.get(key)
|
||||
if (
|
||||
isinstance(raw, str)
|
||||
and raw.strip()
|
||||
and raw.strip().startswith(("http://", "https://"))
|
||||
):
|
||||
if (isinstance(raw,
|
||||
str) and raw.strip() and raw.strip().startswith(
|
||||
("http://",
|
||||
"https://"))):
|
||||
return raw.strip()
|
||||
except Exception:
|
||||
pass
|
||||
@@ -269,15 +267,16 @@ def _extract_hash_from_hydrus_file_url(url: str) -> Optional[str]:
|
||||
return None
|
||||
|
||||
|
||||
def _maybe_download_hydrus_file(
|
||||
item: Any, config: Dict[str, Any], output_dir: Path
|
||||
) -> Optional[str]:
|
||||
def _maybe_download_hydrus_file(item: Any,
|
||||
config: Dict[str,
|
||||
Any],
|
||||
output_dir: Path) -> Optional[str]:
|
||||
"""If the item looks like a Hydrus file (hash + Hydrus URL), download it using Hydrus access key headers.
|
||||
|
||||
This avoids 401 from Hydrus when the URL is /get_files/file?hash=... without headers.
|
||||
"""
|
||||
try:
|
||||
from config import get_hydrus_access_key, get_hydrus_url
|
||||
from SYS.config import get_hydrus_access_key, get_hydrus_url
|
||||
from API.HydrusNetwork import HydrusNetwork as HydrusClient, download_hydrus_file
|
||||
|
||||
# Prefer per-item Hydrus instance name when it matches a configured instance.
|
||||
@@ -319,19 +318,23 @@ def _maybe_download_hydrus_file(
|
||||
) and _extract_hash_from_hydrus_file_url(url) == file_hash
|
||||
hydrus_instances: set[str] = set()
|
||||
try:
|
||||
store_cfg = (config or {}).get("store") if isinstance(config, dict) else None
|
||||
store_cfg = (config
|
||||
or {}).get("store") if isinstance(config,
|
||||
dict) else None
|
||||
if isinstance(store_cfg, dict):
|
||||
hydrus_cfg = store_cfg.get("hydrusnetwork")
|
||||
if isinstance(hydrus_cfg, dict):
|
||||
hydrus_instances = {
|
||||
str(k).strip().lower() for k in hydrus_cfg.keys() if str(k).strip()
|
||||
str(k).strip().lower()
|
||||
for k in hydrus_cfg.keys() if str(k).strip()
|
||||
}
|
||||
except Exception:
|
||||
hydrus_instances = set()
|
||||
|
||||
store_hint = store_name.lower() in {"hydrus", "hydrusnetwork"} or (
|
||||
store_name.lower() in hydrus_instances
|
||||
)
|
||||
store_hint = store_name.lower() in {
|
||||
"hydrus",
|
||||
"hydrusnetwork"
|
||||
} or (store_name.lower() in hydrus_instances)
|
||||
if not (is_hydrus_url or store_hint):
|
||||
return None
|
||||
|
||||
@@ -341,8 +344,13 @@ def _maybe_download_hydrus_file(
|
||||
# Best-effort extension from Hydrus metadata.
|
||||
suffix = ".hydrus"
|
||||
try:
|
||||
meta_response = client.fetch_file_metadata(hashes=[file_hash], include_mime=True)
|
||||
entries = meta_response.get("metadata") if isinstance(meta_response, dict) else None
|
||||
meta_response = client.fetch_file_metadata(
|
||||
hashes=[file_hash],
|
||||
include_mime=True
|
||||
)
|
||||
entries = meta_response.get("metadata"
|
||||
) if isinstance(meta_response,
|
||||
dict) else None
|
||||
if isinstance(entries, list) and entries:
|
||||
entry = entries[0]
|
||||
if isinstance(entry, dict):
|
||||
@@ -362,7 +370,9 @@ def _maybe_download_hydrus_file(
|
||||
# Avoid clobbering; pick a unique name.
|
||||
dest = output_dir / f"{file_hash}_{uuid.uuid4().hex[:10]}{suffix}"
|
||||
|
||||
headers = {"Hydrus-Client-API-Access-Key": access_key}
|
||||
headers = {
|
||||
"Hydrus-Client-API-Access-Key": access_key
|
||||
}
|
||||
download_hydrus_file(file_url, headers, dest, timeout=30.0)
|
||||
if dest.exists():
|
||||
return str(dest)
|
||||
@@ -416,9 +426,8 @@ def _resolve_upload_path(item: Any, config: Dict[str, Any]) -> Optional[str]:
|
||||
if isinstance(config, dict):
|
||||
base_tmp = config.get("temp")
|
||||
output_dir = (
|
||||
Path(str(base_tmp)).expanduser()
|
||||
if base_tmp
|
||||
else (Path(tempfile.gettempdir()) / "Medios-Macina")
|
||||
Path(str(base_tmp)).expanduser() if base_tmp else
|
||||
(Path(tempfile.gettempdir()) / "Medios-Macina")
|
||||
)
|
||||
output_dir = output_dir / "matrix" / "hydrus"
|
||||
hydrus_path = _maybe_download_hydrus_file(item, config, output_dir)
|
||||
@@ -441,19 +450,15 @@ def _resolve_upload_path(item: Any, config: Dict[str, Any]) -> Optional[str]:
|
||||
if isinstance(config, dict):
|
||||
base_tmp = config.get("temp")
|
||||
output_dir = (
|
||||
Path(str(base_tmp)).expanduser()
|
||||
if base_tmp
|
||||
else (Path(tempfile.gettempdir()) / "Medios-Macina")
|
||||
Path(str(base_tmp)).expanduser() if base_tmp else
|
||||
(Path(tempfile.gettempdir()) / "Medios-Macina")
|
||||
)
|
||||
output_dir = output_dir / "matrix"
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
result = _download_direct_file(url, output_dir, quiet=True)
|
||||
if (
|
||||
result
|
||||
and hasattr(result, "path")
|
||||
and isinstance(result.path, Path)
|
||||
and result.path.exists()
|
||||
):
|
||||
if (result and hasattr(result,
|
||||
"path") and isinstance(result.path,
|
||||
Path) and result.path.exists()):
|
||||
return str(result.path)
|
||||
except Exception as exc:
|
||||
debug(f"[matrix] Failed to download URL for upload: {exc}")
|
||||
@@ -503,7 +508,10 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
|
||||
text_value = _extract_text_arg(args)
|
||||
if not text_value:
|
||||
try:
|
||||
text_value = str(ctx.load_value(_MATRIX_PENDING_TEXT_KEY, default="") or "").strip()
|
||||
text_value = str(
|
||||
ctx.load_value(_MATRIX_PENDING_TEXT_KEY,
|
||||
default="") or ""
|
||||
).strip()
|
||||
except Exception:
|
||||
text_value = ""
|
||||
|
||||
@@ -544,7 +552,10 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
|
||||
)
|
||||
continue
|
||||
|
||||
upload_jobs.append({"path": str(media_path), "pipe_obj": item})
|
||||
upload_jobs.append({
|
||||
"path": str(media_path),
|
||||
"pipe_obj": item
|
||||
})
|
||||
|
||||
for rid in room_ids:
|
||||
sent_any_for_room = False
|
||||
@@ -553,14 +564,21 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
|
||||
if not file_path:
|
||||
continue
|
||||
try:
|
||||
link = provider.upload_to_room(file_path, rid, pipe_obj=job.get("pipe_obj"))
|
||||
link = provider.upload_to_room(
|
||||
file_path,
|
||||
rid,
|
||||
pipe_obj=job.get("pipe_obj")
|
||||
)
|
||||
debug(f"✓ Sent {Path(file_path).name} -> {rid}")
|
||||
if link:
|
||||
log(link)
|
||||
sent_any_for_room = True
|
||||
except Exception as exc:
|
||||
any_failed = True
|
||||
log(f"Matrix send failed for {Path(file_path).name}: {exc}", file=sys.stderr)
|
||||
log(
|
||||
f"Matrix send failed for {Path(file_path).name}: {exc}",
|
||||
file=sys.stderr
|
||||
)
|
||||
|
||||
# Optional caption-like follow-up message (sent once per room).
|
||||
if text_value and sent_any_for_room:
|
||||
@@ -581,7 +599,10 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
|
||||
# Default stage: show rooms, then wait for @N selection to resume sending.
|
||||
selected_items = _normalize_to_list(result)
|
||||
if not selected_items:
|
||||
log("Usage: @N | .matrix (select items first, then pick a room)", file=sys.stderr)
|
||||
log(
|
||||
"Usage: @N | .matrix (select items first, then pick a room)",
|
||||
file=sys.stderr
|
||||
)
|
||||
return 1
|
||||
|
||||
ctx.store_value(_MATRIX_PENDING_ITEMS_KEY, selected_items)
|
||||
@@ -601,7 +622,10 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
|
||||
try:
|
||||
configured_ids = None
|
||||
if not _has_flag(args, "-all"):
|
||||
ids = [str(v).strip() for v in _parse_config_room_filter_ids(config) if str(v).strip()]
|
||||
ids = [
|
||||
str(v).strip() for v in _parse_config_room_filter_ids(config)
|
||||
if str(v).strip()
|
||||
]
|
||||
if ids:
|
||||
configured_ids = ids
|
||||
|
||||
@@ -613,7 +637,8 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
|
||||
# Diagnostics if a configured filter yields no rows (provider filtered before name lookups for speed).
|
||||
if not rooms and not _has_flag(args, "-all"):
|
||||
configured_ids_dbg = [
|
||||
str(v).strip() for v in _parse_config_room_filter_ids(config) if str(v).strip()
|
||||
str(v).strip() for v in _parse_config_room_filter_ids(config)
|
||||
if str(v).strip()
|
||||
]
|
||||
if configured_ids_dbg:
|
||||
try:
|
||||
@@ -640,7 +665,9 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
|
||||
for room in rooms:
|
||||
row = table.add_row()
|
||||
name = str(room.get("name") or "").strip() if isinstance(room, dict) else ""
|
||||
room_id = str(room.get("room_id") or "").strip() if isinstance(room, dict) else ""
|
||||
room_id = str(room.get("room_id") or ""
|
||||
).strip() if isinstance(room,
|
||||
dict) else ""
|
||||
row.add_column("Name", name)
|
||||
row.add_column("Room", room_id)
|
||||
|
||||
@@ -669,7 +696,8 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
|
||||
|
||||
CMDLET = Cmdlet(
|
||||
name=".matrix",
|
||||
alias=["matrix", "rooms"],
|
||||
alias=["matrix",
|
||||
"rooms"],
|
||||
summary="Send selected items to a Matrix room",
|
||||
usage="@N | .matrix",
|
||||
arg=[
|
||||
|
||||
@@ -10,7 +10,6 @@ from cmdlet._shared import Cmdlet, CmdletArg
|
||||
from SYS.logger import log
|
||||
import pipeline as ctx
|
||||
|
||||
|
||||
CMDLET = Cmdlet(
|
||||
name=".out-table",
|
||||
summary="Save the current result table to an SVG file.",
|
||||
@@ -32,7 +31,6 @@ CMDLET = Cmdlet(
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
_WINDOWS_RESERVED_NAMES = {
|
||||
"con",
|
||||
"prn",
|
||||
@@ -112,7 +110,8 @@ def _get_active_table(piped_result: Any) -> Optional[Any]:
|
||||
if piped_result.__class__.__name__ == "ResultTable":
|
||||
return piped_result
|
||||
|
||||
return ctx.get_display_table() or ctx.get_current_stage_table() or ctx.get_last_result_table()
|
||||
return ctx.get_display_table() or ctx.get_current_stage_table(
|
||||
) or ctx.get_last_result_table()
|
||||
|
||||
|
||||
def _run(piped_result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
|
||||
@@ -123,7 +122,8 @@ def _run(piped_result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
|
||||
i = 0
|
||||
while i < len(args_list):
|
||||
low = args_list[i].strip().lower()
|
||||
if low in {"-path", "--path"} and i + 1 < len(args_list):
|
||||
if low in {"-path",
|
||||
"--path"} and i + 1 < len(args_list):
|
||||
path_arg = args_list[i + 1]
|
||||
i += 2
|
||||
continue
|
||||
|
||||
374
cmdnat/pipe.py
374
cmdnat/pipe.py
@@ -15,10 +15,10 @@ import pipeline as ctx
|
||||
from models import PipeObject
|
||||
|
||||
from API.folder import LocalLibrarySearchOptimizer
|
||||
from config import get_local_storage_path, get_hydrus_access_key, get_hydrus_url
|
||||
from SYS.config import get_local_storage_path, get_hydrus_access_key, get_hydrus_url
|
||||
|
||||
|
||||
_ALLDEBRID_UNLOCK_CACHE: Dict[str, str] = {}
|
||||
_ALLDEBRID_UNLOCK_CACHE: Dict[str,
|
||||
str] = {}
|
||||
|
||||
|
||||
def _repo_root() -> Path:
|
||||
@@ -66,15 +66,32 @@ def _try_enable_mpv_file_logging(mpv_log_path: str, *, attempts: int = 3) -> boo
|
||||
for _ in range(max(1, int(attempts))):
|
||||
try:
|
||||
# Try to set log-file and verbose level.
|
||||
r1 = _send_ipc_command({"command": ["set_property", "options/log-file", mpv_log_path]})
|
||||
r2 = _send_ipc_command({"command": ["set_property", "options/msg-level", "all=v"]})
|
||||
r1 = _send_ipc_command(
|
||||
{
|
||||
"command": ["set_property",
|
||||
"options/log-file",
|
||||
mpv_log_path]
|
||||
}
|
||||
)
|
||||
r2 = _send_ipc_command(
|
||||
{
|
||||
"command": ["set_property",
|
||||
"options/msg-level",
|
||||
"all=v"]
|
||||
}
|
||||
)
|
||||
ok = bool(
|
||||
(r1 and r1.get("error") == "success") or (r2 and r2.get("error") == "success")
|
||||
(r1 and r1.get("error") == "success")
|
||||
or (r2 and r2.get("error") == "success")
|
||||
)
|
||||
|
||||
# Emit a predictable line so the file isn't empty if logging is active.
|
||||
_send_ipc_command(
|
||||
{"command": ["print-text", f"medeia: log enabled -> {mpv_log_path}"]}, silent=True
|
||||
{
|
||||
"command": ["print-text",
|
||||
f"medeia: log enabled -> {mpv_log_path}"]
|
||||
},
|
||||
silent=True
|
||||
)
|
||||
except Exception:
|
||||
ok = False
|
||||
@@ -186,7 +203,11 @@ def _send_ipc_command(command: Dict[str, Any], silent: bool = False) -> Optional
|
||||
|
||||
def _get_playlist(silent: bool = False) -> Optional[List[Dict[str, Any]]]:
|
||||
"""Get the current playlist from MPV. Returns None if MPV is not running."""
|
||||
cmd = {"command": ["get_property", "playlist"], "request_id": 100}
|
||||
cmd = {
|
||||
"command": ["get_property",
|
||||
"playlist"],
|
||||
"request_id": 100
|
||||
}
|
||||
resp = _send_ipc_command(cmd, silent=silent)
|
||||
if resp is None:
|
||||
return None
|
||||
@@ -216,7 +237,8 @@ def _extract_title_from_item(item: Dict[str, Any]) -> str:
|
||||
lines = filename.splitlines()
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
if line and not line.startswith("#") and not line.startswith("memory://"):
|
||||
if line and not line.startswith("#") and not line.startswith(
|
||||
"memory://"):
|
||||
# Found the URL, use it as title
|
||||
return line
|
||||
except Exception:
|
||||
@@ -320,7 +342,9 @@ def _normalize_playlist_path(text: Optional[str]) -> Optional[str]:
|
||||
# If it's a hydrus file URL, normalize to the hash for dedupe
|
||||
try:
|
||||
parsed = urlparse(real)
|
||||
if parsed.scheme in {"http", "https", "hydrus"}:
|
||||
if parsed.scheme in {"http",
|
||||
"https",
|
||||
"hydrus"}:
|
||||
if parsed.path.endswith("/get_files/file"):
|
||||
qs = parse_qs(parsed.query)
|
||||
h = qs.get("hash", [None])[0]
|
||||
@@ -335,7 +359,9 @@ def _normalize_playlist_path(text: Optional[str]) -> Optional[str]:
|
||||
|
||||
|
||||
def _infer_store_from_playlist_item(
|
||||
item: Dict[str, Any], file_storage: Optional[Any] = None
|
||||
item: Dict[str,
|
||||
Any],
|
||||
file_storage: Optional[Any] = None
|
||||
) -> str:
|
||||
"""Infer a friendly store label from an MPV playlist entry.
|
||||
|
||||
@@ -379,7 +405,9 @@ def _infer_store_from_playlist_item(
|
||||
return "hydrus"
|
||||
|
||||
# Windows / UNC paths
|
||||
if re.match(r"^[a-z]:[\\/]", target, flags=re.IGNORECASE) or target.startswith("\\\\"):
|
||||
if re.match(r"^[a-z]:[\\/]",
|
||||
target,
|
||||
flags=re.IGNORECASE) or target.startswith("\\\\"):
|
||||
return "local"
|
||||
|
||||
# file:// url
|
||||
@@ -394,7 +422,9 @@ def _infer_store_from_playlist_item(
|
||||
return ""
|
||||
|
||||
host_no_port = host.split(":", 1)[0]
|
||||
host_stripped = host_no_port[4:] if host_no_port.startswith("www.") else host_no_port
|
||||
host_stripped = host_no_port[4:] if host_no_port.startswith(
|
||||
"www."
|
||||
) else host_no_port
|
||||
|
||||
if "youtube" in host_stripped or "youtu.be" in target.lower():
|
||||
return "youtube"
|
||||
@@ -402,7 +432,8 @@ def _infer_store_from_playlist_item(
|
||||
return "soundcloud"
|
||||
if "bandcamp" in host_stripped:
|
||||
return "bandcamp"
|
||||
if "get_files" in path or "file?hash=" in path or host_stripped in {"127.0.0.1", "localhost"}:
|
||||
if "get_files" in path or "file?hash=" in path or host_stripped in {"127.0.0.1",
|
||||
"localhost"}:
|
||||
# Hydrus API URL - try to extract hash and find instance
|
||||
if file_storage:
|
||||
# Try to extract hash from URL parameters
|
||||
@@ -448,9 +479,9 @@ def _build_hydrus_header(config: Dict[str, Any]) -> Optional[str]:
|
||||
return f"Hydrus-Client-API-Access-Key: {key}"
|
||||
|
||||
|
||||
def _build_ytdl_options(
|
||||
config: Optional[Dict[str, Any]], hydrus_header: Optional[str]
|
||||
) -> Optional[str]:
|
||||
def _build_ytdl_options(config: Optional[Dict[str,
|
||||
Any]],
|
||||
hydrus_header: Optional[str]) -> Optional[str]:
|
||||
"""Compose ytdl-raw-options string including cookies and optional Hydrus header."""
|
||||
opts: List[str] = []
|
||||
cookies_path = None
|
||||
@@ -516,7 +547,10 @@ def _ensure_ytdl_cookies(config: Optional[Dict[str, Any]] = None) -> None:
|
||||
file_size = file_obj.stat().st_size
|
||||
debug(f"Cookies file verified: {check_path} ({file_size} bytes)")
|
||||
else:
|
||||
debug(f"WARNING: Cookies file does not exist: {check_path}", file=sys.stderr)
|
||||
debug(
|
||||
f"WARNING: Cookies file does not exist: {check_path}",
|
||||
file=sys.stderr
|
||||
)
|
||||
else:
|
||||
debug("No cookies file configured")
|
||||
|
||||
@@ -531,7 +565,10 @@ def _monitor_mpv_logs(duration: float = 3.0) -> None:
|
||||
return
|
||||
|
||||
# Request log messages
|
||||
client.send_command({"command": ["request_log_messages", "warn"]})
|
||||
client.send_command({
|
||||
"command": ["request_log_messages",
|
||||
"warn"]
|
||||
})
|
||||
|
||||
# On Windows named pipes, avoid blocking the CLI; skip log read entirely
|
||||
if client.is_windows:
|
||||
@@ -574,7 +611,10 @@ def _monitor_mpv_logs(duration: float = 3.0) -> None:
|
||||
pass
|
||||
|
||||
|
||||
def _tail_text_file(path: str, *, max_lines: int = 120, max_bytes: int = 65536) -> List[str]:
|
||||
def _tail_text_file(path: str,
|
||||
*,
|
||||
max_lines: int = 120,
|
||||
max_bytes: int = 65536) -> List[str]:
|
||||
try:
|
||||
p = Path(str(path))
|
||||
if not p.exists() or not p.is_file():
|
||||
@@ -602,8 +642,12 @@ def _tail_text_file(path: str, *, max_lines: int = 120, max_bytes: int = 65536)
|
||||
|
||||
|
||||
def _get_playable_path(
|
||||
item: Any, file_storage: Optional[Any], config: Optional[Dict[str, Any]]
|
||||
) -> Optional[tuple[str, Optional[str]]]:
|
||||
item: Any,
|
||||
file_storage: Optional[Any],
|
||||
config: Optional[Dict[str,
|
||||
Any]]
|
||||
) -> Optional[tuple[str,
|
||||
Optional[str]]]:
|
||||
"""Extract a playable path/URL from an item, handling different store types.
|
||||
|
||||
Args:
|
||||
@@ -632,25 +676,31 @@ def _get_playable_path(
|
||||
title = item.get("title") or item.get("file_title")
|
||||
store = item.get("store")
|
||||
file_hash = item.get("hash")
|
||||
elif (
|
||||
hasattr(item, "path")
|
||||
or hasattr(item, "url")
|
||||
or hasattr(item, "source_url")
|
||||
or hasattr(item, "store")
|
||||
or hasattr(item, "hash")
|
||||
):
|
||||
elif (hasattr(item,
|
||||
"path") or hasattr(item,
|
||||
"url") or hasattr(item,
|
||||
"source_url") or hasattr(item,
|
||||
"store")
|
||||
or hasattr(item,
|
||||
"hash")):
|
||||
# Handle PipeObject / dataclass objects - prefer path, but fall back to url/source_url attributes
|
||||
path = getattr(item, "path", None)
|
||||
if not path:
|
||||
path = (
|
||||
getattr(item, "url", None)
|
||||
or getattr(item, "source_url", None)
|
||||
or getattr(item, "target", None)
|
||||
getattr(item,
|
||||
"url",
|
||||
None) or getattr(item,
|
||||
"source_url",
|
||||
None) or getattr(item,
|
||||
"target",
|
||||
None)
|
||||
)
|
||||
if not path:
|
||||
known = getattr(item, "url", None) or (getattr(item, "extra", None) or {}).get(
|
||||
"url"
|
||||
)
|
||||
known = getattr(item,
|
||||
"url",
|
||||
None) or (getattr(item,
|
||||
"extra",
|
||||
None) or {}).get("url")
|
||||
if known and isinstance(known, list):
|
||||
path = known[0]
|
||||
title = getattr(item, "title", None) or getattr(item, "file_title", None)
|
||||
@@ -666,7 +716,11 @@ def _get_playable_path(
|
||||
pass
|
||||
|
||||
# Treat common placeholders as missing.
|
||||
if isinstance(path, str) and path.strip().lower() in {"", "n/a", "na", "none"}:
|
||||
if isinstance(path,
|
||||
str) and path.strip().lower() in {"",
|
||||
"n/a",
|
||||
"na",
|
||||
"none"}:
|
||||
path = None
|
||||
|
||||
if title is not None and not isinstance(title, str):
|
||||
@@ -693,11 +747,10 @@ def _get_playable_path(
|
||||
backend_class = type(backend).__name__
|
||||
|
||||
# Folder stores: resolve to an on-disk file path.
|
||||
if (
|
||||
hasattr(backend, "get_file")
|
||||
and callable(getattr(backend, "get_file"))
|
||||
and backend_class == "Folder"
|
||||
):
|
||||
if (hasattr(backend,
|
||||
"get_file") and callable(getattr(backend,
|
||||
"get_file"))
|
||||
and backend_class == "Folder"):
|
||||
try:
|
||||
resolved = backend.get_file(file_hash)
|
||||
if isinstance(resolved, Path):
|
||||
@@ -705,7 +758,10 @@ def _get_playable_path(
|
||||
elif resolved is not None:
|
||||
path = str(resolved)
|
||||
except Exception as e:
|
||||
debug(f"Error resolving file path from store '{store}': {e}", file=sys.stderr)
|
||||
debug(
|
||||
f"Error resolving file path from store '{store}': {e}",
|
||||
file=sys.stderr
|
||||
)
|
||||
|
||||
# HydrusNetwork: build a playable API file URL without browser side-effects.
|
||||
elif backend_class == "HydrusNetwork":
|
||||
@@ -717,7 +773,10 @@ def _get_playable_path(
|
||||
# Auth is provided via http-header-fields (set in _queue_items).
|
||||
path = f"{base_url}/get_files/file?hash={file_hash}"
|
||||
except Exception as e:
|
||||
debug(f"Error building Hydrus URL from store '{store}': {e}", file=sys.stderr)
|
||||
debug(
|
||||
f"Error building Hydrus URL from store '{store}': {e}",
|
||||
file=sys.stderr
|
||||
)
|
||||
|
||||
if not path:
|
||||
# As a last resort, if we have a hash and no path/url, return the hash.
|
||||
@@ -735,8 +794,10 @@ def _get_playable_path(
|
||||
def _queue_items(
|
||||
items: List[Any],
|
||||
clear_first: bool = False,
|
||||
config: Optional[Dict[str, Any]] = None,
|
||||
start_opts: Optional[Dict[str, Any]] = None,
|
||||
config: Optional[Dict[str,
|
||||
Any]] = None,
|
||||
start_opts: Optional[Dict[str,
|
||||
Any]] = None,
|
||||
) -> bool:
|
||||
"""Queue items to MPV, starting it if necessary.
|
||||
|
||||
@@ -749,7 +810,9 @@ def _queue_items(
|
||||
"""
|
||||
# Debug: print incoming items
|
||||
try:
|
||||
debug(f"_queue_items: count={len(items)} types={[type(i).__name__ for i in items]}")
|
||||
debug(
|
||||
f"_queue_items: count={len(items)} types={[type(i).__name__ for i in items]}"
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -779,7 +842,8 @@ def _queue_items(
|
||||
playlist = _get_playlist(silent=True) or []
|
||||
dup_indexes: List[int] = []
|
||||
for idx, pl_item in enumerate(playlist):
|
||||
fname = pl_item.get("filename") if isinstance(pl_item, dict) else str(pl_item)
|
||||
fname = pl_item.get("filename") if isinstance(pl_item,
|
||||
dict) else str(pl_item)
|
||||
alt = pl_item.get("playlist-path") if isinstance(pl_item, dict) else None
|
||||
norm = _normalize_playlist_path(fname) or _normalize_playlist_path(alt)
|
||||
if not norm:
|
||||
@@ -793,7 +857,12 @@ def _queue_items(
|
||||
for idx in reversed(dup_indexes):
|
||||
try:
|
||||
_send_ipc_command(
|
||||
{"command": ["playlist-remove", idx], "request_id": 106}, silent=True
|
||||
{
|
||||
"command": ["playlist-remove",
|
||||
idx],
|
||||
"request_id": 106
|
||||
},
|
||||
silent=True
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
@@ -803,7 +872,9 @@ def _queue_items(
|
||||
for i, item in enumerate(items):
|
||||
# Debug: show the item being processed
|
||||
try:
|
||||
debug(f"_queue_items: processing idx={i} type={type(item)} repr={repr(item)[:200]}")
|
||||
debug(
|
||||
f"_queue_items: processing idx={i} type={type(item)} repr={repr(item)[:200]}"
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
# Extract URL/Path using store-aware logic
|
||||
@@ -852,13 +923,17 @@ def _queue_items(
|
||||
effective_hydrus_header = (
|
||||
f"Hydrus-Client-API-Access-Key: {str(key).strip()}"
|
||||
)
|
||||
effective_ytdl_opts = _build_ytdl_options(config, effective_hydrus_header)
|
||||
effective_ytdl_opts = _build_ytdl_options(
|
||||
config,
|
||||
effective_hydrus_header
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if target:
|
||||
# If we just have a hydrus hash, build a direct file URL for MPV
|
||||
if re.fullmatch(r"[0-9a-f]{64}", str(target).strip().lower()) and effective_hydrus_url:
|
||||
if re.fullmatch(r"[0-9a-f]{64}",
|
||||
str(target).strip().lower()) and effective_hydrus_url:
|
||||
target = (
|
||||
f"{effective_hydrus_url.rstrip('/')}/get_files/file?hash={str(target).strip()}"
|
||||
)
|
||||
@@ -880,11 +955,9 @@ def _queue_items(
|
||||
# This is especially important for local file-server URLs like /get_files/file?hash=...
|
||||
target_for_m3u = target
|
||||
try:
|
||||
if (
|
||||
item_store_name
|
||||
and isinstance(target_for_m3u, str)
|
||||
and target_for_m3u.startswith("http")
|
||||
):
|
||||
if (item_store_name and isinstance(target_for_m3u,
|
||||
str)
|
||||
and target_for_m3u.startswith("http")):
|
||||
if "get_files/file" in target_for_m3u and "store=" not in target_for_m3u:
|
||||
sep = "&" if "?" in target_for_m3u else "?"
|
||||
target_for_m3u = f"{target_for_m3u}{sep}store={item_store_name}"
|
||||
@@ -902,20 +975,33 @@ def _queue_items(
|
||||
|
||||
# If this is a Hydrus path, set header property and yt-dlp headers before loading.
|
||||
# Use the real target (not the memory:// wrapper) for detection.
|
||||
if effective_hydrus_header and _is_hydrus_path(str(target), effective_hydrus_url):
|
||||
if effective_hydrus_header and _is_hydrus_path(str(target),
|
||||
effective_hydrus_url):
|
||||
header_cmd = {
|
||||
"command": ["set_property", "http-header-fields", effective_hydrus_header],
|
||||
"request_id": 199,
|
||||
"command":
|
||||
["set_property",
|
||||
"http-header-fields",
|
||||
effective_hydrus_header],
|
||||
"request_id":
|
||||
199,
|
||||
}
|
||||
_send_ipc_command(header_cmd, silent=True)
|
||||
if effective_ytdl_opts:
|
||||
ytdl_cmd = {
|
||||
"command": ["set_property", "ytdl-raw-options", effective_ytdl_opts],
|
||||
"command":
|
||||
["set_property",
|
||||
"ytdl-raw-options",
|
||||
effective_ytdl_opts],
|
||||
"request_id": 197,
|
||||
}
|
||||
_send_ipc_command(ytdl_cmd, silent=True)
|
||||
|
||||
cmd = {"command": ["loadfile", target_to_send, mode], "request_id": 200}
|
||||
cmd = {
|
||||
"command": ["loadfile",
|
||||
target_to_send,
|
||||
mode],
|
||||
"request_id": 200
|
||||
}
|
||||
try:
|
||||
debug(f"Sending MPV loadfile: {target_to_send} mode={mode}")
|
||||
resp = _send_ipc_command(cmd, silent=True)
|
||||
@@ -968,8 +1054,8 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
|
||||
except Exception:
|
||||
mpv_log_path = str(
|
||||
(
|
||||
Path(os.environ.get("TEMP") or os.environ.get("TMP") or ".")
|
||||
/ "medeia-mpv.log"
|
||||
Path(os.environ.get("TEMP") or os.environ.get("TMP") or ".") /
|
||||
"medeia-mpv.log"
|
||||
).resolve()
|
||||
)
|
||||
# Ensure file exists early so we can tail it even if mpv writes later.
|
||||
@@ -999,12 +1085,21 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
|
||||
else:
|
||||
if prev_debug:
|
||||
try:
|
||||
devnull_fh = open(os.devnull, "w", encoding="utf-8", errors="replace")
|
||||
devnull_fh = open(
|
||||
os.devnull,
|
||||
"w",
|
||||
encoding="utf-8",
|
||||
errors="replace"
|
||||
)
|
||||
set_thread_stream(devnull_fh)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
start_opts: Dict[str, Any] = {"borderless": borderless, "mpv_log_path": mpv_log_path}
|
||||
start_opts: Dict[str,
|
||||
Any] = {
|
||||
"borderless": borderless,
|
||||
"mpv_log_path": mpv_log_path
|
||||
}
|
||||
|
||||
# Store registry is only needed for certain playlist listing/inference paths.
|
||||
# Keep it lazy so a simple `.pipe <url> -play` doesn't trigger Hydrus/API calls.
|
||||
@@ -1040,16 +1135,9 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
|
||||
# should not print the playlist table. It should only enable/tail logs
|
||||
# (handled in the `finally` block).
|
||||
only_log = bool(
|
||||
log_requested
|
||||
and not url_arg
|
||||
and index_arg is None
|
||||
and not clear_mode
|
||||
and not list_mode
|
||||
and not play_mode
|
||||
and not pause_mode
|
||||
and not save_mode
|
||||
and not load_mode
|
||||
and not current_mode
|
||||
log_requested and not url_arg and index_arg is None and not clear_mode
|
||||
and not list_mode and not play_mode and not pause_mode and not save_mode
|
||||
and not load_mode and not current_mode
|
||||
)
|
||||
if only_log:
|
||||
return 0
|
||||
@@ -1218,11 +1306,19 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
|
||||
# Queue items (replacing current playlist)
|
||||
if items:
|
||||
_queue_items(
|
||||
items, clear_first=True, config=config, start_opts=start_opts
|
||||
items,
|
||||
clear_first=True,
|
||||
config=config,
|
||||
start_opts=start_opts
|
||||
)
|
||||
else:
|
||||
# Empty playlist, just clear
|
||||
_send_ipc_command({"command": ["playlist-clear"]}, silent=True)
|
||||
_send_ipc_command(
|
||||
{
|
||||
"command": ["playlist-clear"]
|
||||
},
|
||||
silent=True
|
||||
)
|
||||
|
||||
# Switch to list mode to show the result
|
||||
list_mode = True
|
||||
@@ -1258,7 +1354,10 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
|
||||
table.set_source_command(".pipe")
|
||||
|
||||
# Register results
|
||||
ctx.set_last_result_table_overlay(table, [p["items"] for p in playlists])
|
||||
ctx.set_last_result_table_overlay(
|
||||
table,
|
||||
[p["items"] for p in playlists]
|
||||
)
|
||||
ctx.set_current_stage_table(table)
|
||||
|
||||
# Do not print directly here.
|
||||
@@ -1270,7 +1369,12 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
|
||||
|
||||
# Handle Play/Pause commands (but skip if we have index_arg to play a specific item)
|
||||
if play_mode and index_arg is None:
|
||||
cmd = {"command": ["set_property", "pause", False], "request_id": 103}
|
||||
cmd = {
|
||||
"command": ["set_property",
|
||||
"pause",
|
||||
False],
|
||||
"request_id": 103
|
||||
}
|
||||
resp = _send_ipc_command(cmd)
|
||||
if resp and resp.get("error") == "success":
|
||||
debug("Resumed playback")
|
||||
@@ -1280,7 +1384,12 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
|
||||
return 1
|
||||
|
||||
if pause_mode:
|
||||
cmd = {"command": ["set_property", "pause", True], "request_id": 104}
|
||||
cmd = {
|
||||
"command": ["set_property",
|
||||
"pause",
|
||||
True],
|
||||
"request_id": 104
|
||||
}
|
||||
resp = _send_ipc_command(cmd)
|
||||
if resp and resp.get("error") == "success":
|
||||
debug("Paused playback")
|
||||
@@ -1291,7 +1400,10 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
|
||||
|
||||
# Handle Clear All command (no index provided)
|
||||
if clear_mode and index_arg is None:
|
||||
cmd = {"command": ["playlist-clear"], "request_id": 105}
|
||||
cmd = {
|
||||
"command": ["playlist-clear"],
|
||||
"request_id": 105
|
||||
}
|
||||
resp = _send_ipc_command(cmd)
|
||||
if resp and resp.get("error") == "success":
|
||||
debug("Playlist cleared")
|
||||
@@ -1307,7 +1419,12 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
|
||||
idle_before = None
|
||||
try:
|
||||
idle_resp = _send_ipc_command(
|
||||
{"command": ["get_property", "idle-active"], "request_id": 111}, silent=True
|
||||
{
|
||||
"command": ["get_property",
|
||||
"idle-active"],
|
||||
"request_id": 111
|
||||
},
|
||||
silent=True
|
||||
)
|
||||
if idle_resp and idle_resp.get("error") == "success":
|
||||
idle_before = bool(idle_resp.get("data"))
|
||||
@@ -1326,7 +1443,9 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
|
||||
|
||||
# Debug: inspect incoming result and attributes
|
||||
try:
|
||||
debug(f"pipe._run: received result type={type(result)} repr={repr(result)[:200]}")
|
||||
debug(
|
||||
f"pipe._run: received result type={type(result)} repr={repr(result)[:200]}"
|
||||
)
|
||||
debug(
|
||||
f"pipe._run: attrs path={getattr(result, 'path', None)} url={getattr(result, 'url', None)} store={getattr(result, 'store', None)} hash={getattr(result, 'hash', None)}"
|
||||
)
|
||||
@@ -1334,7 +1453,9 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
|
||||
pass
|
||||
|
||||
queued_started_mpv = False
|
||||
if items_to_add and _queue_items(items_to_add, config=config, start_opts=start_opts):
|
||||
if items_to_add and _queue_items(items_to_add,
|
||||
config=config,
|
||||
start_opts=start_opts):
|
||||
mpv_started = True
|
||||
queued_started_mpv = True
|
||||
|
||||
@@ -1350,23 +1471,37 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
|
||||
if items_to_add and len(items_to_add) == 1 and not queued_started_mpv:
|
||||
try:
|
||||
playlist_after = _get_playlist(silent=True)
|
||||
before_len = len(playlist_before) if isinstance(playlist_before, list) else 0
|
||||
after_len = len(playlist_after) if isinstance(playlist_after, list) else 0
|
||||
before_len = len(playlist_before
|
||||
) if isinstance(playlist_before,
|
||||
list) else 0
|
||||
after_len = len(playlist_after
|
||||
) if isinstance(playlist_after,
|
||||
list) else 0
|
||||
|
||||
should_autoplay = False
|
||||
if idle_before is True:
|
||||
should_autoplay = True
|
||||
elif isinstance(playlist_before, list) and len(playlist_before) == 0:
|
||||
elif isinstance(playlist_before,
|
||||
list) and len(playlist_before) == 0:
|
||||
should_autoplay = True
|
||||
|
||||
if should_autoplay and after_len > 0:
|
||||
idx_to_play = min(max(0, before_len), after_len - 1)
|
||||
play_resp = _send_ipc_command(
|
||||
{"command": ["playlist-play-index", idx_to_play], "request_id": 112},
|
||||
{
|
||||
"command": ["playlist-play-index",
|
||||
idx_to_play],
|
||||
"request_id": 112
|
||||
},
|
||||
silent=True,
|
||||
)
|
||||
_send_ipc_command(
|
||||
{"command": ["set_property", "pause", False], "request_id": 113},
|
||||
{
|
||||
"command": ["set_property",
|
||||
"pause",
|
||||
False],
|
||||
"request_id": 113
|
||||
},
|
||||
silent=True,
|
||||
)
|
||||
if play_resp and play_resp.get("error") == "success":
|
||||
@@ -1462,7 +1597,11 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
|
||||
|
||||
if clear_mode:
|
||||
# Remove item
|
||||
cmd = {"command": ["playlist-remove", idx], "request_id": 101}
|
||||
cmd = {
|
||||
"command": ["playlist-remove",
|
||||
idx],
|
||||
"request_id": 101
|
||||
}
|
||||
resp = _send_ipc_command(cmd)
|
||||
if resp and resp.get("error") == "success":
|
||||
debug(f"Removed: {title}")
|
||||
@@ -1479,16 +1618,25 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
|
||||
# Play item
|
||||
if hydrus_header and _is_hydrus_path(filename, hydrus_url):
|
||||
header_cmd = {
|
||||
"command": ["set_property", "http-header-fields", hydrus_header],
|
||||
"command":
|
||||
["set_property",
|
||||
"http-header-fields",
|
||||
hydrus_header],
|
||||
"request_id": 198,
|
||||
}
|
||||
_send_ipc_command(header_cmd, silent=True)
|
||||
cmd = {"command": ["playlist-play-index", idx], "request_id": 102}
|
||||
cmd = {
|
||||
"command": ["playlist-play-index",
|
||||
idx],
|
||||
"request_id": 102
|
||||
}
|
||||
resp = _send_ipc_command(cmd)
|
||||
if resp and resp.get("error") == "success":
|
||||
# Ensure playback starts (unpause)
|
||||
unpause_cmd = {
|
||||
"command": ["set_property", "pause", False],
|
||||
"command": ["set_property",
|
||||
"pause",
|
||||
False],
|
||||
"request_id": 103,
|
||||
}
|
||||
_send_ipc_command(unpause_cmd)
|
||||
@@ -1523,7 +1671,10 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
|
||||
|
||||
file_storage = Store(config)
|
||||
except Exception as e:
|
||||
debug(f"Warning: Could not initialize Store registry: {e}", file=sys.stderr)
|
||||
debug(
|
||||
f"Warning: Could not initialize Store registry: {e}",
|
||||
file=sys.stderr
|
||||
)
|
||||
|
||||
# Use the loaded playlist name if available, otherwise default
|
||||
# Note: current_playlist_name is defined in the load_mode block if a playlist was loaded
|
||||
@@ -1556,7 +1707,10 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
|
||||
file_hash = hash_match.group(1)
|
||||
# Try to find which Hydrus instance has this file
|
||||
if file_storage:
|
||||
store_name = _find_hydrus_instance_for_hash(file_hash, file_storage)
|
||||
store_name = _find_hydrus_instance_for_hash(
|
||||
file_hash,
|
||||
file_storage
|
||||
)
|
||||
if not store_name:
|
||||
store_name = "hydrus"
|
||||
# Check if it's a hash-based local file
|
||||
@@ -1564,7 +1718,8 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
|
||||
# Try to extract hash from filename (e.g., C:\path\1e8c46...a1b2.mp4)
|
||||
path_obj = Path(real_path)
|
||||
stem = path_obj.stem # filename without extension
|
||||
if len(stem) == 64 and all(c in "0123456789abcdef" for c in stem.lower()):
|
||||
if len(stem) == 64 and all(c in "0123456789abcdef"
|
||||
for c in stem.lower()):
|
||||
file_hash = stem.lower()
|
||||
# Find which folder store has this file
|
||||
if file_storage:
|
||||
@@ -1574,7 +1729,8 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
|
||||
# Check if this backend has the file
|
||||
try:
|
||||
result_path = backend.get_file(file_hash)
|
||||
if isinstance(result_path, Path) and result_path.exists():
|
||||
if isinstance(result_path,
|
||||
Path) and result_path.exists():
|
||||
store_name = backend_name
|
||||
break
|
||||
except Exception:
|
||||
@@ -1582,7 +1738,10 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
|
||||
|
||||
# Fallback to inferred store if we couldn't find it
|
||||
if not store_name:
|
||||
store_name = _infer_store_from_playlist_item(item, file_storage=file_storage)
|
||||
store_name = _infer_store_from_playlist_item(
|
||||
item,
|
||||
file_storage=file_storage
|
||||
)
|
||||
|
||||
# Build PipeObject with proper metadata
|
||||
pipe_obj = PipeObject(
|
||||
@@ -1701,8 +1860,10 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
|
||||
|
||||
def _start_mpv(
|
||||
items: List[Any],
|
||||
config: Optional[Dict[str, Any]] = None,
|
||||
start_opts: Optional[Dict[str, Any]] = None,
|
||||
config: Optional[Dict[str,
|
||||
Any]] = None,
|
||||
start_opts: Optional[Dict[str,
|
||||
Any]] = None,
|
||||
) -> None:
|
||||
"""Start MPV with a list of items."""
|
||||
import time as _time_module
|
||||
@@ -1772,12 +1933,21 @@ def _start_mpv(
|
||||
time.sleep(0.3) # Give MPV a moment to process the queued items
|
||||
|
||||
# Play the first item (index 0) and unpause
|
||||
play_cmd = {"command": ["playlist-play-index", 0], "request_id": 102}
|
||||
play_cmd = {
|
||||
"command": ["playlist-play-index",
|
||||
0],
|
||||
"request_id": 102
|
||||
}
|
||||
play_resp = _send_ipc_command(play_cmd, silent=True)
|
||||
|
||||
if play_resp and play_resp.get("error") == "success":
|
||||
# Ensure playback starts (unpause)
|
||||
unpause_cmd = {"command": ["set_property", "pause", False], "request_id": 103}
|
||||
unpause_cmd = {
|
||||
"command": ["set_property",
|
||||
"pause",
|
||||
False],
|
||||
"request_id": 103
|
||||
}
|
||||
_send_ipc_command(unpause_cmd, silent=True)
|
||||
debug("Auto-playing first item")
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ from SYS.logger import log
|
||||
from result_table import ResultTable
|
||||
import pipeline as ctx
|
||||
|
||||
|
||||
_TELEGRAM_PENDING_ITEMS_KEY = "telegram_pending_items"
|
||||
|
||||
|
||||
@@ -101,6 +100,7 @@ def _extract_title(item: Any) -> str:
|
||||
|
||||
|
||||
def _extract_file_path(item: Any) -> Optional[str]:
|
||||
|
||||
def _maybe(value: Any) -> Optional[str]:
|
||||
if value is None:
|
||||
return None
|
||||
@@ -194,13 +194,18 @@ def _run(_result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
|
||||
pass
|
||||
try:
|
||||
chat_usernames = list(
|
||||
dict.fromkeys([str(u).strip() for u in chat_usernames if str(u).strip()])
|
||||
dict.fromkeys(
|
||||
[str(u).strip() for u in chat_usernames if str(u).strip()]
|
||||
)
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if not chat_ids and not chat_usernames:
|
||||
log("No Telegram chat selected (use @N on the Telegram table)", file=sys.stderr)
|
||||
log(
|
||||
"No Telegram chat selected (use @N on the Telegram table)",
|
||||
file=sys.stderr
|
||||
)
|
||||
return 1
|
||||
|
||||
pending_items = ctx.load_value(_TELEGRAM_PENDING_ITEMS_KEY, default=[])
|
||||
@@ -221,7 +226,10 @@ def _run(_result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
|
||||
)
|
||||
continue
|
||||
title = _extract_title(item)
|
||||
file_jobs.append({"path": p, "title": title})
|
||||
file_jobs.append({
|
||||
"path": p,
|
||||
"title": title
|
||||
})
|
||||
|
||||
# De-dupe file paths (preserve order).
|
||||
try:
|
||||
@@ -242,7 +250,9 @@ def _run(_result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
|
||||
|
||||
try:
|
||||
provider.send_files_to_chats(
|
||||
chat_ids=chat_ids, usernames=chat_usernames, files=file_jobs
|
||||
chat_ids=chat_ids,
|
||||
usernames=chat_usernames,
|
||||
files=file_jobs
|
||||
)
|
||||
except Exception as exc:
|
||||
log(f"Telegram send failed: {exc}", file=sys.stderr)
|
||||
@@ -276,7 +286,8 @@ def _run(_result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
|
||||
# Only show dialogs you can typically post to.
|
||||
try:
|
||||
rows = [
|
||||
r for r in (rows or []) if str(r.get("type") or "").strip().lower() in {"group", "user"}
|
||||
r for r in (rows or [])
|
||||
if str(r.get("type") or "").strip().lower() in {"group", "user"}
|
||||
]
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -11,11 +11,19 @@ from cmdlet import register
|
||||
from cmdlet._shared import Cmdlet, CmdletArg
|
||||
import pipeline as ctx
|
||||
from SYS.logger import log
|
||||
from config import get_local_storage_path
|
||||
from SYS.config import get_local_storage_path
|
||||
|
||||
DEFAULT_LIMIT = 100
|
||||
WORKER_STATUS_FILTERS = {"running", "completed", "error", "cancelled"}
|
||||
HELP_FLAGS = {"-?", "/?", "--help", "-h", "help", "--cmdlet"}
|
||||
WORKER_STATUS_FILTERS = {"running",
|
||||
"completed",
|
||||
"error",
|
||||
"cancelled"}
|
||||
HELP_FLAGS = {"-?",
|
||||
"/?",
|
||||
"--help",
|
||||
"-h",
|
||||
"help",
|
||||
"--cmdlet"}
|
||||
|
||||
CMDLET = Cmdlet(
|
||||
name=".worker",
|
||||
@@ -28,14 +36,21 @@ CMDLET = Cmdlet(
|
||||
requires_db=True,
|
||||
),
|
||||
CmdletArg(
|
||||
"limit", type="integer", description="Limit results (default: 100)", requires_db=True
|
||||
"limit",
|
||||
type="integer",
|
||||
description="Limit results (default: 100)",
|
||||
requires_db=True
|
||||
),
|
||||
CmdletArg(
|
||||
"@N",
|
||||
description="Select worker by index (1-based) and display full logs",
|
||||
requires_db=True,
|
||||
),
|
||||
CmdletArg("-id", description="Show full logs for a specific worker", requires_db=True),
|
||||
CmdletArg(
|
||||
"-id",
|
||||
description="Show full logs for a specific worker",
|
||||
requires_db=True
|
||||
),
|
||||
CmdletArg(
|
||||
"-clear",
|
||||
type="flag",
|
||||
@@ -75,7 +90,9 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
|
||||
"""Display workers table or show detailed logs for a specific worker."""
|
||||
args_list = [str(arg) for arg in (args or [])]
|
||||
selection_indices = ctx.get_last_selection()
|
||||
selection_requested = bool(selection_indices) and isinstance(result, list) and len(result) > 0
|
||||
selection_requested = bool(selection_indices) and isinstance(result,
|
||||
list
|
||||
) and len(result) > 0
|
||||
|
||||
if _has_help_flag(args_list):
|
||||
ctx.emit(CMDLET.__dict__)
|
||||
@@ -130,16 +147,20 @@ def _parse_worker_args(args_list: Sequence[str]) -> WorkerCommandOptions:
|
||||
while i < len(args_list):
|
||||
arg = args_list[i]
|
||||
low = arg.lower()
|
||||
if low in {"-limit", "--limit"} and i + 1 < len(args_list):
|
||||
if low in {"-limit",
|
||||
"--limit"} and i + 1 < len(args_list):
|
||||
options.limit = _normalize_limit(args_list[i + 1])
|
||||
i += 2
|
||||
elif low in {"-id", "--id"} and i + 1 < len(args_list):
|
||||
elif low in {"-id",
|
||||
"--id"} and i + 1 < len(args_list):
|
||||
options.worker_id = args_list[i + 1]
|
||||
i += 2
|
||||
elif low in {"-clear", "--clear"}:
|
||||
elif low in {"-clear",
|
||||
"--clear"}:
|
||||
options.clear = True
|
||||
i += 1
|
||||
elif low in {"-status", "--status"} and i + 1 < len(args_list):
|
||||
elif low in {"-status",
|
||||
"--status"} and i + 1 < len(args_list):
|
||||
options.status = args_list[i + 1].lower()
|
||||
i += 2
|
||||
elif low in WORKER_STATUS_FILTERS:
|
||||
@@ -163,7 +184,9 @@ def _normalize_limit(value: Any) -> int:
|
||||
def _render_worker_list(db, status_filter: str | None, limit: int) -> int:
|
||||
workers = db.get_all_workers(limit=limit)
|
||||
if status_filter:
|
||||
workers = [w for w in workers if str(w.get("status", "")).lower() == status_filter]
|
||||
workers = [
|
||||
w for w in workers if str(w.get("status", "")).lower() == status_filter
|
||||
]
|
||||
|
||||
if not workers:
|
||||
log("No workers found", file=sys.stderr)
|
||||
@@ -179,14 +202,22 @@ def _render_worker_list(db, status_filter: str | None, limit: int) -> int:
|
||||
|
||||
item = {
|
||||
"columns": [
|
||||
("Status", worker.get("status", "")),
|
||||
("Pipe", _summarize_pipe(worker.get("pipe"))),
|
||||
("Date", date_str),
|
||||
("Start Time", start_time),
|
||||
("End Time", end_time),
|
||||
("Status",
|
||||
worker.get("status",
|
||||
"")),
|
||||
("Pipe",
|
||||
_summarize_pipe(worker.get("pipe"))),
|
||||
("Date",
|
||||
date_str),
|
||||
("Start Time",
|
||||
start_time),
|
||||
("End Time",
|
||||
end_time),
|
||||
],
|
||||
"__worker_metadata": worker,
|
||||
"_selection_args": ["-id", worker.get("worker_id")],
|
||||
"__worker_metadata":
|
||||
worker,
|
||||
"_selection_args": ["-id",
|
||||
worker.get("worker_id")],
|
||||
}
|
||||
ctx.emit(item)
|
||||
return 0
|
||||
@@ -206,8 +237,8 @@ def _render_worker_selection(db, selected_items: Any) -> int:
|
||||
try:
|
||||
events = (
|
||||
db.get_worker_events(worker.get("worker_id"))
|
||||
if hasattr(db, "get_worker_events")
|
||||
else []
|
||||
if hasattr(db,
|
||||
"get_worker_events") else []
|
||||
)
|
||||
except Exception:
|
||||
events = []
|
||||
@@ -267,9 +298,12 @@ def _emit_worker_detail(worker: Dict[str, Any], events: List[Dict[str, Any]]) ->
|
||||
|
||||
item = {
|
||||
"columns": [
|
||||
("Time", timestamp),
|
||||
("Level", level),
|
||||
("Message", message),
|
||||
("Time",
|
||||
timestamp),
|
||||
("Level",
|
||||
level),
|
||||
("Message",
|
||||
message),
|
||||
]
|
||||
}
|
||||
ctx.emit(item)
|
||||
@@ -281,7 +315,7 @@ def _summarize_pipe(pipe_value: Any, limit: int = 60) -> str:
|
||||
text = str(pipe_value or "").strip()
|
||||
if not text:
|
||||
return "(none)"
|
||||
return text if len(text) <= limit else text[: limit - 3] + "..."
|
||||
return text if len(text) <= limit else text[:limit - 3] + "..."
|
||||
|
||||
|
||||
def _format_event_timestamp(raw_timestamp: Any) -> str:
|
||||
|
||||
Reference in New Issue
Block a user