dfdsf
This commit is contained in:
256
cmdnats/pipe.py
256
cmdnats/pipe.py
@@ -1,54 +1,34 @@
|
||||
from typing import Any, Dict, Sequence, List, Optional
|
||||
import sys
|
||||
import json
|
||||
import platform
|
||||
import socket
|
||||
import re
|
||||
import subprocess
|
||||
from urllib.parse import urlparse, parse_qs
|
||||
from pathlib import Path
|
||||
from cmdlets._shared import Cmdlet, CmdletArg, parse_cmdlet_args
|
||||
from helper.logger import log, debug
|
||||
from SYS.logger import debug
|
||||
from result_table import ResultTable
|
||||
from helper.mpv_ipc import get_ipc_pipe_path, MPVIPCClient
|
||||
from MPV.mpv_ipc import MPV
|
||||
import pipeline as ctx
|
||||
from helper.download import is_url_supported_by_ytdlp
|
||||
from SYS.download import is_url_supported_by_ytdlp
|
||||
from models import PipeObject
|
||||
|
||||
from helper.folder_store import LocalLibrarySearchOptimizer
|
||||
from API.folder import LocalLibrarySearchOptimizer
|
||||
from config import get_local_storage_path, get_hydrus_access_key, get_hydrus_url
|
||||
from hydrus_health_check import get_cookies_file_path
|
||||
|
||||
def _send_ipc_command(command: Dict[str, Any], silent: bool = False) -> Optional[Any]:
|
||||
"""Send a command to the MPV IPC pipe and return the response."""
|
||||
try:
|
||||
ipc_pipe = get_ipc_pipe_path()
|
||||
client = MPVIPCClient(socket_path=ipc_pipe)
|
||||
|
||||
if not client.connect():
|
||||
return None # MPV not running
|
||||
|
||||
response = client.send_command(command)
|
||||
client.disconnect()
|
||||
return response
|
||||
mpv = MPV()
|
||||
return mpv.send(command, silent=silent)
|
||||
except Exception as e:
|
||||
if not silent:
|
||||
debug(f"IPC Error: {e}", file=sys.stderr)
|
||||
return None
|
||||
|
||||
|
||||
def _is_mpv_running() -> bool:
|
||||
"""Check if MPV is currently running and accessible via IPC."""
|
||||
try:
|
||||
ipc_pipe = get_ipc_pipe_path()
|
||||
client = MPVIPCClient(socket_path=ipc_pipe)
|
||||
if client.connect():
|
||||
client.disconnect()
|
||||
return True
|
||||
return False
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
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}
|
||||
@@ -194,8 +174,7 @@ def _normalize_playlist_path(text: Optional[str]) -> Optional[str]:
|
||||
pass
|
||||
|
||||
# Normalize slashes for Windows paths and lowercase for comparison
|
||||
real = real.replace('\\', '\\')
|
||||
real = real.replace('\\', '\\')
|
||||
real = real.replace('\\', '/')
|
||||
return real.lower()
|
||||
|
||||
|
||||
@@ -300,37 +279,6 @@ def _infer_store_from_playlist_item(item: Dict[str, Any], file_storage: Optional
|
||||
return host_stripped
|
||||
|
||||
|
||||
def _format_playlist_location(name: str, max_len: int = 48) -> str:
|
||||
"""Format playlist filename/URL for display while keeping backend untouched."""
|
||||
target = name or ""
|
||||
memory_target = _extract_target_from_memory_uri(target)
|
||||
if memory_target:
|
||||
target = memory_target
|
||||
|
||||
lower = target.lower()
|
||||
# Local paths: show basename only
|
||||
if re.match(r"^[a-z]:[\\/]", target, flags=re.IGNORECASE) or target.startswith("\\\\"):
|
||||
target = Path(target).name
|
||||
elif lower.startswith("file://"):
|
||||
parsed = urlparse(target)
|
||||
target = Path(parsed.path or "").name or target
|
||||
else:
|
||||
parsed = urlparse(target)
|
||||
host = parsed.netloc or ""
|
||||
if host:
|
||||
host_no_port = host.split(":", 1)[0]
|
||||
host_no_port = host_no_port[4:] if host_no_port.startswith("www.") else host_no_port
|
||||
tail = parsed.path.split('/')[-1] if parsed.path else ""
|
||||
if tail:
|
||||
target = f"{host_no_port}/{tail}"
|
||||
else:
|
||||
target = host_no_port
|
||||
|
||||
if len(target) > max_len:
|
||||
return target[: max_len - 3] + "..."
|
||||
return target
|
||||
|
||||
|
||||
def _build_hydrus_header(config: Dict[str, Any]) -> Optional[str]:
|
||||
"""Return header string for Hydrus auth if configured."""
|
||||
try:
|
||||
@@ -399,7 +347,8 @@ def _ensure_ytdl_cookies() -> None:
|
||||
def _monitor_mpv_logs(duration: float = 3.0) -> None:
|
||||
"""Monitor MPV logs for a short duration to capture errors."""
|
||||
try:
|
||||
client = MPVIPCClient()
|
||||
mpv = MPV()
|
||||
client = mpv.client()
|
||||
if not client.connect():
|
||||
debug("Failed to connect to MPV for log monitoring", file=sys.stderr)
|
||||
return
|
||||
@@ -416,9 +365,14 @@ def _monitor_mpv_logs(duration: float = 3.0) -> None:
|
||||
start_time = time.time()
|
||||
|
||||
# Unix sockets already have timeouts set; read until duration expires
|
||||
sock_obj = client.sock
|
||||
if not isinstance(sock_obj, socket.socket):
|
||||
client.disconnect()
|
||||
return
|
||||
|
||||
while time.time() - start_time < duration:
|
||||
try:
|
||||
chunk = client.sock.recv(4096)
|
||||
chunk = sock_obj.recv(4096)
|
||||
except socket.timeout:
|
||||
continue
|
||||
except Exception:
|
||||
@@ -451,15 +405,14 @@ def _get_playable_path(item: Any, file_storage: Optional[Any], config: Optional[
|
||||
Returns:
|
||||
Tuple of (path, title) or None if no valid path found
|
||||
"""
|
||||
path = None
|
||||
title = None
|
||||
store = None
|
||||
file_hash = None
|
||||
path: Optional[str] = None
|
||||
title: Optional[str] = None
|
||||
store: Optional[str] = None
|
||||
file_hash: Optional[str] = None
|
||||
|
||||
# Extract fields from item - prefer a disk path ('path'), but accept 'url' as fallback for providers
|
||||
if isinstance(item, dict):
|
||||
# Support both canonical 'path' and legacy 'file_path' keys, and provider 'url' keys
|
||||
path = item.get("path") or item.get("file_path")
|
||||
path = item.get("path")
|
||||
# Fallbacks for provider-style entries where URL is stored in 'url' or 'source_url' or 'target'
|
||||
if not path:
|
||||
path = item.get("url") or item.get("source_url") or item.get("target")
|
||||
@@ -468,11 +421,11 @@ def _get_playable_path(item: Any, file_storage: Optional[Any], config: Optional[
|
||||
if known and isinstance(known, list):
|
||||
path = known[0]
|
||||
title = item.get("title") or item.get("file_title")
|
||||
store = item.get("store") or item.get("storage") or item.get("storage_source") or item.get("origin")
|
||||
file_hash = item.get("hash") or item.get("file_hash") or item.get("hash_hex")
|
||||
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"):
|
||||
# Handle PipeObject / dataclass objects - prefer path, but fall back to url/source_url attributes
|
||||
path = getattr(item, "path", None) or getattr(item, "file_path", None)
|
||||
path = getattr(item, "path", None)
|
||||
if not path:
|
||||
path = getattr(item, "url", None) or getattr(item, "source_url", None) or getattr(item, "target", None)
|
||||
if not path:
|
||||
@@ -480,7 +433,7 @@ def _get_playable_path(item: Any, file_storage: Optional[Any], config: Optional[
|
||||
if known and isinstance(known, list):
|
||||
path = known[0]
|
||||
title = getattr(item, "title", None) or getattr(item, "file_title", None)
|
||||
store = getattr(item, "store", None) or getattr(item, "origin", None)
|
||||
store = getattr(item, "store", None)
|
||||
file_hash = getattr(item, "hash", None)
|
||||
elif isinstance(item, str):
|
||||
path = item
|
||||
@@ -493,56 +446,51 @@ def _get_playable_path(item: Any, file_storage: Optional[Any], config: Optional[
|
||||
|
||||
if not path:
|
||||
return None
|
||||
|
||||
if not isinstance(path, str):
|
||||
path = str(path)
|
||||
if title is not None and not isinstance(title, str):
|
||||
title = str(title)
|
||||
|
||||
# If we have a store and hash, use store's .pipe() method if available
|
||||
# Skip this for URL-based providers (YouTube, SoundCloud, etc.) which have hash="unknown"
|
||||
# Also skip if path is already a URL (http/https)
|
||||
# Resolve hash+store into a playable target (file path or URL).
|
||||
# This is unrelated to MPV's IPC pipe and keeps "pipe" terminology reserved for:
|
||||
# - MPV IPC pipe (transport)
|
||||
# - PipeObject (pipeline data)
|
||||
if store and file_hash and file_hash != "unknown" and file_storage:
|
||||
# Check if this is actually a URL - if so, just return it
|
||||
# If it's already a URL, MPV can usually play it directly.
|
||||
if path.startswith(("http://", "https://")):
|
||||
return (path, title)
|
||||
|
||||
|
||||
try:
|
||||
backend = file_storage[store]
|
||||
# Check if backend has a .pipe() method
|
||||
if hasattr(backend, 'pipe') and callable(backend.pipe):
|
||||
pipe_path = backend.pipe(file_hash, config)
|
||||
if pipe_path:
|
||||
path = pipe_path
|
||||
debug(f"Got pipe path from {store} backend: {path}")
|
||||
except KeyError:
|
||||
# Store not found in file_storage - it could be a search provider (youtube, bandcamp, etc.)
|
||||
from helper.provider import get_search_provider
|
||||
try:
|
||||
provider = get_search_provider(store, config or {})
|
||||
if provider and hasattr(provider, 'pipe') and callable(provider.pipe):
|
||||
try:
|
||||
debug(f"Calling provider.pipe for '{store}' with path: {path}")
|
||||
provider_path = provider.pipe(path, config or {})
|
||||
debug(f"provider.pipe returned: {provider_path}")
|
||||
if provider_path:
|
||||
path = provider_path
|
||||
debug(f"Got pipe path from provider '{store}': {path}")
|
||||
except Exception as e:
|
||||
debug(f"Error in provider.pipe for '{store}': {e}", file=sys.stderr)
|
||||
except Exception as e:
|
||||
debug(f"Error calling provider.pipe for '{store}': {e}", file=sys.stderr)
|
||||
except Exception as e:
|
||||
debug(f"Error calling .pipe() on store '{store}': {e}", file=sys.stderr)
|
||||
|
||||
# As a fallback, if a provider exists for this store (e.g., youtube) and
|
||||
# this store is not part of FileStorage backends, call provider.pipe()
|
||||
if store and (not file_storage or store not in (file_storage.list_backends() if file_storage else [])):
|
||||
try:
|
||||
from helper.provider import get_search_provider
|
||||
provider = get_search_provider(store, config or {})
|
||||
if provider and hasattr(provider, 'pipe') and callable(provider.pipe):
|
||||
provider_path = provider.pipe(path, config or {})
|
||||
if provider_path:
|
||||
path = provider_path
|
||||
debug(f"Got pipe path from provider '{store}' (fallback): {path}")
|
||||
except Exception as e:
|
||||
debug(f"Error calling provider.pipe (fallback) for '{store}': {e}", file=sys.stderr)
|
||||
except Exception:
|
||||
backend = None
|
||||
|
||||
if backend is not None:
|
||||
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":
|
||||
try:
|
||||
resolved = backend.get_file(file_hash)
|
||||
if isinstance(resolved, Path):
|
||||
path = str(resolved)
|
||||
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)
|
||||
|
||||
# HydrusNetwork: build a playable API file URL without browser side-effects.
|
||||
elif backend_class == "HydrusNetwork":
|
||||
try:
|
||||
client = getattr(backend, "_client", None)
|
||||
base_url = getattr(client, "url", None)
|
||||
access_key = getattr(client, "access_key", None)
|
||||
if base_url and access_key:
|
||||
base_url = str(base_url).rstrip("/")
|
||||
path = f"{base_url}/get_files/file?hash={file_hash}&Hydrus-Client-API-Access-Key={access_key}"
|
||||
except Exception as e:
|
||||
debug(f"Error building Hydrus URL from store '{store}': {e}", file=sys.stderr)
|
||||
|
||||
return (path, title)
|
||||
|
||||
@@ -574,13 +522,13 @@ def _queue_items(items: List[Any], clear_first: bool = False, config: Optional[D
|
||||
except Exception:
|
||||
hydrus_url = None
|
||||
|
||||
# Initialize FileStorage for path resolution
|
||||
# Initialize Store registry for path resolution
|
||||
file_storage = None
|
||||
try:
|
||||
from helper.store import FileStorage
|
||||
file_storage = FileStorage(config or {})
|
||||
from Store import Store
|
||||
file_storage = Store(config or {})
|
||||
except Exception as e:
|
||||
debug(f"Warning: Could not initialize FileStorage: {e}", file=sys.stderr)
|
||||
debug(f"Warning: Could not initialize Store registry: {e}", file=sys.stderr)
|
||||
|
||||
# Dedupe existing playlist before adding more (unless we're replacing it)
|
||||
existing_targets: set[str] = set()
|
||||
@@ -695,13 +643,13 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
|
||||
|
||||
parsed = parse_cmdlet_args(args, CMDLET)
|
||||
|
||||
# Initialize FileStorage for detecting Hydrus instance names
|
||||
# Initialize Store registry for detecting Hydrus instance names
|
||||
file_storage = None
|
||||
try:
|
||||
from helper.store import FileStorage
|
||||
file_storage = FileStorage(config)
|
||||
from Store import Store
|
||||
file_storage = Store(config)
|
||||
except Exception as e:
|
||||
debug(f"Warning: Could not initialize FileStorage: {e}", file=sys.stderr)
|
||||
debug(f"Warning: Could not initialize Store registry: {e}", file=sys.stderr)
|
||||
|
||||
# Initialize mpv_started flag
|
||||
mpv_started = False
|
||||
@@ -1119,7 +1067,7 @@ 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 result_path and result_path.exists():
|
||||
if isinstance(result_path, Path) and result_path.exists():
|
||||
store_name = backend_name
|
||||
break
|
||||
except Exception:
|
||||
@@ -1130,7 +1078,6 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
|
||||
store_name = _infer_store_from_playlist_item(item, file_storage=file_storage)
|
||||
|
||||
# Build PipeObject with proper metadata
|
||||
from models import PipeObject
|
||||
pipe_obj = PipeObject(
|
||||
hash=file_hash or "unknown",
|
||||
store=store_name or "unknown",
|
||||
@@ -1163,23 +1110,11 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
|
||||
|
||||
def _start_mpv(items: List[Any], config: Optional[Dict[str, Any]] = None) -> None:
|
||||
"""Start MPV with a list of items."""
|
||||
import subprocess
|
||||
import time as _time_module
|
||||
|
||||
# Kill any existing MPV processes to ensure clean start
|
||||
try:
|
||||
subprocess.run(['taskkill', '/IM', 'mpv.exe', '/F'],
|
||||
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL, timeout=2)
|
||||
_time_module.sleep(0.5) # Wait for process to die
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
ipc_pipe = get_ipc_pipe_path()
|
||||
|
||||
# Start MPV in idle mode with IPC server
|
||||
cmd = ['mpv', f'--input-ipc-server={ipc_pipe}', '--idle', '--force-window']
|
||||
cmd.append('--ytdl-format=bestvideo[height<=?1080]+bestaudio/best[height<=?1080]')
|
||||
|
||||
mpv = MPV()
|
||||
mpv.kill_existing_windows()
|
||||
_time_module.sleep(0.5) # Wait for process to die
|
||||
|
||||
hydrus_header = _build_hydrus_header(config or {})
|
||||
ytdl_opts = _build_ytdl_options(config, hydrus_header)
|
||||
@@ -1190,35 +1125,26 @@ def _start_mpv(items: List[Any], config: Optional[Dict[str, Any]] = None) -> Non
|
||||
else:
|
||||
debug("Starting MPV with browser cookies: chrome")
|
||||
|
||||
if ytdl_opts:
|
||||
cmd.append(f'--ytdl-raw-options={ytdl_opts}')
|
||||
|
||||
try:
|
||||
kwargs = {}
|
||||
if platform.system() == 'Windows':
|
||||
kwargs['creationflags'] = 0x00000008 # DETACHED_PROCESS
|
||||
|
||||
# Log the complete MPV command being executed
|
||||
debug(f"DEBUG: Full MPV command: {' '.join(cmd)}")
|
||||
|
||||
if hydrus_header:
|
||||
cmd.append(f'--http-header-fields={hydrus_header}')
|
||||
subprocess.Popen(cmd, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.PIPE, **kwargs)
|
||||
debug(f"Started MPV process")
|
||||
# Always start MPV with the bundled Lua script via MPV class.
|
||||
mpv.start(
|
||||
extra_args=[
|
||||
'--ytdl-format=bestvideo[height<=?1080]+bestaudio/best[height<=?1080]',
|
||||
],
|
||||
ytdl_raw_options=ytdl_opts,
|
||||
http_header_fields=hydrus_header,
|
||||
detached=True,
|
||||
)
|
||||
debug("Started MPV process")
|
||||
|
||||
# Wait for IPC pipe to be ready
|
||||
import time
|
||||
max_retries = 20
|
||||
for i in range(max_retries):
|
||||
time.sleep(0.2)
|
||||
client = MPVIPCClient(socket_path=ipc_pipe)
|
||||
if client.connect():
|
||||
client.disconnect()
|
||||
break
|
||||
else:
|
||||
if not mpv.wait_for_ipc(retries=20, delay_seconds=0.2):
|
||||
debug("Timed out waiting for MPV IPC connection", file=sys.stderr)
|
||||
return
|
||||
|
||||
# Ensure Lua script is loaded (redundant when started with --script, but safe)
|
||||
mpv.ensure_lua_loaded()
|
||||
|
||||
# Queue items via IPC
|
||||
if items:
|
||||
_queue_items(items, config=config)
|
||||
|
||||
Reference in New Issue
Block a user