your commit message
This commit is contained in:
@@ -87,10 +87,9 @@ def _load_root_modules() -> None:
|
||||
|
||||
|
||||
def _load_helper_modules() -> None:
|
||||
try:
|
||||
import API.alldebrid as _alldebrid
|
||||
except Exception:
|
||||
pass
|
||||
# Provider-specific module pre-loading removed; providers are loaded lazily
|
||||
# through ProviderCore.registry when first referenced.
|
||||
pass
|
||||
|
||||
|
||||
def _register_native_commands() -> None:
|
||||
|
||||
@@ -965,6 +965,48 @@ def normalize_hash(hash_hex: Optional[str]) -> Optional[str]:
|
||||
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]:
|
||||
"""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
|
||||
|
||||
# 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:
|
||||
raw = str(value or "")
|
||||
@@ -1203,7 +1210,7 @@ class Add_File(Cmdlet):
|
||||
|
||||
if candidate:
|
||||
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)
|
||||
return None, None, None
|
||||
|
||||
@@ -1427,7 +1434,7 @@ class Add_File(Cmdlet):
|
||||
if not val:
|
||||
return False
|
||||
# Obvious schemes
|
||||
if val.startswith(("http://", "https://", "magnet:", "torrent:", "tidal:", "hydrus:")):
|
||||
if val.startswith(_REMOTE_URL_PREFIXES):
|
||||
return True
|
||||
# Domain-like patterns or local file paths (but we want URLs here)
|
||||
if "://" in val:
|
||||
|
||||
@@ -175,25 +175,9 @@ class Add_Note(Cmdlet):
|
||||
self,
|
||||
raw_hash: Optional[str],
|
||||
raw_path: Optional[str],
|
||||
override_hash: Optional[str]
|
||||
override_hash: Optional[str],
|
||||
) -> Optional[str]:
|
||||
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():
|
||||
return sha256_file(p)
|
||||
except Exception:
|
||||
return None
|
||||
return None
|
||||
return sh.resolve_hash_for_cmdlet(raw_hash, raw_path, override_hash)
|
||||
|
||||
def run(self, result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
|
||||
if should_show_help(args):
|
||||
|
||||
@@ -54,24 +54,9 @@ class Delete_Note(Cmdlet):
|
||||
self,
|
||||
raw_hash: Optional[str],
|
||||
raw_path: Optional[str],
|
||||
override_hash: Optional[str]
|
||||
override_hash: Optional[str],
|
||||
) -> Optional[str]:
|
||||
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():
|
||||
return sha256_file(p)
|
||||
except Exception:
|
||||
return None
|
||||
return None
|
||||
return sh.resolve_hash_for_cmdlet(raw_hash, raw_path, override_hash)
|
||||
|
||||
def run(self, result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
|
||||
if should_show_help(args):
|
||||
|
||||
@@ -54,6 +54,10 @@ resolve_target_dir = sh.resolve_target_dir
|
||||
coerce_to_path = sh.coerce_to_path
|
||||
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-based download-file cmdlet - direct HTTP downloads."""
|
||||
@@ -652,9 +656,12 @@ class Download_File(Cmdlet):
|
||||
notes: Optional[Dict[str, str]] = None
|
||||
try:
|
||||
if isinstance(full_metadata, dict):
|
||||
subtitles = full_metadata.get("_tidal_lyrics_subtitles")
|
||||
if isinstance(subtitles, str) and subtitles.strip():
|
||||
notes = {"lyric": subtitles}
|
||||
# Providers attach pre-built notes under the generic "_notes" key
|
||||
# (e.g. Tidal sets {"lyric": subtitles} during download enrichment).
|
||||
# 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:
|
||||
notes = None
|
||||
tag: List[str] = []
|
||||
@@ -2787,7 +2794,9 @@ class Download_File(Cmdlet):
|
||||
s_val = str(value or "").strip().lower()
|
||||
except Exception:
|
||||
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]]:
|
||||
selection_args: Optional[List[str]] = None
|
||||
@@ -2955,15 +2964,13 @@ class Download_File(Cmdlet):
|
||||
and (not parsed.get("path"))):
|
||||
candidate = str(raw_url[0] or "").strip()
|
||||
low = candidate.lower()
|
||||
looks_like_url = low.startswith((
|
||||
"http://", "https://", "ftp://", "magnet:", "torrent:",
|
||||
"alldebrid:", "alldebrid🧲"
|
||||
))
|
||||
looks_like_url = low.startswith(
|
||||
("http://", "https://", "ftp://", "magnet:", "torrent:") + _ALLDEBRID_PREFIXES
|
||||
)
|
||||
looks_like_provider = (
|
||||
":" in candidate and not candidate.startswith((
|
||||
"http:", "https:", "ftp:", "ftps:", "file:",
|
||||
"alldebrid:"
|
||||
))
|
||||
":" in candidate and not candidate.startswith(
|
||||
("http:", "https:", "ftp:", "ftps:", "file:") + _ALLDEBRID_PREFIXES
|
||||
)
|
||||
)
|
||||
looks_like_windows_path = (
|
||||
(len(candidate) >= 2 and candidate[1] == ":")
|
||||
|
||||
@@ -49,24 +49,9 @@ class Get_Note(Cmdlet):
|
||||
self,
|
||||
raw_hash: Optional[str],
|
||||
raw_path: Optional[str],
|
||||
override_hash: Optional[str]
|
||||
override_hash: Optional[str],
|
||||
) -> Optional[str]:
|
||||
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():
|
||||
return sha256_file(p)
|
||||
except Exception:
|
||||
return None
|
||||
return None
|
||||
return sh.resolve_hash_for_cmdlet(raw_hash, raw_path, override_hash)
|
||||
|
||||
def run(self, result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
|
||||
if should_show_help(args):
|
||||
|
||||
Reference in New Issue
Block a user