your commit message
This commit is contained in:
@@ -92,7 +92,7 @@
|
|||||||
"(hitfile\\.net/[a-z0-9A-Z]{4,9})"
|
"(hitfile\\.net/[a-z0-9A-Z]{4,9})"
|
||||||
],
|
],
|
||||||
"regexp": "(hitf\\.(to|cc)/([a-z0-9A-Z]{4,9}))|(htfl\\.(net|to|cc)/([a-z0-9A-Z]{4,9}))|(hitfile\\.(net)/download/free/([a-z0-9A-Z]{4,9}))|((hitfile\\.net/[a-z0-9A-Z]{4,9}))",
|
"regexp": "(hitf\\.(to|cc)/([a-z0-9A-Z]{4,9}))|(htfl\\.(net|to|cc)/([a-z0-9A-Z]{4,9}))|(hitfile\\.(net)/download/free/([a-z0-9A-Z]{4,9}))|((hitfile\\.net/[a-z0-9A-Z]{4,9}))",
|
||||||
"status": false
|
"status": true
|
||||||
},
|
},
|
||||||
"mega": {
|
"mega": {
|
||||||
"name": "mega",
|
"name": "mega",
|
||||||
@@ -482,7 +482,7 @@
|
|||||||
"(katfile\\.com/[0-9a-zA-Z]{12})"
|
"(katfile\\.com/[0-9a-zA-Z]{12})"
|
||||||
],
|
],
|
||||||
"regexp": "(katfile\\.(cloud|online|vip)/([0-9a-zA-Z]{12}))|((katfile\\.com/[0-9a-zA-Z]{12}))",
|
"regexp": "(katfile\\.(cloud|online|vip)/([0-9a-zA-Z]{12}))|((katfile\\.com/[0-9a-zA-Z]{12}))",
|
||||||
"status": false
|
"status": true
|
||||||
},
|
},
|
||||||
"mediafire": {
|
"mediafire": {
|
||||||
"name": "mediafire",
|
"name": "mediafire",
|
||||||
@@ -690,7 +690,7 @@
|
|||||||
"uploadrar\\.(net|com)/([0-9a-z]{12})"
|
"uploadrar\\.(net|com)/([0-9a-z]{12})"
|
||||||
],
|
],
|
||||||
"regexp": "((get|cloud)\\.rahim-soft\\.com/([0-9a-z]{12}))|((fingau\\.com/([0-9a-z]{12})))|((tech|miui|cloud|flash)\\.getpczone\\.com/([0-9a-z]{12}))|(miui.rahim-soft\\.com/([0-9a-z]{12}))|(uploadrar\\.(net|com)/([0-9a-z]{12}))",
|
"regexp": "((get|cloud)\\.rahim-soft\\.com/([0-9a-z]{12}))|((fingau\\.com/([0-9a-z]{12})))|((tech|miui|cloud|flash)\\.getpczone\\.com/([0-9a-z]{12}))|(miui.rahim-soft\\.com/([0-9a-z]{12}))|(uploadrar\\.(net|com)/([0-9a-z]{12}))",
|
||||||
"status": false,
|
"status": true,
|
||||||
"hardRedirect": [
|
"hardRedirect": [
|
||||||
"uploadrar.com/([0-9a-zA-Z]{12})"
|
"uploadrar.com/([0-9a-zA-Z]{12})"
|
||||||
]
|
]
|
||||||
@@ -775,7 +775,7 @@
|
|||||||
"(worldbytez\\.(net|com)/[a-zA-Z0-9]{12})"
|
"(worldbytez\\.(net|com)/[a-zA-Z0-9]{12})"
|
||||||
],
|
],
|
||||||
"regexp": "(worldbytez\\.(net|com)/[a-zA-Z0-9]{12})",
|
"regexp": "(worldbytez\\.(net|com)/[a-zA-Z0-9]{12})",
|
||||||
"status": false
|
"status": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"streams": {
|
"streams": {
|
||||||
|
|||||||
768
MPV/lyric.py
768
MPV/lyric.py
File diff suppressed because it is too large
Load Diff
@@ -1,2 +1,2 @@
|
|||||||
# Medeia MPV script options
|
# Medeia MPV script options
|
||||||
store=rpi
|
store=
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import subprocess
|
|||||||
import time
|
import time
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, List, Optional, Tuple
|
from typing import Any, Callable, Dict, List, Optional, Tuple
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from API.Tidal import (
|
from API.Tidal import (
|
||||||
@@ -457,15 +457,32 @@ class Tidal(Provider):
|
|||||||
if idx >= len(parts):
|
if idx >= len(parts):
|
||||||
return "", None
|
return "", None
|
||||||
|
|
||||||
view = parts[idx].lower()
|
# Scan ALL (view, id) pairs in the path, e.g.
|
||||||
if view not in {"album", "track", "artist"}:
|
# /album/634516/track/634519 → [("album", 634516), ("track", 634519)]
|
||||||
|
# When multiple views are present, prefer the more specific one:
|
||||||
|
# track > album > artist
|
||||||
|
_VIEW_PRIORITY = {"track": 2, "album": 1, "artist": 0}
|
||||||
|
_VALID_VIEWS = set(_VIEW_PRIORITY)
|
||||||
|
found: dict[str, int] = {}
|
||||||
|
i = idx
|
||||||
|
while i < len(parts):
|
||||||
|
v = parts[i].lower()
|
||||||
|
if v in _VALID_VIEWS:
|
||||||
|
# Look ahead for the first integer following this view keyword
|
||||||
|
for j in range(i + 1, len(parts)):
|
||||||
|
cand = self._parse_int(parts[j])
|
||||||
|
if cand is not None:
|
||||||
|
found[v] = cand
|
||||||
|
i = j # advance past the id
|
||||||
|
break
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
if not found:
|
||||||
return "", None
|
return "", None
|
||||||
|
|
||||||
for segment in parts[idx + 1:]:
|
# Return the highest-priority view that was found
|
||||||
identifier = self._parse_int(segment)
|
best_view = max(found, key=lambda v: _VIEW_PRIORITY.get(v, -1))
|
||||||
if identifier is not None:
|
return best_view, found[best_view]
|
||||||
return view, identifier
|
|
||||||
return view, None
|
|
||||||
|
|
||||||
def _track_detail_to_result(self, detail: Optional[Dict[str, Any]], track_id: int) -> SearchResult:
|
def _track_detail_to_result(self, detail: Optional[Dict[str, Any]], track_id: int) -> SearchResult:
|
||||||
if isinstance(detail, dict):
|
if isinstance(detail, dict):
|
||||||
@@ -700,7 +717,9 @@ class Tidal(Provider):
|
|||||||
|
|
||||||
def _tracks_for_album(self, *, album_id: Optional[int], album_title: str, artist_name: str = "", limit: int = 200) -> List[SearchResult]:
|
def _tracks_for_album(self, *, album_id: Optional[int], album_title: str, artist_name: str = "", limit: int = 200) -> List[SearchResult]:
|
||||||
title = str(album_title or "").strip()
|
title = str(album_title or "").strip()
|
||||||
if not title:
|
# When album_id is provided the /album/ endpoint can resolve tracks directly —
|
||||||
|
# no title is required. Only bail out early when we have neither.
|
||||||
|
if not title and not album_id:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def _norm_album(text: str) -> str:
|
def _norm_album(text: str) -> str:
|
||||||
@@ -1351,6 +1370,9 @@ class Tidal(Provider):
|
|||||||
subtitles = lyrics.get("subtitles")
|
subtitles = lyrics.get("subtitles")
|
||||||
if isinstance(subtitles, str) and subtitles.strip():
|
if isinstance(subtitles, str) and subtitles.strip():
|
||||||
md["_tidal_lyrics_subtitles"] = subtitles.strip()
|
md["_tidal_lyrics_subtitles"] = subtitles.strip()
|
||||||
|
# Generic key consumed by download-file._emit_local_file to
|
||||||
|
# persist lyrics as a store note without provider-specific logic.
|
||||||
|
md["_notes"] = {"lyric": subtitles.strip()}
|
||||||
|
|
||||||
# Ensure downstream cmdlets see our enriched metadata.
|
# Ensure downstream cmdlets see our enriched metadata.
|
||||||
try:
|
try:
|
||||||
@@ -1523,6 +1545,19 @@ class Tidal(Provider):
|
|||||||
if not identifier:
|
if not identifier:
|
||||||
return False, None
|
return False, None
|
||||||
|
|
||||||
|
# In download-file flows, return a provider action so the cmdlet can
|
||||||
|
# invoke this provider's bulk download hook and emit each track.
|
||||||
|
if output_dir is not None:
|
||||||
|
return True, {
|
||||||
|
"action": "download_items",
|
||||||
|
"path": f"tidal://album/{identifier}",
|
||||||
|
"title": f"Album {identifier}",
|
||||||
|
"metadata": {
|
||||||
|
"album_id": identifier,
|
||||||
|
},
|
||||||
|
"media_kind": "audio",
|
||||||
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
track_results = self._tracks_for_album(
|
track_results = self._tracks_for_album(
|
||||||
album_id=identifier,
|
album_id=identifier,
|
||||||
@@ -1562,6 +1597,76 @@ class Tidal(Provider):
|
|||||||
|
|
||||||
return False, None
|
return False, None
|
||||||
|
|
||||||
|
def download_items(
|
||||||
|
self,
|
||||||
|
result: SearchResult,
|
||||||
|
output_dir: Path,
|
||||||
|
*,
|
||||||
|
emit: Callable[[Path, str, str, Dict[str, Any]], None],
|
||||||
|
progress: Any,
|
||||||
|
quiet_mode: bool,
|
||||||
|
path_from_result: Callable[[Any], Path],
|
||||||
|
config: Optional[Dict[str, Any]] = None,
|
||||||
|
) -> int:
|
||||||
|
_ = progress
|
||||||
|
_ = quiet_mode
|
||||||
|
_ = path_from_result
|
||||||
|
_ = config
|
||||||
|
|
||||||
|
metadata = getattr(result, "full_metadata", None)
|
||||||
|
md: Dict[str, Any] = dict(metadata) if isinstance(metadata, dict) else {}
|
||||||
|
|
||||||
|
album_id = self._parse_int(md.get("album_id") or md.get("albumId") or md.get("id"))
|
||||||
|
album_title = stringify(md.get("album_title") or md.get("title") or md.get("album"))
|
||||||
|
|
||||||
|
artist_name = stringify(md.get("artist_name") or md.get("_artist_name") or md.get("artist"))
|
||||||
|
if not artist_name:
|
||||||
|
artist_obj = md.get("artist")
|
||||||
|
if isinstance(artist_obj, dict):
|
||||||
|
artist_name = stringify(artist_obj.get("name"))
|
||||||
|
|
||||||
|
path_text = stringify(getattr(result, "path", ""))
|
||||||
|
if path_text:
|
||||||
|
view, identifier = self._parse_tidal_url(path_text)
|
||||||
|
if view == "album" and not album_id:
|
||||||
|
album_id = identifier
|
||||||
|
|
||||||
|
if not album_id:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
try:
|
||||||
|
track_results = self._tracks_for_album(
|
||||||
|
album_id=album_id,
|
||||||
|
album_title=album_title,
|
||||||
|
artist_name=artist_name,
|
||||||
|
limit=500,
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if not track_results:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
downloaded_count = 0
|
||||||
|
for track_result in track_results:
|
||||||
|
try:
|
||||||
|
downloaded = self.download(track_result, output_dir)
|
||||||
|
except Exception:
|
||||||
|
downloaded = None
|
||||||
|
|
||||||
|
if not downloaded:
|
||||||
|
continue
|
||||||
|
|
||||||
|
tr_md_raw = getattr(track_result, "full_metadata", None)
|
||||||
|
tr_md = dict(tr_md_raw) if isinstance(tr_md_raw, dict) else {}
|
||||||
|
source = stringify(tr_md.get("url") or getattr(track_result, "path", ""))
|
||||||
|
relpath = str(downloaded.name)
|
||||||
|
|
||||||
|
emit(downloaded, source, relpath, tr_md)
|
||||||
|
downloaded_count += 1
|
||||||
|
|
||||||
|
return downloaded_count
|
||||||
|
|
||||||
def _get_api_client_for_base(self, base_url: str) -> Optional[TidalApiClient]:
|
def _get_api_client_for_base(self, base_url: str) -> Optional[TidalApiClient]:
|
||||||
base = base_url.rstrip("/")
|
base = base_url.rstrip("/")
|
||||||
for client in self.api_clients:
|
for client in self.api_clients:
|
||||||
|
|||||||
@@ -1895,7 +1895,7 @@ class PipelineExecutor:
|
|||||||
if row_args:
|
if row_args:
|
||||||
selected_row_args.extend(row_args)
|
selected_row_args.extend(row_args)
|
||||||
|
|
||||||
if selected_row_args:
|
if selected_row_args and not stages:
|
||||||
if isinstance(source_cmd, list):
|
if isinstance(source_cmd, list):
|
||||||
cmd_list: List[str] = [str(x) for x in source_cmd if x is not None]
|
cmd_list: List[str] = [str(x) for x in source_cmd if x is not None]
|
||||||
elif isinstance(source_cmd, str):
|
elif isinstance(source_cmd, str):
|
||||||
@@ -1914,11 +1914,7 @@ class PipelineExecutor:
|
|||||||
# as the positional URL and avoid this class of parsing errors.
|
# as the positional URL and avoid this class of parsing errors.
|
||||||
expanded_stage: List[str] = cmd_list + selected_row_args + source_args
|
expanded_stage: List[str] = cmd_list + selected_row_args + source_args
|
||||||
|
|
||||||
if first_stage_had_extra_args and stages:
|
stages.insert(0, expanded_stage)
|
||||||
expanded_stage += stages[0]
|
|
||||||
stages[0] = expanded_stage
|
|
||||||
else:
|
|
||||||
stages.insert(0, expanded_stage)
|
|
||||||
|
|
||||||
if pipeline_session and worker_manager:
|
if pipeline_session and worker_manager:
|
||||||
try:
|
try:
|
||||||
@@ -1928,6 +1924,8 @@ class PipelineExecutor:
|
|||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("Failed to record pipeline log step for @N expansion (pipeline_session=%r)", getattr(pipeline_session, 'worker_id', None))
|
logger.exception("Failed to record pipeline log step for @N expansion (pipeline_session=%r)", getattr(pipeline_session, 'worker_id', None))
|
||||||
|
elif selected_row_args and stages:
|
||||||
|
debug("@N: skipping source command expansion because downstream stages exist")
|
||||||
|
|
||||||
stage_table = None
|
stage_table = None
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -87,10 +87,9 @@ def _load_root_modules() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def _load_helper_modules() -> None:
|
def _load_helper_modules() -> None:
|
||||||
try:
|
# Provider-specific module pre-loading removed; providers are loaded lazily
|
||||||
import API.alldebrid as _alldebrid
|
# through ProviderCore.registry when first referenced.
|
||||||
except Exception:
|
pass
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def _register_native_commands() -> None:
|
def _register_native_commands() -> None:
|
||||||
|
|||||||
@@ -965,6 +965,48 @@ def normalize_hash(hash_hex: Optional[str]) -> Optional[str]:
|
|||||||
return text
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_hash_for_cmdlet(
|
||||||
|
raw_hash: Optional[str],
|
||||||
|
raw_path: Optional[str],
|
||||||
|
override_hash: Optional[str],
|
||||||
|
) -> Optional[str]:
|
||||||
|
"""Resolve a file hash for note/tag/file cmdlets.
|
||||||
|
|
||||||
|
Shared implementation used by add-note, delete-note, get-note, and similar
|
||||||
|
cmdlets that need to identify a file by its SHA-256 hash.
|
||||||
|
|
||||||
|
Resolution order:
|
||||||
|
1. ``override_hash`` — explicit hash provided via *-query* (highest priority)
|
||||||
|
2. ``raw_hash`` — positional hash argument
|
||||||
|
3. ``raw_path`` stem — if the filename stem is a 64-char hex string it is
|
||||||
|
treated directly as the hash (Hydrus-style naming convention)
|
||||||
|
4. SHA-256 computed from the file at ``raw_path``
|
||||||
|
|
||||||
|
Args:
|
||||||
|
raw_hash: Hash string from positional argument.
|
||||||
|
raw_path: Filesystem path to the file (may be None).
|
||||||
|
override_hash: Hash extracted from *-query* (takes precedence).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Normalised 64-char lowercase hex hash, or ``None`` if unresolvable.
|
||||||
|
"""
|
||||||
|
resolved = normalize_hash(override_hash) if override_hash else normalize_hash(raw_hash)
|
||||||
|
if resolved:
|
||||||
|
return resolved
|
||||||
|
if raw_path:
|
||||||
|
try:
|
||||||
|
p = Path(str(raw_path))
|
||||||
|
stem = p.stem
|
||||||
|
if len(stem) == 64 and all(c in "0123456789abcdef" for c in stem.lower()):
|
||||||
|
return stem.lower()
|
||||||
|
if p.exists() and p.is_file():
|
||||||
|
from SYS.utils import sha256_file as _sha256_file
|
||||||
|
return _sha256_file(p)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def parse_hash_query(query: Optional[str]) -> List[str]:
|
def parse_hash_query(query: Optional[str]) -> List[str]:
|
||||||
"""Parse a unified query string for `hash:` into normalized SHA256 hashes.
|
"""Parse a unified query string for `hash:` into normalized SHA256 hashes.
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,13 @@ SUPPORTED_MEDIA_EXTENSIONS = ALL_SUPPORTED_EXTENSIONS
|
|||||||
|
|
||||||
DEBUG_PIPE_NOTE_PREVIEW_LENGTH = 256
|
DEBUG_PIPE_NOTE_PREVIEW_LENGTH = 256
|
||||||
|
|
||||||
|
# Protocol schemes that identify a remote resource / not a local file path.
|
||||||
|
# Used by multiple methods in this file to guard against URL strings being
|
||||||
|
# treated as local file paths.
|
||||||
|
_REMOTE_URL_PREFIXES: tuple[str, ...] = (
|
||||||
|
"http://", "https://", "magnet:", "torrent:", "tidal:", "hydrus:",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _truncate_debug_note_text(value: Any) -> str:
|
def _truncate_debug_note_text(value: Any) -> str:
|
||||||
raw = str(value or "")
|
raw = str(value or "")
|
||||||
@@ -1203,7 +1210,7 @@ class Add_File(Cmdlet):
|
|||||||
|
|
||||||
if candidate:
|
if candidate:
|
||||||
s = str(candidate).lower()
|
s = str(candidate).lower()
|
||||||
if s.startswith(("http://", "https://", "magnet:", "torrent:", "tidal:", "hydrus:")):
|
if s.startswith(_REMOTE_URL_PREFIXES):
|
||||||
log("add-file ingests local files only. Use download-file first.", file=sys.stderr)
|
log("add-file ingests local files only. Use download-file first.", file=sys.stderr)
|
||||||
return None, None, None
|
return None, None, None
|
||||||
|
|
||||||
@@ -1427,7 +1434,7 @@ class Add_File(Cmdlet):
|
|||||||
if not val:
|
if not val:
|
||||||
return False
|
return False
|
||||||
# Obvious schemes
|
# Obvious schemes
|
||||||
if val.startswith(("http://", "https://", "magnet:", "torrent:", "tidal:", "hydrus:")):
|
if val.startswith(_REMOTE_URL_PREFIXES):
|
||||||
return True
|
return True
|
||||||
# Domain-like patterns or local file paths (but we want URLs here)
|
# Domain-like patterns or local file paths (but we want URLs here)
|
||||||
if "://" in val:
|
if "://" in val:
|
||||||
|
|||||||
@@ -175,25 +175,9 @@ class Add_Note(Cmdlet):
|
|||||||
self,
|
self,
|
||||||
raw_hash: Optional[str],
|
raw_hash: Optional[str],
|
||||||
raw_path: Optional[str],
|
raw_path: Optional[str],
|
||||||
override_hash: Optional[str]
|
override_hash: Optional[str],
|
||||||
) -> Optional[str]:
|
) -> Optional[str]:
|
||||||
resolved = normalize_hash(override_hash
|
return sh.resolve_hash_for_cmdlet(raw_hash, raw_path, override_hash)
|
||||||
) if override_hash else normalize_hash(raw_hash)
|
|
||||||
if resolved:
|
|
||||||
return resolved
|
|
||||||
|
|
||||||
if raw_path:
|
|
||||||
try:
|
|
||||||
p = Path(str(raw_path))
|
|
||||||
stem = p.stem
|
|
||||||
if len(stem) == 64 and all(c in "0123456789abcdef"
|
|
||||||
for c in stem.lower()):
|
|
||||||
return stem.lower()
|
|
||||||
if p.exists() and p.is_file():
|
|
||||||
return sha256_file(p)
|
|
||||||
except Exception:
|
|
||||||
return None
|
|
||||||
return None
|
|
||||||
|
|
||||||
def run(self, result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
|
def run(self, result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
|
||||||
if should_show_help(args):
|
if should_show_help(args):
|
||||||
|
|||||||
@@ -54,24 +54,9 @@ class Delete_Note(Cmdlet):
|
|||||||
self,
|
self,
|
||||||
raw_hash: Optional[str],
|
raw_hash: Optional[str],
|
||||||
raw_path: Optional[str],
|
raw_path: Optional[str],
|
||||||
override_hash: Optional[str]
|
override_hash: Optional[str],
|
||||||
) -> Optional[str]:
|
) -> Optional[str]:
|
||||||
resolved = normalize_hash(override_hash
|
return sh.resolve_hash_for_cmdlet(raw_hash, raw_path, override_hash)
|
||||||
) if override_hash else normalize_hash(raw_hash)
|
|
||||||
if resolved:
|
|
||||||
return resolved
|
|
||||||
if raw_path:
|
|
||||||
try:
|
|
||||||
p = Path(str(raw_path))
|
|
||||||
stem = p.stem
|
|
||||||
if len(stem) == 64 and all(c in "0123456789abcdef"
|
|
||||||
for c in stem.lower()):
|
|
||||||
return stem.lower()
|
|
||||||
if p.exists() and p.is_file():
|
|
||||||
return sha256_file(p)
|
|
||||||
except Exception:
|
|
||||||
return None
|
|
||||||
return None
|
|
||||||
|
|
||||||
def run(self, result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
|
def run(self, result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
|
||||||
if should_show_help(args):
|
if should_show_help(args):
|
||||||
|
|||||||
@@ -54,6 +54,10 @@ resolve_target_dir = sh.resolve_target_dir
|
|||||||
coerce_to_path = sh.coerce_to_path
|
coerce_to_path = sh.coerce_to_path
|
||||||
build_pipeline_preview = sh.build_pipeline_preview
|
build_pipeline_preview = sh.build_pipeline_preview
|
||||||
|
|
||||||
|
# URI scheme prefixes owned by AllDebrid (magic-link and emoji shorthand).
|
||||||
|
# Defined once here so every method in this file references the same constant.
|
||||||
|
_ALLDEBRID_PREFIXES: tuple[str, ...] = ("alldebrid:", "alldebrid🧲")
|
||||||
|
|
||||||
|
|
||||||
class Download_File(Cmdlet):
|
class Download_File(Cmdlet):
|
||||||
"""Class-based download-file cmdlet - direct HTTP downloads."""
|
"""Class-based download-file cmdlet - direct HTTP downloads."""
|
||||||
@@ -652,9 +656,12 @@ class Download_File(Cmdlet):
|
|||||||
notes: Optional[Dict[str, str]] = None
|
notes: Optional[Dict[str, str]] = None
|
||||||
try:
|
try:
|
||||||
if isinstance(full_metadata, dict):
|
if isinstance(full_metadata, dict):
|
||||||
subtitles = full_metadata.get("_tidal_lyrics_subtitles")
|
# Providers attach pre-built notes under the generic "_notes" key
|
||||||
if isinstance(subtitles, str) and subtitles.strip():
|
# (e.g. Tidal sets {"lyric": subtitles} during download enrichment).
|
||||||
notes = {"lyric": subtitles}
|
# This keeps provider-specific metadata handling inside the provider.
|
||||||
|
_provider_notes = full_metadata.get("_notes")
|
||||||
|
if isinstance(_provider_notes, dict) and _provider_notes:
|
||||||
|
notes = {str(k): str(v) for k, v in _provider_notes.items() if k and v}
|
||||||
except Exception:
|
except Exception:
|
||||||
notes = None
|
notes = None
|
||||||
tag: List[str] = []
|
tag: List[str] = []
|
||||||
@@ -2787,7 +2794,9 @@ class Download_File(Cmdlet):
|
|||||||
s_val = str(value or "").strip().lower()
|
s_val = str(value or "").strip().lower()
|
||||||
except Exception:
|
except Exception:
|
||||||
return False
|
return False
|
||||||
return s_val.startswith(("http://", "https://", "magnet:", "torrent:", "alldebrid:", "alldebrid🧲"))
|
return s_val.startswith(
|
||||||
|
("http://", "https://", "magnet:", "torrent:") + _ALLDEBRID_PREFIXES
|
||||||
|
)
|
||||||
|
|
||||||
def _extract_selection_args(item: Any) -> tuple[Optional[List[str]], Optional[str]]:
|
def _extract_selection_args(item: Any) -> tuple[Optional[List[str]], Optional[str]]:
|
||||||
selection_args: Optional[List[str]] = None
|
selection_args: Optional[List[str]] = None
|
||||||
@@ -2955,15 +2964,13 @@ class Download_File(Cmdlet):
|
|||||||
and (not parsed.get("path"))):
|
and (not parsed.get("path"))):
|
||||||
candidate = str(raw_url[0] or "").strip()
|
candidate = str(raw_url[0] or "").strip()
|
||||||
low = candidate.lower()
|
low = candidate.lower()
|
||||||
looks_like_url = low.startswith((
|
looks_like_url = low.startswith(
|
||||||
"http://", "https://", "ftp://", "magnet:", "torrent:",
|
("http://", "https://", "ftp://", "magnet:", "torrent:") + _ALLDEBRID_PREFIXES
|
||||||
"alldebrid:", "alldebrid🧲"
|
)
|
||||||
))
|
|
||||||
looks_like_provider = (
|
looks_like_provider = (
|
||||||
":" in candidate and not candidate.startswith((
|
":" in candidate and not candidate.startswith(
|
||||||
"http:", "https:", "ftp:", "ftps:", "file:",
|
("http:", "https:", "ftp:", "ftps:", "file:") + _ALLDEBRID_PREFIXES
|
||||||
"alldebrid:"
|
)
|
||||||
))
|
|
||||||
)
|
)
|
||||||
looks_like_windows_path = (
|
looks_like_windows_path = (
|
||||||
(len(candidate) >= 2 and candidate[1] == ":")
|
(len(candidate) >= 2 and candidate[1] == ":")
|
||||||
|
|||||||
@@ -49,24 +49,9 @@ class Get_Note(Cmdlet):
|
|||||||
self,
|
self,
|
||||||
raw_hash: Optional[str],
|
raw_hash: Optional[str],
|
||||||
raw_path: Optional[str],
|
raw_path: Optional[str],
|
||||||
override_hash: Optional[str]
|
override_hash: Optional[str],
|
||||||
) -> Optional[str]:
|
) -> Optional[str]:
|
||||||
resolved = normalize_hash(override_hash
|
return sh.resolve_hash_for_cmdlet(raw_hash, raw_path, override_hash)
|
||||||
) if override_hash else normalize_hash(raw_hash)
|
|
||||||
if resolved:
|
|
||||||
return resolved
|
|
||||||
if raw_path:
|
|
||||||
try:
|
|
||||||
p = Path(str(raw_path))
|
|
||||||
stem = p.stem
|
|
||||||
if len(stem) == 64 and all(c in "0123456789abcdef"
|
|
||||||
for c in stem.lower()):
|
|
||||||
return stem.lower()
|
|
||||||
if p.exists() and p.is_file():
|
|
||||||
return sha256_file(p)
|
|
||||||
except Exception:
|
|
||||||
return None
|
|
||||||
return None
|
|
||||||
|
|
||||||
def run(self, result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
|
def run(self, result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
|
||||||
if should_show_help(args):
|
if should_show_help(args):
|
||||||
|
|||||||
Reference in New Issue
Block a user