update screenshot
This commit is contained in:
@@ -71,7 +71,7 @@
|
||||
"(wayupload\\.com/[a-z0-9]{12}\\.html)"
|
||||
],
|
||||
"regexp": "(turbobit5?a?\\.(net|cc|com)/([a-z0-9]{12}))|(turbobif\\.(net|cc|com)/([a-z0-9]{12}))|(turb[o]?\\.(to|cc|pw)\\/([a-z0-9]{12}))|(turbobit\\.(net|cc)/download/free/([a-z0-9]{12}))|((trbbt|tourbobit|torbobit|tbit|turbobita|trbt)\\.(net|cc|com|to)/([a-z0-9]{12}))|((turbobit\\.cloud/turbo/[a-z0-9]+))|((wayupload\\.com/[a-z0-9]{12}\\.html))",
|
||||
"status": true
|
||||
"status": false
|
||||
},
|
||||
"hitfile": {
|
||||
"name": "hitfile",
|
||||
@@ -92,7 +92,7 @@
|
||||
"(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": true
|
||||
"status": false
|
||||
},
|
||||
"mega": {
|
||||
"name": "mega",
|
||||
@@ -478,10 +478,10 @@
|
||||
"katfile.vip"
|
||||
],
|
||||
"regexps": [
|
||||
"katfile\\.(cloud|online|vip)/([0-9a-zA-Z]{12})",
|
||||
"katfile\\.(cloud|online|vip|ws)/([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|ws)/([0-9a-zA-Z]{12}))|((katfile\\.com/[0-9a-zA-Z]{12}))",
|
||||
"status": true
|
||||
},
|
||||
"mediafire": {
|
||||
@@ -494,7 +494,7 @@
|
||||
"mediafire\\.com/(\\?|download/|file/|download\\.php\\?)([0-9a-z]{15})"
|
||||
],
|
||||
"regexp": "mediafire\\.com/(\\?|download/|file/|download\\.php\\?)([0-9a-z]{15})",
|
||||
"status": false
|
||||
"status": true
|
||||
},
|
||||
"mixdrop": {
|
||||
"name": "mixdrop",
|
||||
@@ -618,7 +618,7 @@
|
||||
"(upload42\\.com/[0-9a-zA-Z]{12})"
|
||||
],
|
||||
"regexp": "(upload42\\.com/[0-9a-zA-Z]{12})",
|
||||
"status": true
|
||||
"status": false
|
||||
},
|
||||
"uploadbank": {
|
||||
"name": "uploadbank",
|
||||
|
||||
+279
-35
@@ -60,6 +60,7 @@ _LYRIC_VISIBLE_PROP = "user-data/medeia-lyric-visible"
|
||||
# to a store via the store DB.
|
||||
_ITEM_STORE_PROP = "user-data/medeia-item-store"
|
||||
_ITEM_HASH_PROP = "user-data/medeia-item-hash"
|
||||
_LEGACY_SUB_TRACK_TITLES = ("medeia-note-sub", "medeia-lyric-sub", "medeia-sub")
|
||||
|
||||
# Note: We previously used `osd-overlay`, but some mpv builds return
|
||||
# error='invalid parameter' for that command. We now use `show-text`, which is
|
||||
@@ -540,6 +541,39 @@ def _lyric_duration_ms(idx: int, times: List[float], current_t: float) -> int:
|
||||
return 1200
|
||||
|
||||
|
||||
def _format_vtt_timestamp(seconds: float) -> str:
|
||||
total_ms = max(0, int(round(float(seconds or 0.0) * 1000.0)))
|
||||
hours = total_ms // 3600000
|
||||
minutes = (total_ms // 60000) % 60
|
||||
secs = (total_ms // 1000) % 60
|
||||
millis = total_ms % 1000
|
||||
return f"{hours:02d}:{minutes:02d}:{secs:02d}.{millis:03d}"
|
||||
|
||||
|
||||
def _lrc_entries_to_vtt_text(entries: List[LrcLine]) -> str:
|
||||
if not entries:
|
||||
return "WEBVTT\n\n"
|
||||
|
||||
lines: List[str] = ["WEBVTT", ""]
|
||||
times = [entry.time_s for entry in entries]
|
||||
for idx, entry in enumerate(entries, start=1):
|
||||
start_s = max(0.0, float(entry.time_s or 0.0))
|
||||
if idx < len(entries):
|
||||
end_s = max(start_s + 0.25, float(times[idx]))
|
||||
else:
|
||||
end_s = start_s + 1.2
|
||||
|
||||
text = str(entry.text or "").replace("\r\n", "\n").replace("\r", "\n")
|
||||
cue_text = text if text.strip() else " "
|
||||
|
||||
lines.append(str(idx))
|
||||
lines.append(f"{_format_vtt_timestamp(start_s)} --> {_format_vtt_timestamp(end_s)}")
|
||||
lines.extend(cue_text.split("\n"))
|
||||
lines.append("")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def _unwrap_memory_m3u(text: Optional[str]) -> Optional[str]:
|
||||
"""Extract the real target URL/path from a memory:// M3U payload."""
|
||||
if not isinstance(text, str) or not text.startswith("memory://"):
|
||||
@@ -596,6 +630,12 @@ def _notes_cache_root() -> Path:
|
||||
return root
|
||||
|
||||
|
||||
def _generated_sub_root() -> Path:
|
||||
root = Path(tempfile.gettempdir()) / "medeia-mpv-notes"
|
||||
root.mkdir(parents=True, exist_ok=True)
|
||||
return root
|
||||
|
||||
|
||||
def _notes_cache_key(store: str, file_hash: str) -> str:
|
||||
return hashlib.sha1(
|
||||
f"{str(store or '').strip().lower()}:{str(file_hash or '').strip().lower()}".encode(
|
||||
@@ -790,6 +830,26 @@ def _extract_note_text(notes: Dict[str, str], name: str) -> Optional[str]:
|
||||
return text if text.strip() else None
|
||||
|
||||
|
||||
def _extract_first_note_text(
|
||||
notes: Dict[str, str],
|
||||
names: List[str],
|
||||
*,
|
||||
predicate: Optional[Any] = None,
|
||||
) -> tuple[Optional[str], Optional[str]]:
|
||||
for name in names:
|
||||
candidate = _extract_note_text(notes, name)
|
||||
if not candidate:
|
||||
continue
|
||||
if predicate is not None:
|
||||
try:
|
||||
if not bool(predicate(candidate)):
|
||||
continue
|
||||
except Exception:
|
||||
continue
|
||||
return name, candidate
|
||||
return None, None
|
||||
|
||||
|
||||
def _extract_lrc_from_notes(notes: Dict[str, str]) -> Optional[str]:
|
||||
"""Return raw LRC text from the note named 'lyric'."""
|
||||
return _extract_note_text(notes, "lyric")
|
||||
@@ -811,18 +871,61 @@ def _looks_like_subtitle_text(text: str) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def _extract_sub_from_notes(notes: Dict[str, str]) -> Optional[str]:
|
||||
"""Return raw subtitle text from note-backed subtitle/transcript keys."""
|
||||
def _extract_sub_from_notes(notes: Dict[str, str]) -> tuple[Optional[str], Optional[str]]:
|
||||
"""Return (note_name, subtitle_text) from note-backed subtitle/transcript keys."""
|
||||
primary = _extract_note_text(notes, "sub")
|
||||
if primary:
|
||||
return primary
|
||||
for note_name in _SUBTITLE_NOTE_ALIASES:
|
||||
candidate = _extract_note_text(notes, note_name)
|
||||
if candidate and _looks_like_subtitle_text(candidate):
|
||||
return candidate
|
||||
return "sub", primary
|
||||
return _extract_first_note_text(
|
||||
notes,
|
||||
list(_SUBTITLE_NOTE_ALIASES),
|
||||
predicate=_looks_like_subtitle_text,
|
||||
)
|
||||
|
||||
|
||||
def _display_note_name(note_name: Optional[str]) -> str:
|
||||
text = re.sub(r"\s+", " ", str(note_name or "").replace("_", " ")).strip()
|
||||
if not text:
|
||||
return "subtitle"
|
||||
lowered = text.casefold()
|
||||
if lowered == "lyric":
|
||||
return "lyrics"
|
||||
if lowered == "sub":
|
||||
return "subtitles"
|
||||
return text
|
||||
|
||||
|
||||
def _display_media_title(client: MPVIPCClient) -> Optional[str]:
|
||||
for key in ("metadata/by-key/title", "metadata/by-key/Title", "media-title"):
|
||||
try:
|
||||
value = _ipc_get_property(client, key, None)
|
||||
except Exception:
|
||||
value = None
|
||||
if isinstance(value, str):
|
||||
text = re.sub(r"\s+", " ", value).strip()
|
||||
if text:
|
||||
return text
|
||||
return None
|
||||
|
||||
|
||||
def _generated_subtitle_title(client: MPVIPCClient, *, note_name: Optional[str]) -> str:
|
||||
note_label = _display_note_name(note_name)
|
||||
media_title = _display_media_title(client)
|
||||
if media_title:
|
||||
title = f"{note_label}: {media_title}"
|
||||
else:
|
||||
title = note_label
|
||||
title = re.sub(r"\s+", " ", title).strip()
|
||||
return title[:96] if len(title) > 96 else title
|
||||
|
||||
|
||||
def _filename_slug(text: Optional[str], *, default: str) -> str:
|
||||
value = re.sub(r"[^A-Za-z0-9._ -]+", " ", str(text or ""))
|
||||
value = re.sub(r"\s+", "-", value).strip("- ._")
|
||||
value = value[:48]
|
||||
return value or default
|
||||
|
||||
|
||||
def _infer_sub_extension(text: str) -> str:
|
||||
# Best-effort: mpv generally understands SRT/VTT; choose based on content.
|
||||
t = (text or "").lstrip("\ufeff\r\n").lstrip()
|
||||
@@ -839,39 +942,122 @@ def _infer_sub_extension(text: str) -> str:
|
||||
return ".vtt"
|
||||
|
||||
|
||||
def _write_temp_sub_file(*, key: str, text: str) -> Path:
|
||||
def _write_temp_sub_file(*, key: str, text: str, label: Optional[str] = None) -> Path:
|
||||
# Write to a content-addressed temp path so updates force mpv reload.
|
||||
tmp_dir = Path(tempfile.gettempdir()) / "medeia-mpv-notes"
|
||||
tmp_dir.mkdir(parents=True, exist_ok=True)
|
||||
tmp_dir = _generated_sub_root()
|
||||
|
||||
ext = _infer_sub_extension(text)
|
||||
digest = hashlib.sha1((key + "\n" + (text or "")).encode("utf-8",
|
||||
errors="ignore")
|
||||
).hexdigest()[:16]
|
||||
safe_key = hashlib.sha1((key or "").encode("utf-8",
|
||||
errors="ignore")).hexdigest()[:12]
|
||||
path = (tmp_dir / f"sub-{safe_key}-{digest}{ext}").resolve()
|
||||
prefix = _filename_slug(label, default="subtitle")
|
||||
path = (tmp_dir / f"{prefix}-{digest}{ext}").resolve()
|
||||
path.write_text(text or "", encoding="utf-8", errors="replace")
|
||||
return path
|
||||
|
||||
|
||||
def _try_remove_selected_external_sub(client: MPVIPCClient) -> None:
|
||||
def _subtitle_track_snapshot(client: MPVIPCClient) -> List[Dict[str, Any]]:
|
||||
raw = _ipc_get_property(client, "track-list", [])
|
||||
return raw if isinstance(raw, list) else []
|
||||
|
||||
|
||||
def _track_external_sub_path(track: Dict[str, Any]) -> Optional[Path]:
|
||||
if not isinstance(track, dict):
|
||||
return None
|
||||
for key in ("external-filename", "external_filename", "demux-filename", "demux_filename"):
|
||||
raw = track.get(key)
|
||||
if not isinstance(raw, str):
|
||||
continue
|
||||
text = raw.strip()
|
||||
if not text:
|
||||
continue
|
||||
try:
|
||||
return Path(text).expanduser().resolve()
|
||||
except Exception:
|
||||
return Path(text)
|
||||
return None
|
||||
|
||||
|
||||
def _is_medeia_generated_sub_track(track: Dict[str, Any]) -> bool:
|
||||
if not isinstance(track, dict):
|
||||
return False
|
||||
title = str(track.get("title") or "").strip()
|
||||
if title in _LEGACY_SUB_TRACK_TITLES:
|
||||
return True
|
||||
path = _track_external_sub_path(track)
|
||||
if path is None:
|
||||
return False
|
||||
try:
|
||||
client.send_command({
|
||||
"command": ["sub-remove"]
|
||||
})
|
||||
path.relative_to(_generated_sub_root().resolve())
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def _find_medeia_sub_track_ids(client: MPVIPCClient) -> List[int]:
|
||||
out: List[int] = []
|
||||
for track in _subtitle_track_snapshot(client):
|
||||
if not isinstance(track, dict):
|
||||
continue
|
||||
if str(track.get("type") or "") != "sub":
|
||||
continue
|
||||
if not _is_medeia_generated_sub_track(track):
|
||||
continue
|
||||
try:
|
||||
track_id = int(track.get("id"))
|
||||
except Exception:
|
||||
continue
|
||||
out.append(track_id)
|
||||
return out
|
||||
|
||||
|
||||
def _log_medeia_sub_tracks(client: MPVIPCClient, reason: str) -> None:
|
||||
parts: List[str] = []
|
||||
for track in _subtitle_track_snapshot(client):
|
||||
if not isinstance(track, dict):
|
||||
continue
|
||||
if str(track.get("type") or "") != "sub":
|
||||
continue
|
||||
if not _is_medeia_generated_sub_track(track):
|
||||
continue
|
||||
title = str(track.get("title") or "").strip()
|
||||
source = _track_external_sub_path(track)
|
||||
parts.append(
|
||||
f"id={track.get('id')}"
|
||||
f" title={title!r}"
|
||||
f" selected={bool(track.get('selected'))}"
|
||||
f" external={bool(track.get('external'))}"
|
||||
f" source={source.name if source is not None else '<none>'}"
|
||||
)
|
||||
if parts:
|
||||
_log(f"Medeia subtitle tracks {reason}: " + " | ".join(parts))
|
||||
else:
|
||||
_log(f"Medeia subtitle tracks {reason}: <none>")
|
||||
|
||||
|
||||
def _remove_medeia_external_subs(client: MPVIPCClient, *, reason: str = "") -> None:
|
||||
track_ids = _find_medeia_sub_track_ids(client)
|
||||
if not track_ids:
|
||||
return
|
||||
_log(f"Removing Medeia subtitle tracks reason={reason or 'unknown'} ids={track_ids}")
|
||||
for track_id in track_ids:
|
||||
try:
|
||||
client.send_command({
|
||||
"command": ["sub-remove", int(track_id)]
|
||||
})
|
||||
except Exception:
|
||||
continue
|
||||
_log_medeia_sub_tracks(client, f"after-remove:{reason or 'unknown'}")
|
||||
|
||||
|
||||
def _try_add_external_sub(client: MPVIPCClient, path: Path) -> None:
|
||||
def _try_add_external_sub(client: MPVIPCClient, path: Path, *, title: str) -> None:
|
||||
try:
|
||||
client.send_command(
|
||||
{
|
||||
"command": ["sub-add",
|
||||
str(path),
|
||||
"select",
|
||||
"medeia-sub"]
|
||||
str(title or _NOTE_SUB_TRACK_TITLE)]
|
||||
}
|
||||
)
|
||||
except Exception:
|
||||
@@ -1099,7 +1285,7 @@ class _PlaybackState:
|
||||
entries: List[LrcLine] = field(default_factory=list)
|
||||
times: List[float] = field(default_factory=list)
|
||||
loaded_key: Optional[str] = None
|
||||
loaded_mode: Optional[str] = None # 'lyric' | 'sub' | None
|
||||
loaded_mode: Optional[str] = None # 'lyric' | 'sub' | 'lyric-sub' | None
|
||||
loaded_sub_path: Optional[Path] = None
|
||||
last_target: Optional[str] = None
|
||||
fetch_attempt_key: Optional[str] = None
|
||||
@@ -1130,7 +1316,7 @@ class _PlaybackState:
|
||||
self.loaded_key = None
|
||||
self.loaded_mode = None
|
||||
if self.loaded_sub_path is not None:
|
||||
_try_remove_selected_external_sub(client)
|
||||
_remove_medeia_external_subs(client, reason="state-clear")
|
||||
self.loaded_sub_path = None
|
||||
|
||||
|
||||
@@ -1153,6 +1339,7 @@ def run_auto_overlay(
|
||||
return 3
|
||||
|
||||
_log(f"Auto overlay connected (ipc={getattr(mpv, 'ipc_path', None)})")
|
||||
_remove_medeia_external_subs(client, reason="startup-sweep")
|
||||
|
||||
state = _PlaybackState()
|
||||
last_idx: Optional[int] = None
|
||||
@@ -1196,6 +1383,12 @@ def run_auto_overlay(
|
||||
if not client.connect():
|
||||
_log("mpv IPC disconnected; exiting MPV.lyric")
|
||||
return 4
|
||||
_remove_medeia_external_subs(client, reason="reconnect-sweep:path")
|
||||
state.clear(client)
|
||||
state.last_target = None
|
||||
last_idx = None
|
||||
last_text = None
|
||||
last_visible = None
|
||||
time.sleep(poll_s)
|
||||
continue
|
||||
|
||||
@@ -1207,11 +1400,14 @@ def run_auto_overlay(
|
||||
last_visible = visible
|
||||
elif last_visible is True and visible is False:
|
||||
_osd_clear_and_restore(client)
|
||||
_try_remove_selected_external_sub(client)
|
||||
_remove_medeia_external_subs(client, reason="visibility-off")
|
||||
state.loaded_sub_path = None
|
||||
last_idx = None
|
||||
last_text = None
|
||||
last_visible = visible
|
||||
elif last_visible is False and visible is True:
|
||||
if state.loaded_mode in {"sub", "lyric-sub"} and state.loaded_sub_path is None:
|
||||
state.loaded_key = None
|
||||
last_idx = None
|
||||
last_text = None
|
||||
last_visible = visible
|
||||
@@ -1459,21 +1655,26 @@ def run_auto_overlay(
|
||||
except Exception:
|
||||
_log("Loaded notes keys: <error>")
|
||||
|
||||
sub_text = _extract_note_text(notes, "sub")
|
||||
sub_note_name, sub_text = _extract_sub_from_notes(notes)
|
||||
if sub_text:
|
||||
# Hand subtitles to mpv's track subsystem; suppress OSD lyric overlay.
|
||||
_osd_clear_and_restore(client)
|
||||
sub_path: Optional[Path] = None
|
||||
sub_title = _generated_subtitle_title(client, note_name=sub_note_name)
|
||||
try:
|
||||
sub_path = _write_temp_sub_file(key=state.key, text=sub_text)
|
||||
sub_path = _write_temp_sub_file(key=state.key, text=sub_text, label=sub_title)
|
||||
except Exception as exc:
|
||||
_log(f"Failed to write sub note temp file: {exc}")
|
||||
|
||||
if sub_path is not None:
|
||||
if state.loaded_sub_path is not None:
|
||||
_try_remove_selected_external_sub(client)
|
||||
_try_add_external_sub(client, sub_path)
|
||||
_remove_medeia_external_subs(client, reason="load-note-sub")
|
||||
_try_add_external_sub(client, sub_path, title=sub_title)
|
||||
state.loaded_sub_path = sub_path
|
||||
_log(
|
||||
f"Loaded note-backed native subtitle track"
|
||||
f" note={sub_note_name!r} title={sub_title!r} path={sub_path}"
|
||||
)
|
||||
_log_medeia_sub_tracks(client, "after-add-note-sub")
|
||||
|
||||
state.entries = []
|
||||
state.times = []
|
||||
@@ -1481,12 +1682,12 @@ def run_auto_overlay(
|
||||
state.loaded_mode = "sub"
|
||||
|
||||
else:
|
||||
# Switching away from sub mode: unload the external subtitle.
|
||||
if state.loaded_mode == "sub" and state.loaded_sub_path is not None:
|
||||
_try_remove_selected_external_sub(client)
|
||||
# Switching away from native subtitle mode: unload the external subtitle.
|
||||
if state.loaded_sub_path is not None:
|
||||
_remove_medeia_external_subs(client, reason="switch-away-native-sub")
|
||||
state.loaded_sub_path = None
|
||||
|
||||
lrc_text = _extract_note_text(notes, "lyric")
|
||||
lrc_text = _extract_lrc_from_notes(notes)
|
||||
if not lrc_text:
|
||||
_log("No lyric note found (note name: 'lyric')")
|
||||
|
||||
@@ -1569,10 +1770,47 @@ def run_auto_overlay(
|
||||
else:
|
||||
_log(f"Loaded lyric note ({len(lrc_text)} chars)")
|
||||
parsed = parse_lrc(lrc_text)
|
||||
state.entries = parsed
|
||||
state.times = [e.time_s for e in parsed]
|
||||
state.loaded_key = state.key
|
||||
state.loaded_mode = "lyric"
|
||||
if not parsed:
|
||||
_log("Lyric note contained no timestamped entries")
|
||||
_osd_clear_and_restore(client)
|
||||
state.entries = []
|
||||
state.times = []
|
||||
state.loaded_key = state.key
|
||||
state.loaded_mode = None
|
||||
else:
|
||||
lyric_sub_path: Optional[Path] = None
|
||||
lyric_sub_title = _generated_subtitle_title(client, note_name="lyric")
|
||||
try:
|
||||
lyric_sub_text = _lrc_entries_to_vtt_text(parsed)
|
||||
lyric_sub_path = _write_temp_sub_file(
|
||||
key=f"{state.key}:lyric",
|
||||
text=lyric_sub_text,
|
||||
label=lyric_sub_title,
|
||||
)
|
||||
except Exception as exc:
|
||||
_log(f"Failed to write lyric note temp subtitle: {exc}")
|
||||
|
||||
if lyric_sub_path is None:
|
||||
_osd_clear_and_restore(client)
|
||||
state.entries = []
|
||||
state.times = []
|
||||
state.loaded_key = state.key
|
||||
state.loaded_mode = None
|
||||
else:
|
||||
_osd_clear_and_restore(client)
|
||||
_remove_medeia_external_subs(client, reason="load-lyric-sub")
|
||||
_try_add_external_sub(client, lyric_sub_path, title=lyric_sub_title)
|
||||
state.loaded_sub_path = lyric_sub_path
|
||||
state.entries = []
|
||||
state.times = []
|
||||
state.loaded_key = state.key
|
||||
state.loaded_mode = "lyric-sub"
|
||||
_log(
|
||||
f"Loaded lyric note as native subtitle track"
|
||||
f" title={lyric_sub_title!r} entries={len(parsed)}"
|
||||
f" path={lyric_sub_path}"
|
||||
)
|
||||
_log_medeia_sub_tracks(client, "after-add-lyric-sub")
|
||||
|
||||
# ----------------------------------------------------------------
|
||||
# 8. Render the current lyric line.
|
||||
@@ -1590,6 +1828,12 @@ def run_auto_overlay(
|
||||
if not client.connect():
|
||||
_log("mpv IPC disconnected; exiting MPV.lyric")
|
||||
return 4
|
||||
_remove_medeia_external_subs(client, reason="reconnect-sweep:time")
|
||||
state.clear(client)
|
||||
state.last_target = None
|
||||
last_idx = None
|
||||
last_text = None
|
||||
last_visible = None
|
||||
time.sleep(poll_s)
|
||||
continue
|
||||
|
||||
|
||||
@@ -756,7 +756,6 @@ def _run_op(op: str, data: Any) -> Dict[str, Any]:
|
||||
refresh = bool(data.get("refresh") or data.get("reload"))
|
||||
|
||||
if cached_choices and not refresh:
|
||||
debug(f"[store-choices] using cached choices={len(cached_choices)}")
|
||||
return {
|
||||
"success": True,
|
||||
"stdout": "",
|
||||
@@ -767,20 +766,14 @@ def _run_op(op: str, data: Any) -> Dict[str, Any]:
|
||||
}
|
||||
|
||||
try:
|
||||
config_root = _runtime_config_root()
|
||||
choices = _load_store_choices_from_config(force_reload=refresh)
|
||||
|
||||
if not choices and cached_choices:
|
||||
choices = cached_choices
|
||||
debug(
|
||||
f"[store-choices] config returned empty; falling back to cached choices={len(choices)}"
|
||||
)
|
||||
|
||||
if choices:
|
||||
choices = _set_cached_store_choices(choices)
|
||||
|
||||
debug(f"[store-choices] config_dir={config_root} choices={len(choices)}")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"stdout": "",
|
||||
@@ -791,9 +784,6 @@ def _run_op(op: str, data: Any) -> Dict[str, Any]:
|
||||
}
|
||||
except Exception as exc:
|
||||
if cached_choices:
|
||||
debug(
|
||||
f"[store-choices] refresh failed; returning cached choices={len(cached_choices)} error={type(exc).__name__}: {exc}"
|
||||
)
|
||||
return {
|
||||
"success": True,
|
||||
"stdout": "",
|
||||
|
||||
+2
-11
@@ -101,10 +101,7 @@ class SharedArgs:
|
||||
if not force and hasattr(SharedArgs, "_cached_available_stores"):
|
||||
return SharedArgs._cached_available_stores or []
|
||||
|
||||
if not force:
|
||||
SharedArgs._refresh_store_choices_cache(config, skip_instantiation=True)
|
||||
else:
|
||||
SharedArgs._refresh_store_choices_cache(config, skip_instantiation=False)
|
||||
SharedArgs._refresh_store_choices_cache(config, skip_instantiation=False)
|
||||
return SharedArgs._cached_available_stores or []
|
||||
|
||||
@staticmethod
|
||||
@@ -119,13 +116,7 @@ class SharedArgs:
|
||||
SharedArgs._cached_available_stores = []
|
||||
return
|
||||
|
||||
try:
|
||||
from Store.registry import list_configured_backend_names
|
||||
|
||||
SharedArgs._cached_available_stores = list_configured_backend_names(config) or []
|
||||
except Exception:
|
||||
SharedArgs._cached_available_stores = []
|
||||
|
||||
SharedArgs._cached_available_stores = []
|
||||
if skip_instantiation:
|
||||
return
|
||||
|
||||
|
||||
+9
-20
@@ -228,23 +228,18 @@ class SharedArgs:
|
||||
if not force and hasattr(SharedArgs, "_cached_available_stores"):
|
||||
return SharedArgs._cached_available_stores or []
|
||||
|
||||
# Refresh the cache. When not forcing, prefer a lightweight configured-name
|
||||
# pass to avoid instantiating backends (which may perform work such as opening DBs).
|
||||
if not force:
|
||||
SharedArgs._refresh_store_choices_cache(config, skip_instantiation=True)
|
||||
else:
|
||||
SharedArgs._refresh_store_choices_cache(config, skip_instantiation=False)
|
||||
# Autocomplete and shared arg choices must only expose backends that actually
|
||||
# initialized successfully. Do a full refresh when the cache is missing.
|
||||
SharedArgs._refresh_store_choices_cache(config, skip_instantiation=False)
|
||||
return SharedArgs._cached_available_stores or []
|
||||
|
||||
@staticmethod
|
||||
def _refresh_store_choices_cache(config: Optional[Dict[str, Any]] = None, skip_instantiation: bool = False) -> None:
|
||||
"""Refresh the cached store choices list. Should be called once at startup.
|
||||
|
||||
This performs a lightweight pass first (reads configured names only, without
|
||||
instantiating backend classes) to avoid side-effects during autocompletion or
|
||||
other quick lookups. When `skip_instantiation` is False, the function will
|
||||
attempt a full StoreRegistry initialization to filter out backends that failed
|
||||
to initialize properly.
|
||||
Store choices are user-facing and should only include backends that actually
|
||||
initialized successfully. When `skip_instantiation` is True, this method keeps
|
||||
the cache empty rather than surfacing configured-but-disabled store names.
|
||||
|
||||
Args:
|
||||
config: Config dict. If not provided, will try to load from config module.
|
||||
@@ -259,15 +254,10 @@ class SharedArgs:
|
||||
SharedArgs._cached_available_stores = []
|
||||
return
|
||||
|
||||
# Lightweight pass: return configured names without instantiating backends
|
||||
try:
|
||||
from Store.registry import list_configured_backend_names
|
||||
SharedArgs._cached_available_stores = list_configured_backend_names(config) or []
|
||||
except Exception:
|
||||
SharedArgs._cached_available_stores = []
|
||||
SharedArgs._cached_available_stores = []
|
||||
|
||||
# If caller explicitly requested a full scan, instantiate registry to get
|
||||
# only backends that actually initialized successfully.
|
||||
# If caller requested a lightweight pass, avoid exposing configured names
|
||||
# that may be disabled or unavailable.
|
||||
if skip_instantiation:
|
||||
return
|
||||
|
||||
@@ -278,7 +268,6 @@ class SharedArgs:
|
||||
if available:
|
||||
SharedArgs._cached_available_stores = available
|
||||
except Exception:
|
||||
# Keep the lightweight list if full initialization fails
|
||||
pass
|
||||
except Exception:
|
||||
SharedArgs._cached_available_stores = []
|
||||
|
||||
@@ -338,6 +338,14 @@ class Add_File(Cmdlet):
|
||||
except Exception:
|
||||
is_storage_backend_location = False
|
||||
|
||||
if location and not plugin_name and not is_storage_backend_location:
|
||||
if not Add_File._looks_like_local_export_target(str(location)):
|
||||
log(
|
||||
f"Storage backend '{location}' not found. Use -path for local export or configure that store backend.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
|
||||
# Decide which items to process.
|
||||
# - If directory scan was performed, use those results
|
||||
# - If user provided -path (and it was not reinterpreted as destination), treat this invocation as single-item.
|
||||
@@ -1262,6 +1270,27 @@ class Add_File(Cmdlet):
|
||||
pass
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _looks_like_local_export_target(location: str) -> bool:
|
||||
target = str(location or "").strip()
|
||||
if not target:
|
||||
return False
|
||||
|
||||
target_path = Path(target).expanduser()
|
||||
try:
|
||||
if target_path.exists():
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if target.startswith((".", "~")):
|
||||
return True
|
||||
if "\\" in target or "/" in target:
|
||||
return True
|
||||
if len(target) >= 2 and target[1] == ":":
|
||||
return True
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def _resolve_source(
|
||||
result: Any,
|
||||
|
||||
+890
-344
File diff suppressed because it is too large
Load Diff
+3
-13
@@ -191,23 +191,19 @@ class PlaywrightTool:
|
||||
if candidate and Path(candidate).exists():
|
||||
ffmpeg_path = candidate
|
||||
else:
|
||||
debug(f"Configured ffmpeg path does not exist: {candidate}")
|
||||
ffmpeg_path = None
|
||||
|
||||
if not ffmpeg_path:
|
||||
# Prefer a global FFMPEG_PATH env var (shared by tools) before Playwright-specific one
|
||||
env_ffmpeg = os.environ.get("FFMPEG_PATH")
|
||||
if env_ffmpeg and Path(env_ffmpeg).exists():
|
||||
ffmpeg_path = env_ffmpeg
|
||||
elif env_ffmpeg:
|
||||
debug(f"FFMPEG_PATH set but path does not exist: {env_ffmpeg}")
|
||||
|
||||
if not ffmpeg_path:
|
||||
# Backward-compatible Playwright-specific env var
|
||||
env_ffmpeg2 = os.environ.get("PLAYWRIGHT_FFMPEG_PATH")
|
||||
if env_ffmpeg2 and Path(env_ffmpeg2).exists():
|
||||
ffmpeg_path = env_ffmpeg2
|
||||
elif env_ffmpeg2:
|
||||
debug(f"PLAYWRIGHT_FFMPEG_PATH set but path does not exist: {env_ffmpeg2}")
|
||||
|
||||
if not ffmpeg_path:
|
||||
# Try to find bundled ffmpeg in the project (Windows-only, in MPV/ffmpeg/bin)
|
||||
@@ -218,20 +214,14 @@ class PlaywrightTool:
|
||||
ffmpeg_exe = bundled_ffmpeg / ("ffmpeg.exe" if os.name == "nt" else "ffmpeg")
|
||||
if ffmpeg_exe.exists():
|
||||
ffmpeg_path = str(ffmpeg_exe)
|
||||
debug(f"Found bundled ffmpeg at: {ffmpeg_path}")
|
||||
except Exception as e:
|
||||
debug(f"Error checking for bundled ffmpeg: {e}")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if not ffmpeg_path:
|
||||
# Try system ffmpeg if bundled not found
|
||||
system_ffmpeg = shutil.which("ffmpeg")
|
||||
if system_ffmpeg:
|
||||
ffmpeg_path = system_ffmpeg
|
||||
debug(f"Found system ffmpeg at: {ffmpeg_path}")
|
||||
else:
|
||||
# ffmpeg not found - log a debug message but don't fail
|
||||
# ffmpeg-python may still work with system installation, or user might not need it
|
||||
debug("ffmpeg not found on PATH. For best compatibility, install ffmpeg: Windows (use bundled or choco install ffmpeg), macOS (brew install ffmpeg), Linux (apt install ffmpeg or equivalent)")
|
||||
|
||||
return PlaywrightDefaults(
|
||||
browser=browser,
|
||||
|
||||
Reference in New Issue
Block a user