update
This commit is contained in:
104
MPV/LUA/main.lua
104
MPV/LUA/main.lua
@@ -4,7 +4,7 @@ local msg = require 'mp.msg'
|
||||
|
||||
local M = {}
|
||||
|
||||
local MEDEIA_LUA_VERSION = '2026-03-21.1'
|
||||
local MEDEIA_LUA_VERSION = '2026-03-21.3'
|
||||
|
||||
-- Expose a tiny breadcrumb for debugging which script version is loaded.
|
||||
pcall(mp.set_property, 'user-data/medeia-lua-version', MEDEIA_LUA_VERSION)
|
||||
@@ -748,6 +748,7 @@ local _current_url_for_web_actions
|
||||
local _store_status_hint_for_url
|
||||
local _refresh_current_store_url_status
|
||||
local _skip_next_store_check_url = ''
|
||||
local _pick_folder_windows
|
||||
|
||||
local function _normalize_store_name(store)
|
||||
store = trim(tostring(store or ''))
|
||||
@@ -755,6 +756,22 @@ local function _normalize_store_name(store)
|
||||
return trim(store)
|
||||
end
|
||||
|
||||
local function _is_cached_store_name(store)
|
||||
local needle = _normalize_store_name(store)
|
||||
if needle == '' then
|
||||
return false
|
||||
end
|
||||
if type(_cached_store_names) ~= 'table' then
|
||||
return false
|
||||
end
|
||||
for _, name in ipairs(_cached_store_names) do
|
||||
if _normalize_store_name(name) == needle then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function _get_script_opts_dir()
|
||||
local dir = nil
|
||||
pcall(function()
|
||||
@@ -863,7 +880,7 @@ local function _is_pipeline_helper_ready()
|
||||
helper_version = mp.get_property_native('user-data/medeia-pipeline-helper-version')
|
||||
end
|
||||
helper_version = tostring(helper_version or '')
|
||||
if helper_version ~= '2026-03-22.2' then
|
||||
if helper_version ~= '2026-03-22.4' then
|
||||
return false
|
||||
end
|
||||
|
||||
@@ -930,7 +947,7 @@ local function _helper_ready_diagnostics()
|
||||
end
|
||||
return 'ready=' .. tostring(ready or '')
|
||||
.. ' helper_version=' .. tostring(helper_version or '')
|
||||
.. ' required_version=2026-03-22.2'
|
||||
.. ' required_version=2026-03-22.4'
|
||||
.. ' last_value=' .. tostring(_helper_ready_last_value or '')
|
||||
.. ' last_seen_age=' .. tostring(age)
|
||||
end
|
||||
@@ -973,12 +990,29 @@ end
|
||||
local function attempt_start_pipeline_helper_async(callback)
|
||||
-- Async version: spawn helper without blocking UI. Calls callback(success) when done.
|
||||
callback = callback or function() end
|
||||
local helper_start_state = M._helper_start_state or { inflight = false, callbacks = {} }
|
||||
M._helper_start_state = helper_start_state
|
||||
|
||||
local function finish(success)
|
||||
local callbacks = helper_start_state.callbacks or {}
|
||||
helper_start_state.callbacks = {}
|
||||
helper_start_state.inflight = false
|
||||
for _, cb in ipairs(callbacks) do
|
||||
pcall(cb, success)
|
||||
end
|
||||
end
|
||||
|
||||
if _is_pipeline_helper_ready() then
|
||||
callback(true)
|
||||
return
|
||||
end
|
||||
|
||||
if helper_start_state.inflight then
|
||||
table.insert(helper_start_state.callbacks, callback)
|
||||
_lua_log('attempt_start_pipeline_helper_async: join existing startup')
|
||||
return
|
||||
end
|
||||
|
||||
-- Debounce: don't spawn multiple helpers in quick succession
|
||||
local now = mp.get_time()
|
||||
if _helper_start_debounce_ts > -1 and (now - _helper_start_debounce_ts) < HELPER_START_DEBOUNCE then
|
||||
@@ -987,6 +1021,8 @@ local function attempt_start_pipeline_helper_async(callback)
|
||||
return
|
||||
end
|
||||
_helper_start_debounce_ts = now
|
||||
helper_start_state.inflight = true
|
||||
helper_start_state.callbacks = { callback }
|
||||
|
||||
-- Clear any stale ready heartbeat from an earlier helper instance before spawning.
|
||||
pcall(mp.set_property, PIPELINE_READY_PROP, '')
|
||||
@@ -997,7 +1033,7 @@ local function attempt_start_pipeline_helper_async(callback)
|
||||
local python = _resolve_python_exe(true)
|
||||
if not python or python == '' then
|
||||
_lua_log('attempt_start_pipeline_helper_async: no python executable available')
|
||||
callback(false)
|
||||
finish(false)
|
||||
return
|
||||
end
|
||||
|
||||
@@ -1015,7 +1051,7 @@ local function attempt_start_pipeline_helper_async(callback)
|
||||
|
||||
if not ok then
|
||||
_lua_log('attempt_start_pipeline_helper_async: spawn failed final=' .. tostring(detail or _describe_subprocess_result(result)))
|
||||
callback(false)
|
||||
finish(false)
|
||||
return
|
||||
end
|
||||
|
||||
@@ -1026,13 +1062,13 @@ local function attempt_start_pipeline_helper_async(callback)
|
||||
if _is_pipeline_helper_ready() then
|
||||
timer:kill()
|
||||
_lua_log('attempt_start_pipeline_helper_async: helper ready')
|
||||
callback(true)
|
||||
finish(true)
|
||||
return
|
||||
end
|
||||
if mp.get_time() >= deadline then
|
||||
timer:kill()
|
||||
_lua_log('attempt_start_pipeline_helper_async: timeout waiting for ready')
|
||||
callback(false)
|
||||
finish(false)
|
||||
end
|
||||
end)
|
||||
end
|
||||
@@ -1954,6 +1990,7 @@ local function _start_screenshot_store_save(store, out_path, tags)
|
||||
return false
|
||||
end
|
||||
|
||||
local is_named_store = _is_cached_store_name(store)
|
||||
local tag_list = _normalize_tag_list(tags)
|
||||
local screenshot_url = trim(tostring((_current_url_for_web_actions and _current_url_for_web_actions()) or mp.get_property(CURRENT_WEB_URL_PROP) or ''))
|
||||
if screenshot_url == '' or not screenshot_url:match('^https?://') then
|
||||
@@ -1961,29 +1998,35 @@ local function _start_screenshot_store_save(store, out_path, tags)
|
||||
end
|
||||
local cmd = 'add-file -store ' .. quote_pipeline_arg(store)
|
||||
.. ' -path ' .. quote_pipeline_arg(out_path)
|
||||
_set_selected_store(store)
|
||||
if screenshot_url ~= '' then
|
||||
cmd = cmd .. ' -url ' .. quote_pipeline_arg(screenshot_url)
|
||||
end
|
||||
if is_named_store then
|
||||
_set_selected_store(store)
|
||||
end
|
||||
|
||||
local tag_suffix = (#tag_list > 0) and (' | tags: ' .. tostring(#tag_list)) or ''
|
||||
if #tag_list > 0 then
|
||||
local tag_string = table.concat(tag_list, ',')
|
||||
cmd = cmd .. ' | add-tag ' .. quote_pipeline_arg(tag_string)
|
||||
end
|
||||
if screenshot_url ~= '' then
|
||||
cmd = cmd .. ' | add-url ' .. quote_pipeline_arg(screenshot_url)
|
||||
end
|
||||
|
||||
local queue_target = is_named_store and ('store ' .. store) or 'folder'
|
||||
local success_text = is_named_store and ('Screenshot saved to store: ' .. store .. tag_suffix) or ('Screenshot saved to folder' .. tag_suffix)
|
||||
local failure_text = is_named_store and 'Screenshot upload failed' or 'Screenshot save failed'
|
||||
|
||||
_lua_log('screenshot-save: queueing repl pipeline cmd=' .. cmd)
|
||||
|
||||
return _queue_pipeline_in_repl(
|
||||
cmd,
|
||||
'Queued in REPL: screenshot -> ' .. store .. tag_suffix,
|
||||
'Queued in REPL: screenshot -> ' .. queue_target .. tag_suffix,
|
||||
'Screenshot queue failed',
|
||||
'screenshot-save',
|
||||
{
|
||||
kind = 'mpv-screenshot',
|
||||
mpv_notify = {
|
||||
success_text = 'Screenshot saved to store: ' .. store .. tag_suffix,
|
||||
failure_text = 'Screenshot upload failed',
|
||||
success_text = success_text,
|
||||
failure_text = failure_text,
|
||||
duration_ms = 3500,
|
||||
},
|
||||
}
|
||||
@@ -2049,6 +2092,14 @@ local function _open_store_picker_for_pending_screenshot()
|
||||
local selected = _get_selected_store()
|
||||
local items = {}
|
||||
|
||||
if _is_windows() then
|
||||
items[#items + 1] = {
|
||||
title = 'Pick folder…',
|
||||
hint = 'Save screenshot to a local folder',
|
||||
value = { 'script-message-to', mp.get_script_name(), 'medeia-image-screenshot-pick-path', '{}' },
|
||||
}
|
||||
end
|
||||
|
||||
if type(_cached_store_names) == 'table' and #_cached_store_names > 0 then
|
||||
for _, name in ipairs(_cached_store_names) do
|
||||
name = trim(tostring(name or ''))
|
||||
@@ -2061,7 +2112,7 @@ local function _open_store_picker_for_pending_screenshot()
|
||||
}
|
||||
end
|
||||
end
|
||||
else
|
||||
elseif #items == 0 then
|
||||
items[#items + 1] = {
|
||||
title = 'No stores found',
|
||||
hint = 'Configure stores in config.conf',
|
||||
@@ -2149,6 +2200,9 @@ local function _capture_screenshot()
|
||||
local function dispatch_screenshot_save()
|
||||
local store_count = (type(_cached_store_names) == 'table') and #_cached_store_names or 0
|
||||
local selected_store = _normalize_store_name(_get_selected_store())
|
||||
if not _is_cached_store_name(selected_store) then
|
||||
selected_store = ''
|
||||
end
|
||||
|
||||
if store_count > 1 then
|
||||
_pending_screenshot = { path = out_path }
|
||||
@@ -2200,6 +2254,24 @@ mp.register_script_message('medeia-image-screenshot-pick-store', function(json)
|
||||
_open_screenshot_tag_prompt(store, out_path)
|
||||
end)
|
||||
|
||||
mp.register_script_message('medeia-image-screenshot-pick-path', function()
|
||||
if type(_pending_screenshot) ~= 'table' or not _pending_screenshot.path then
|
||||
return
|
||||
end
|
||||
if not _is_windows() then
|
||||
mp.osd_message('Folder picker is Windows-only', 4)
|
||||
return
|
||||
end
|
||||
|
||||
local folder = _pick_folder_windows()
|
||||
if not folder or folder == '' then
|
||||
return
|
||||
end
|
||||
|
||||
local out_path = tostring(_pending_screenshot.path or '')
|
||||
_open_screenshot_tag_prompt(folder, out_path)
|
||||
end)
|
||||
|
||||
mp.register_script_message('medeia-image-screenshot-tags-search', function(query)
|
||||
_apply_screenshot_tag_query(query)
|
||||
end)
|
||||
@@ -2591,7 +2663,7 @@ local function _extract_store_hash(target)
|
||||
return nil
|
||||
end
|
||||
|
||||
local function _pick_folder_windows()
|
||||
_pick_folder_windows = function()
|
||||
-- Native folder picker via PowerShell + WinForms.
|
||||
local ps = [[Add-Type -AssemblyName System.Windows.Forms; $d = New-Object System.Windows.Forms.FolderBrowserDialog; $d.Description = 'Select download folder'; $d.ShowNewFolderButton = $true; if ($d.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) { $d.SelectedPath }]]
|
||||
local res = utils.subprocess({
|
||||
|
||||
@@ -20,7 +20,7 @@ This helper is intentionally minimal: one request at a time, last-write-wins.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
MEDEIA_MPV_HELPER_VERSION = "2026-03-22.2"
|
||||
MEDEIA_MPV_HELPER_VERSION = "2026-03-22.4"
|
||||
|
||||
import argparse
|
||||
import json
|
||||
@@ -194,20 +194,35 @@ def _append_prefixed_log_lines(prefix: str, text: Any, *, max_lines: int = 40) -
|
||||
break
|
||||
|
||||
|
||||
def _start_ready_heartbeat(ipc_path: str, stop_event: threading.Event) -> threading.Thread:
|
||||
def _start_ready_heartbeat(
|
||||
ipc_path: str,
|
||||
stop_event: threading.Event,
|
||||
mark_alive: Optional[Callable[[str], None]] = None,
|
||||
note_ipc_unavailable: Optional[Callable[[str], None]] = None,
|
||||
) -> threading.Thread:
|
||||
"""Keep READY_PROP fresh even when the main loop blocks on Windows pipes."""
|
||||
|
||||
def _heartbeat_loop() -> None:
|
||||
hb_client = MPVIPCClient(socket_path=ipc_path, timeout=0.5, silent=True)
|
||||
while not stop_event.is_set():
|
||||
try:
|
||||
if hb_client.sock is None and not hb_client.connect():
|
||||
stop_event.wait(0.25)
|
||||
continue
|
||||
was_disconnected = hb_client.sock is None
|
||||
if was_disconnected:
|
||||
if not hb_client.connect():
|
||||
if note_ipc_unavailable is not None:
|
||||
note_ipc_unavailable("heartbeat-connect")
|
||||
stop_event.wait(0.25)
|
||||
continue
|
||||
if mark_alive is not None:
|
||||
mark_alive("heartbeat-connect")
|
||||
hb_client.send_command_no_wait(
|
||||
["set_property_string", READY_PROP, str(int(time.time()))]
|
||||
)
|
||||
if mark_alive is not None:
|
||||
mark_alive("heartbeat-send")
|
||||
except Exception:
|
||||
if note_ipc_unavailable is not None:
|
||||
note_ipc_unavailable("heartbeat-send")
|
||||
try:
|
||||
hb_client.disconnect()
|
||||
except Exception:
|
||||
@@ -1207,6 +1222,8 @@ def _start_request_poll_loop(
|
||||
ipc_path: str,
|
||||
stop_event: threading.Event,
|
||||
handle_request: Callable[[Any, str], bool],
|
||||
mark_alive: Optional[Callable[[str], None]] = None,
|
||||
note_ipc_unavailable: Optional[Callable[[str], None]] = None,
|
||||
) -> threading.Thread:
|
||||
"""Poll the request property on a separate IPC connection.
|
||||
|
||||
@@ -1219,12 +1236,20 @@ def _start_request_poll_loop(
|
||||
poll_client = MPVIPCClient(socket_path=ipc_path, timeout=0.75, silent=True)
|
||||
while not stop_event.is_set():
|
||||
try:
|
||||
if poll_client.sock is None and not poll_client.connect():
|
||||
stop_event.wait(0.10)
|
||||
continue
|
||||
was_disconnected = poll_client.sock is None
|
||||
if was_disconnected:
|
||||
if not poll_client.connect():
|
||||
if note_ipc_unavailable is not None:
|
||||
note_ipc_unavailable("request-poll-connect")
|
||||
stop_event.wait(0.10)
|
||||
continue
|
||||
if mark_alive is not None:
|
||||
mark_alive("request-poll-connect")
|
||||
|
||||
resp = poll_client.send_command(["get_property", REQUEST_PROP])
|
||||
if not resp:
|
||||
if note_ipc_unavailable is not None:
|
||||
note_ipc_unavailable("request-poll-read")
|
||||
try:
|
||||
poll_client.disconnect()
|
||||
except Exception:
|
||||
@@ -1232,10 +1257,14 @@ def _start_request_poll_loop(
|
||||
stop_event.wait(0.10)
|
||||
continue
|
||||
|
||||
if mark_alive is not None:
|
||||
mark_alive("request-poll-read")
|
||||
if resp.get("error") == "success":
|
||||
handle_request(resp.get("data"), "poll")
|
||||
stop_event.wait(0.05)
|
||||
except Exception:
|
||||
if note_ipc_unavailable is not None:
|
||||
note_ipc_unavailable("request-poll-exception")
|
||||
try:
|
||||
poll_client.disconnect()
|
||||
except Exception:
|
||||
@@ -1386,9 +1415,48 @@ def main(argv: Optional[list[str]] = None) -> int:
|
||||
seen_request_ttl_seconds = 180.0
|
||||
request_processing_lock = threading.Lock()
|
||||
command_client_lock = threading.Lock()
|
||||
stop_event = threading.Event()
|
||||
ipc_loss_grace_seconds = 4.0
|
||||
ipc_lost_since: Optional[float] = None
|
||||
ipc_connected_once = False
|
||||
shutdown_reason = ""
|
||||
shutdown_reason_lock = threading.Lock()
|
||||
_send_helper_command = lambda _command, _label='': False
|
||||
_publish_store_choices_cached_property = lambda _choices: None
|
||||
|
||||
def _request_shutdown(reason: str) -> None:
|
||||
nonlocal shutdown_reason
|
||||
message = str(reason or "").strip() or "unknown"
|
||||
with shutdown_reason_lock:
|
||||
if shutdown_reason:
|
||||
return
|
||||
shutdown_reason = message
|
||||
_append_helper_log(f"[helper] shutdown requested: {message}")
|
||||
stop_event.set()
|
||||
|
||||
def _mark_ipc_alive(source: str = "") -> None:
|
||||
nonlocal ipc_lost_since, ipc_connected_once
|
||||
if ipc_lost_since is not None and source:
|
||||
_append_helper_log(f"[helper] ipc restored via {source}")
|
||||
ipc_connected_once = True
|
||||
ipc_lost_since = None
|
||||
|
||||
def _note_ipc_unavailable(source: str) -> None:
|
||||
nonlocal ipc_lost_since
|
||||
if stop_event.is_set() or not ipc_connected_once:
|
||||
return
|
||||
now = time.time()
|
||||
if ipc_lost_since is None:
|
||||
ipc_lost_since = now
|
||||
_append_helper_log(
|
||||
f"[helper] ipc unavailable via {source}; waiting {ipc_loss_grace_seconds:.1f}s for reconnect"
|
||||
)
|
||||
return
|
||||
if (now - ipc_lost_since) >= ipc_loss_grace_seconds:
|
||||
_request_shutdown(
|
||||
f"mpv ipc unavailable for {now - ipc_lost_since:.2f}s via {source}"
|
||||
)
|
||||
|
||||
def _write_error_log(text: str, *, req_id: str) -> Optional[str]:
|
||||
try:
|
||||
error_log_dir.mkdir(parents=True, exist_ok=True)
|
||||
@@ -1593,6 +1661,7 @@ def main(argv: Optional[list[str]] = None) -> int:
|
||||
while True:
|
||||
try:
|
||||
if client.connect():
|
||||
_mark_ipc_alive("startup-connect")
|
||||
break
|
||||
except Exception as exc:
|
||||
last_connect_error = f"{type(exc).__name__}: {exc}"
|
||||
@@ -1611,26 +1680,32 @@ def main(argv: Optional[list[str]] = None) -> int:
|
||||
def _send_helper_command(command: Any, label: str = "") -> bool:
|
||||
with command_client_lock:
|
||||
try:
|
||||
if command_client.sock is None and not command_client.connect():
|
||||
_append_helper_log(
|
||||
f"[helper-ipc] connect failed label={label or '?'}"
|
||||
)
|
||||
return False
|
||||
if command_client.sock is None:
|
||||
if not command_client.connect():
|
||||
_append_helper_log(
|
||||
f"[helper-ipc] connect failed label={label or '?'}"
|
||||
)
|
||||
_note_ipc_unavailable(f"helper-command-connect:{label or '?' }")
|
||||
return False
|
||||
_mark_ipc_alive(f"helper-command-connect:{label or '?'}")
|
||||
rid = command_client.send_command_no_wait(command)
|
||||
if rid is None:
|
||||
_append_helper_log(
|
||||
f"[helper-ipc] send failed label={label or '?'}"
|
||||
)
|
||||
_note_ipc_unavailable(f"helper-command-send:{label or '?'}")
|
||||
try:
|
||||
command_client.disconnect()
|
||||
except Exception:
|
||||
pass
|
||||
return False
|
||||
_mark_ipc_alive(f"helper-command-send:{label or '?'}")
|
||||
return True
|
||||
except Exception as exc:
|
||||
_append_helper_log(
|
||||
f"[helper-ipc] exception label={label or '?'} error={type(exc).__name__}: {exc}"
|
||||
)
|
||||
_note_ipc_unavailable(f"helper-command-exception:{label or '?'}")
|
||||
try:
|
||||
command_client.disconnect()
|
||||
except Exception:
|
||||
@@ -1740,9 +1815,19 @@ def main(argv: Optional[list[str]] = None) -> int:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
heartbeat_stop = threading.Event()
|
||||
_start_ready_heartbeat(str(args.ipc), heartbeat_stop)
|
||||
_start_request_poll_loop(str(args.ipc), heartbeat_stop, _process_request)
|
||||
_start_ready_heartbeat(
|
||||
str(args.ipc),
|
||||
stop_event,
|
||||
_mark_ipc_alive,
|
||||
_note_ipc_unavailable,
|
||||
)
|
||||
_start_request_poll_loop(
|
||||
str(args.ipc),
|
||||
stop_event,
|
||||
_process_request,
|
||||
_mark_ipc_alive,
|
||||
_note_ipc_unavailable,
|
||||
)
|
||||
|
||||
# Pre-compute store choices at startup and publish to a cached property so Lua
|
||||
# can read immediately without waiting for a request/response cycle (which may timeout).
|
||||
@@ -1824,67 +1909,91 @@ def main(argv: Optional[list[str]] = None) -> int:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
while True:
|
||||
msg = client.read_message(timeout=0.25)
|
||||
if msg is None:
|
||||
# Keep READY fresh even when idle (Lua may clear it on timeouts).
|
||||
_touch_ready()
|
||||
time.sleep(0.02)
|
||||
continue
|
||||
try:
|
||||
while not stop_event.is_set():
|
||||
msg = client.read_message(timeout=0.25)
|
||||
if msg is None:
|
||||
if client.sock is None:
|
||||
_note_ipc_unavailable("main-read")
|
||||
else:
|
||||
_mark_ipc_alive("main-idle")
|
||||
# Keep READY fresh even when idle (Lua may clear it on timeouts).
|
||||
_touch_ready()
|
||||
time.sleep(0.02)
|
||||
continue
|
||||
|
||||
if msg.get("event") == "__eof__":
|
||||
try:
|
||||
_flush_mpv_repeat()
|
||||
except Exception:
|
||||
pass
|
||||
heartbeat_stop.set()
|
||||
return 0
|
||||
_mark_ipc_alive("main-read")
|
||||
|
||||
if msg.get("event") == "log-message":
|
||||
try:
|
||||
level = str(msg.get("level") or "")
|
||||
prefix = str(msg.get("prefix") or "")
|
||||
text = str(msg.get("text") or "").rstrip()
|
||||
if msg.get("event") == "__eof__":
|
||||
_request_shutdown("mpv closed ipc stream")
|
||||
break
|
||||
|
||||
if not text:
|
||||
continue
|
||||
if msg.get("event") == "log-message":
|
||||
try:
|
||||
level = str(msg.get("level") or "")
|
||||
prefix = str(msg.get("prefix") or "")
|
||||
text = str(msg.get("text") or "").rstrip()
|
||||
|
||||
# Filter excessive noise unless debug is enabled.
|
||||
if not debug_enabled:
|
||||
lower_prefix = prefix.lower()
|
||||
if "quic" in lower_prefix and "DEBUG:" in text:
|
||||
if not text:
|
||||
continue
|
||||
# Suppress progress-bar style lines (keep true errors).
|
||||
if ("ETA" in text or "%" in text) and ("ERROR:" not in text
|
||||
and "WARNING:" not in text):
|
||||
# Typical yt-dlp progress bar line.
|
||||
if text.lstrip().startswith("["):
|
||||
|
||||
# Filter excessive noise unless debug is enabled.
|
||||
if not debug_enabled:
|
||||
lower_prefix = prefix.lower()
|
||||
if "quic" in lower_prefix and "DEBUG:" in text:
|
||||
continue
|
||||
# Suppress progress-bar style lines (keep true errors).
|
||||
if ("ETA" in text or "%" in text) and ("ERROR:" not in text
|
||||
and "WARNING:" not in text):
|
||||
# Typical yt-dlp progress bar line.
|
||||
if text.lstrip().startswith("["):
|
||||
continue
|
||||
|
||||
line = f"[mpv {level}] {prefix} {text}".strip()
|
||||
line = f"[mpv {level}] {prefix} {text}".strip()
|
||||
|
||||
now = time.time()
|
||||
if last_mpv_line == line and (now - last_mpv_ts) < 2.0:
|
||||
last_mpv_count += 1
|
||||
now = time.time()
|
||||
if last_mpv_line == line and (now - last_mpv_ts) < 2.0:
|
||||
last_mpv_count += 1
|
||||
last_mpv_ts = now
|
||||
continue
|
||||
|
||||
_flush_mpv_repeat()
|
||||
last_mpv_line = line
|
||||
last_mpv_count = 1
|
||||
last_mpv_ts = now
|
||||
continue
|
||||
_append_helper_log(line)
|
||||
except Exception:
|
||||
pass
|
||||
continue
|
||||
|
||||
_flush_mpv_repeat()
|
||||
last_mpv_line = line
|
||||
last_mpv_count = 1
|
||||
last_mpv_ts = now
|
||||
_append_helper_log(line)
|
||||
if msg.get("event") != "property-change":
|
||||
continue
|
||||
|
||||
if msg.get("id") != OBS_ID_REQUEST:
|
||||
continue
|
||||
|
||||
_process_request(msg.get("data"), "observe")
|
||||
finally:
|
||||
stop_event.set()
|
||||
try:
|
||||
_flush_mpv_repeat()
|
||||
except Exception:
|
||||
pass
|
||||
if shutdown_reason:
|
||||
try:
|
||||
_append_helper_log(f"[helper] exiting reason={shutdown_reason}")
|
||||
except Exception:
|
||||
pass
|
||||
continue
|
||||
try:
|
||||
command_client.disconnect()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
client.disconnect()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if msg.get("event") != "property-change":
|
||||
continue
|
||||
|
||||
if msg.get("id") != OBS_ID_REQUEST:
|
||||
continue
|
||||
|
||||
_process_request(msg.get("data"), "observe")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -353,19 +353,35 @@ def list_configured_backend_names(config: Optional[Dict[str, Any]]) -> list[str]
|
||||
without triggering backend initialization (which may perform network calls).
|
||||
|
||||
Behaviour:
|
||||
- For each configured store type, returns the per-instance NAME override (case-insensitive)
|
||||
when present, otherwise the instance key.
|
||||
- Only includes store types that map to a discovered save backend class.
|
||||
- Skips folder/provider-only/unknown entries so UI pickers do not surface
|
||||
non-ingest destinations such as provider helpers.
|
||||
- For each configured store type, returns the per-instance NAME override
|
||||
(case-insensitive) when present, otherwise the instance key.
|
||||
"""
|
||||
try:
|
||||
store_cfg = (config or {}).get("store") or {}
|
||||
if not isinstance(store_cfg, dict):
|
||||
return []
|
||||
|
||||
classes_by_type = _discover_store_classes()
|
||||
names: list[str] = []
|
||||
for raw_store_type, instances in store_cfg.items():
|
||||
if not isinstance(instances, dict):
|
||||
continue
|
||||
store_type = _normalize_store_type(str(raw_store_type))
|
||||
if store_type == "folder" or store_type in _PROVIDER_ONLY_STORE_NAMES:
|
||||
continue
|
||||
|
||||
store_cls = classes_by_type.get(store_type)
|
||||
if store_cls is None:
|
||||
continue
|
||||
|
||||
for instance_name, instance_config in instances.items():
|
||||
try:
|
||||
_build_kwargs(store_cls, str(instance_name), instance_config)
|
||||
except Exception:
|
||||
continue
|
||||
if isinstance(instance_config, dict):
|
||||
override_name = _get_case_insensitive(dict(instance_config), "NAME")
|
||||
if override_name:
|
||||
|
||||
@@ -180,6 +180,7 @@ class Add_File(Cmdlet):
|
||||
arg=[
|
||||
SharedArgs.PATH,
|
||||
SharedArgs.STORE,
|
||||
SharedArgs.URL,
|
||||
SharedArgs.PROVIDER,
|
||||
CmdletArg(
|
||||
name="delete",
|
||||
@@ -220,6 +221,7 @@ class Add_File(Cmdlet):
|
||||
|
||||
path_arg = parsed.get("path")
|
||||
location = parsed.get("store")
|
||||
source_url_arg = parsed.get("url")
|
||||
provider_name = parsed.get("provider")
|
||||
delete_after = parsed.get("delete", False)
|
||||
|
||||
@@ -519,6 +521,29 @@ class Add_File(Cmdlet):
|
||||
for idx, item in enumerate(items_to_process, 1):
|
||||
pipe_obj = coerce_to_pipe_object(item, path_arg)
|
||||
|
||||
if source_url_arg:
|
||||
try:
|
||||
from SYS.metadata import normalize_urls
|
||||
|
||||
cli_urls = [u.strip() for u in str(source_url_arg).split(",") if u and u.strip()]
|
||||
merged_urls: List[str] = []
|
||||
|
||||
if isinstance(getattr(pipe_obj, "extra", None), dict):
|
||||
existing_url = pipe_obj.extra.get("url")
|
||||
if isinstance(existing_url, list):
|
||||
merged_urls.extend(str(u) for u in existing_url if u)
|
||||
elif isinstance(existing_url, str) and existing_url.strip():
|
||||
merged_urls.append(existing_url.strip())
|
||||
else:
|
||||
pipe_obj.extra = {}
|
||||
|
||||
merged_urls.extend(cli_urls)
|
||||
merged_urls = normalize_urls(merged_urls)
|
||||
if merged_urls:
|
||||
pipe_obj.extra["url"] = merged_urls
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
label = pipe_obj.title
|
||||
if not label and pipe_obj.path:
|
||||
|
||||
Reference in New Issue
Block a user