d
This commit is contained in:
128
cmdnat/pipe.py
128
cmdnat/pipe.py
@@ -12,7 +12,6 @@ from SYS.logger import debug, get_thread_stream, is_debug_enabled, set_debug, se
|
||||
from result_table import ResultTable
|
||||
from MPV.mpv_ipc import MPV
|
||||
import pipeline as ctx
|
||||
from SYS.download import is_url_supported_by_ytdlp
|
||||
from models import PipeObject
|
||||
|
||||
from API.folder import LocalLibrarySearchOptimizer
|
||||
@@ -20,6 +19,78 @@ from config import get_local_storage_path, get_hydrus_access_key, get_hydrus_url
|
||||
from hydrus_health_check import get_cookies_file_path
|
||||
|
||||
|
||||
_ALLDEBRID_UNLOCK_CACHE: Dict[str, str] = {}
|
||||
|
||||
|
||||
def _get_alldebrid_api_key(config: Optional[Dict[str, Any]]) -> Optional[str]:
|
||||
try:
|
||||
if not isinstance(config, dict):
|
||||
return None
|
||||
provider_cfg = config.get("provider")
|
||||
if not isinstance(provider_cfg, dict):
|
||||
return None
|
||||
ad_cfg = provider_cfg.get("alldebrid")
|
||||
if not isinstance(ad_cfg, dict):
|
||||
return None
|
||||
key = ad_cfg.get("api_key")
|
||||
if not isinstance(key, str):
|
||||
return None
|
||||
key = key.strip()
|
||||
return key or None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _is_alldebrid_protected_url(url: str) -> bool:
|
||||
try:
|
||||
if not isinstance(url, str):
|
||||
return False
|
||||
u = url.strip()
|
||||
if not u.startswith(("http://", "https://")):
|
||||
return False
|
||||
p = urlparse(u)
|
||||
host = (p.netloc or "").lower()
|
||||
path = p.path or ""
|
||||
# AllDebrid file page links (require auth; not directly streamable by mpv)
|
||||
return host == "alldebrid.com" and path.startswith("/f/")
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def _maybe_unlock_alldebrid_url(url: str, config: Optional[Dict[str, Any]]) -> str:
|
||||
"""Convert AllDebrid protected file URLs into direct streamable links.
|
||||
|
||||
When AllDebrid returns `https://alldebrid.com/f/...`, that URL typically requires
|
||||
authentication. MPV cannot access it without credentials. We transparently call
|
||||
the AllDebrid API `link/unlock` (using the configured API key) to obtain a direct
|
||||
URL that MPV can stream.
|
||||
"""
|
||||
if not _is_alldebrid_protected_url(url):
|
||||
return url
|
||||
|
||||
cached = _ALLDEBRID_UNLOCK_CACHE.get(url)
|
||||
if isinstance(cached, str) and cached:
|
||||
return cached
|
||||
|
||||
api_key = _get_alldebrid_api_key(config)
|
||||
if not api_key:
|
||||
return url
|
||||
|
||||
try:
|
||||
from API.alldebrid import AllDebridClient
|
||||
|
||||
client = AllDebridClient(api_key)
|
||||
unlocked = client.unlock_link(url)
|
||||
if isinstance(unlocked, str) and unlocked.strip():
|
||||
unlocked = unlocked.strip()
|
||||
_ALLDEBRID_UNLOCK_CACHE[url] = unlocked
|
||||
return unlocked
|
||||
except Exception as e:
|
||||
debug(f"AllDebrid unlock failed for MPV target: {e}", file=sys.stderr)
|
||||
|
||||
return url
|
||||
|
||||
|
||||
def _ensure_lyric_overlay(mpv: MPV) -> None:
|
||||
try:
|
||||
mpv.ensure_lyric_loader_running()
|
||||
@@ -621,6 +692,13 @@ def _queue_items(
|
||||
|
||||
target, title = result
|
||||
|
||||
# If the target is an AllDebrid protected file URL, unlock it to a direct link for MPV.
|
||||
try:
|
||||
if isinstance(target, str):
|
||||
target = _maybe_unlock_alldebrid_url(target, config)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Prefer per-item Hydrus instance credentials when the item belongs to a Hydrus store.
|
||||
effective_hydrus_url = hydrus_url
|
||||
effective_hydrus_header = hydrus_header
|
||||
@@ -665,21 +743,10 @@ def _queue_items(
|
||||
continue
|
||||
new_targets.add(norm_key)
|
||||
|
||||
# Check if it's a yt-dlp supported URL
|
||||
is_ytdlp = False
|
||||
# Treat any http(s) target as yt-dlp candidate. If the Python yt-dlp
|
||||
# module is available we also check more deeply, but default to True
|
||||
# so MPV can use its ytdl hooks for remote streaming sites.
|
||||
is_hydrus_target = _is_hydrus_path(str(target), effective_hydrus_url)
|
||||
try:
|
||||
# Hydrus direct file URLs should not be treated as yt-dlp targets.
|
||||
is_ytdlp = (not is_hydrus_target) and (target.startswith("http") or is_url_supported_by_ytdlp(target))
|
||||
except Exception:
|
||||
is_ytdlp = (not is_hydrus_target) and target.startswith("http")
|
||||
|
||||
# Use memory:// M3U hack to pass title to MPV
|
||||
# Skip for yt-dlp url to ensure proper handling
|
||||
if title and (is_hydrus_target or not is_ytdlp):
|
||||
# Use memory:// M3U hack to pass title to MPV.
|
||||
# This is especially important for remote URLs (e.g., YouTube) where MPV may otherwise
|
||||
# show the raw URL as the playlist title.
|
||||
if title:
|
||||
# Sanitize title for M3U (remove newlines)
|
||||
safe_title = title.replace('\n', ' ').replace('\r', '')
|
||||
|
||||
@@ -703,8 +770,9 @@ def _queue_items(
|
||||
if clear_first and i == 0:
|
||||
mode = "replace"
|
||||
|
||||
# If this is a Hydrus path, set header property and yt-dlp headers before loading
|
||||
if effective_hydrus_header and _is_hydrus_path(target_to_send, effective_hydrus_url):
|
||||
# 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):
|
||||
header_cmd = {"command": ["set_property", "http-header-fields", effective_hydrus_header], "request_id": 199}
|
||||
_send_ipc_command(header_cmd, silent=True)
|
||||
if effective_ytdl_opts:
|
||||
@@ -727,10 +795,8 @@ def _queue_items(
|
||||
_start_mpv(items[i:], config=config, start_opts=start_opts)
|
||||
return True
|
||||
elif resp.get("error") == "success":
|
||||
# Also set property for good measure
|
||||
if title:
|
||||
title_cmd = {"command": ["set_property", "force-media-title", title], "request_id": 201}
|
||||
_send_ipc_command(title_cmd)
|
||||
# Do not set `force-media-title` when queueing items. It's a global property and
|
||||
# would change the MPV window title even if the item isn't currently playing.
|
||||
debug(f"Queued: {title or target}")
|
||||
else:
|
||||
error_msg = str(resp.get('error'))
|
||||
@@ -1008,7 +1074,10 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
|
||||
ctx.set_last_result_table_overlay(table, [p['items'] for p in playlists])
|
||||
ctx.set_current_stage_table(table)
|
||||
|
||||
print(table)
|
||||
# In pipeline mode, the CLI renders current-stage tables; printing here duplicates output.
|
||||
suppress_direct_print = bool(isinstance(config, dict) and config.get("_quiet_background_output"))
|
||||
if not suppress_direct_print:
|
||||
print(table)
|
||||
return 0
|
||||
|
||||
# Everything below was originally outside a try block; keep it inside so `start_opts` is in scope.
|
||||
@@ -1153,9 +1222,11 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
|
||||
debug("MPV is starting up...")
|
||||
return 0
|
||||
|
||||
debug("MPV is not running. Starting new instance...")
|
||||
_start_mpv([], config=config, start_opts=start_opts)
|
||||
return 0
|
||||
# IPC is ready; continue without restarting MPV again.
|
||||
else:
|
||||
debug("MPV is not running. Starting new instance...")
|
||||
_start_mpv([], config=config, start_opts=start_opts)
|
||||
return 0
|
||||
|
||||
if not items:
|
||||
debug("MPV playlist is empty.")
|
||||
@@ -1314,7 +1385,10 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
|
||||
ctx.set_last_result_table_overlay(table, pipe_objects)
|
||||
ctx.set_current_stage_table(table)
|
||||
|
||||
print(table)
|
||||
# In pipeline mode, the CLI renders current-stage tables; printing here duplicates output.
|
||||
suppress_direct_print = bool(isinstance(config, dict) and config.get("_quiet_background_output"))
|
||||
if not suppress_direct_print:
|
||||
print(table)
|
||||
|
||||
return 0
|
||||
finally:
|
||||
|
||||
Reference in New Issue
Block a user