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 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.
|
-- Expose a tiny breadcrumb for debugging which script version is loaded.
|
||||||
pcall(mp.set_property, 'user-data/medeia-lua-version', MEDEIA_LUA_VERSION)
|
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 _store_status_hint_for_url
|
||||||
local _refresh_current_store_url_status
|
local _refresh_current_store_url_status
|
||||||
local _skip_next_store_check_url = ''
|
local _skip_next_store_check_url = ''
|
||||||
|
local _pick_folder_windows
|
||||||
|
|
||||||
local function _normalize_store_name(store)
|
local function _normalize_store_name(store)
|
||||||
store = trim(tostring(store or ''))
|
store = trim(tostring(store or ''))
|
||||||
@@ -755,6 +756,22 @@ local function _normalize_store_name(store)
|
|||||||
return trim(store)
|
return trim(store)
|
||||||
end
|
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 function _get_script_opts_dir()
|
||||||
local dir = nil
|
local dir = nil
|
||||||
pcall(function()
|
pcall(function()
|
||||||
@@ -863,7 +880,7 @@ local function _is_pipeline_helper_ready()
|
|||||||
helper_version = mp.get_property_native('user-data/medeia-pipeline-helper-version')
|
helper_version = mp.get_property_native('user-data/medeia-pipeline-helper-version')
|
||||||
end
|
end
|
||||||
helper_version = tostring(helper_version or '')
|
helper_version = tostring(helper_version or '')
|
||||||
if helper_version ~= '2026-03-22.2' then
|
if helper_version ~= '2026-03-22.4' then
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -930,7 +947,7 @@ local function _helper_ready_diagnostics()
|
|||||||
end
|
end
|
||||||
return 'ready=' .. tostring(ready or '')
|
return 'ready=' .. tostring(ready or '')
|
||||||
.. ' helper_version=' .. tostring(helper_version 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_value=' .. tostring(_helper_ready_last_value or '')
|
||||||
.. ' last_seen_age=' .. tostring(age)
|
.. ' last_seen_age=' .. tostring(age)
|
||||||
end
|
end
|
||||||
@@ -973,11 +990,28 @@ end
|
|||||||
local function attempt_start_pipeline_helper_async(callback)
|
local function attempt_start_pipeline_helper_async(callback)
|
||||||
-- Async version: spawn helper without blocking UI. Calls callback(success) when done.
|
-- Async version: spawn helper without blocking UI. Calls callback(success) when done.
|
||||||
callback = callback or function() end
|
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
|
if _is_pipeline_helper_ready() then
|
||||||
callback(true)
|
callback(true)
|
||||||
return
|
return
|
||||||
end
|
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
|
-- Debounce: don't spawn multiple helpers in quick succession
|
||||||
local now = mp.get_time()
|
local now = mp.get_time()
|
||||||
@@ -987,6 +1021,8 @@ local function attempt_start_pipeline_helper_async(callback)
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
_helper_start_debounce_ts = now
|
_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.
|
-- Clear any stale ready heartbeat from an earlier helper instance before spawning.
|
||||||
pcall(mp.set_property, PIPELINE_READY_PROP, '')
|
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)
|
local python = _resolve_python_exe(true)
|
||||||
if not python or python == '' then
|
if not python or python == '' then
|
||||||
_lua_log('attempt_start_pipeline_helper_async: no python executable available')
|
_lua_log('attempt_start_pipeline_helper_async: no python executable available')
|
||||||
callback(false)
|
finish(false)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -1015,7 +1051,7 @@ local function attempt_start_pipeline_helper_async(callback)
|
|||||||
|
|
||||||
if not ok then
|
if not ok then
|
||||||
_lua_log('attempt_start_pipeline_helper_async: spawn failed final=' .. tostring(detail or _describe_subprocess_result(result)))
|
_lua_log('attempt_start_pipeline_helper_async: spawn failed final=' .. tostring(detail or _describe_subprocess_result(result)))
|
||||||
callback(false)
|
finish(false)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -1026,13 +1062,13 @@ local function attempt_start_pipeline_helper_async(callback)
|
|||||||
if _is_pipeline_helper_ready() then
|
if _is_pipeline_helper_ready() then
|
||||||
timer:kill()
|
timer:kill()
|
||||||
_lua_log('attempt_start_pipeline_helper_async: helper ready')
|
_lua_log('attempt_start_pipeline_helper_async: helper ready')
|
||||||
callback(true)
|
finish(true)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
if mp.get_time() >= deadline then
|
if mp.get_time() >= deadline then
|
||||||
timer:kill()
|
timer:kill()
|
||||||
_lua_log('attempt_start_pipeline_helper_async: timeout waiting for ready')
|
_lua_log('attempt_start_pipeline_helper_async: timeout waiting for ready')
|
||||||
callback(false)
|
finish(false)
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
@@ -1954,6 +1990,7 @@ local function _start_screenshot_store_save(store, out_path, tags)
|
|||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local is_named_store = _is_cached_store_name(store)
|
||||||
local tag_list = _normalize_tag_list(tags)
|
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 ''))
|
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
|
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
|
end
|
||||||
local cmd = 'add-file -store ' .. quote_pipeline_arg(store)
|
local cmd = 'add-file -store ' .. quote_pipeline_arg(store)
|
||||||
.. ' -path ' .. quote_pipeline_arg(out_path)
|
.. ' -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 ''
|
local tag_suffix = (#tag_list > 0) and (' | tags: ' .. tostring(#tag_list)) or ''
|
||||||
if #tag_list > 0 then
|
if #tag_list > 0 then
|
||||||
local tag_string = table.concat(tag_list, ',')
|
local tag_string = table.concat(tag_list, ',')
|
||||||
cmd = cmd .. ' | add-tag ' .. quote_pipeline_arg(tag_string)
|
cmd = cmd .. ' | add-tag ' .. quote_pipeline_arg(tag_string)
|
||||||
end
|
end
|
||||||
if screenshot_url ~= '' then
|
|
||||||
cmd = cmd .. ' | add-url ' .. quote_pipeline_arg(screenshot_url)
|
local queue_target = is_named_store and ('store ' .. store) or 'folder'
|
||||||
end
|
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)
|
_lua_log('screenshot-save: queueing repl pipeline cmd=' .. cmd)
|
||||||
|
|
||||||
return _queue_pipeline_in_repl(
|
return _queue_pipeline_in_repl(
|
||||||
cmd,
|
cmd,
|
||||||
'Queued in REPL: screenshot -> ' .. store .. tag_suffix,
|
'Queued in REPL: screenshot -> ' .. queue_target .. tag_suffix,
|
||||||
'Screenshot queue failed',
|
'Screenshot queue failed',
|
||||||
'screenshot-save',
|
'screenshot-save',
|
||||||
{
|
{
|
||||||
kind = 'mpv-screenshot',
|
kind = 'mpv-screenshot',
|
||||||
mpv_notify = {
|
mpv_notify = {
|
||||||
success_text = 'Screenshot saved to store: ' .. store .. tag_suffix,
|
success_text = success_text,
|
||||||
failure_text = 'Screenshot upload failed',
|
failure_text = failure_text,
|
||||||
duration_ms = 3500,
|
duration_ms = 3500,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -2049,6 +2092,14 @@ local function _open_store_picker_for_pending_screenshot()
|
|||||||
local selected = _get_selected_store()
|
local selected = _get_selected_store()
|
||||||
local items = {}
|
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
|
if type(_cached_store_names) == 'table' and #_cached_store_names > 0 then
|
||||||
for _, name in ipairs(_cached_store_names) do
|
for _, name in ipairs(_cached_store_names) do
|
||||||
name = trim(tostring(name or ''))
|
name = trim(tostring(name or ''))
|
||||||
@@ -2061,7 +2112,7 @@ local function _open_store_picker_for_pending_screenshot()
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
else
|
elseif #items == 0 then
|
||||||
items[#items + 1] = {
|
items[#items + 1] = {
|
||||||
title = 'No stores found',
|
title = 'No stores found',
|
||||||
hint = 'Configure stores in config.conf',
|
hint = 'Configure stores in config.conf',
|
||||||
@@ -2149,6 +2200,9 @@ local function _capture_screenshot()
|
|||||||
local function dispatch_screenshot_save()
|
local function dispatch_screenshot_save()
|
||||||
local store_count = (type(_cached_store_names) == 'table') and #_cached_store_names or 0
|
local store_count = (type(_cached_store_names) == 'table') and #_cached_store_names or 0
|
||||||
local selected_store = _normalize_store_name(_get_selected_store())
|
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
|
if store_count > 1 then
|
||||||
_pending_screenshot = { path = out_path }
|
_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)
|
_open_screenshot_tag_prompt(store, out_path)
|
||||||
end)
|
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)
|
mp.register_script_message('medeia-image-screenshot-tags-search', function(query)
|
||||||
_apply_screenshot_tag_query(query)
|
_apply_screenshot_tag_query(query)
|
||||||
end)
|
end)
|
||||||
@@ -2591,7 +2663,7 @@ local function _extract_store_hash(target)
|
|||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
local function _pick_folder_windows()
|
_pick_folder_windows = function()
|
||||||
-- Native folder picker via PowerShell + WinForms.
|
-- 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 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({
|
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
|
from __future__ import annotations
|
||||||
|
|
||||||
MEDEIA_MPV_HELPER_VERSION = "2026-03-22.2"
|
MEDEIA_MPV_HELPER_VERSION = "2026-03-22.4"
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import json
|
import json
|
||||||
@@ -194,20 +194,35 @@ def _append_prefixed_log_lines(prefix: str, text: Any, *, max_lines: int = 40) -
|
|||||||
break
|
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."""
|
"""Keep READY_PROP fresh even when the main loop blocks on Windows pipes."""
|
||||||
|
|
||||||
def _heartbeat_loop() -> None:
|
def _heartbeat_loop() -> None:
|
||||||
hb_client = MPVIPCClient(socket_path=ipc_path, timeout=0.5, silent=True)
|
hb_client = MPVIPCClient(socket_path=ipc_path, timeout=0.5, silent=True)
|
||||||
while not stop_event.is_set():
|
while not stop_event.is_set():
|
||||||
try:
|
try:
|
||||||
if hb_client.sock is None and not hb_client.connect():
|
was_disconnected = hb_client.sock is None
|
||||||
stop_event.wait(0.25)
|
if was_disconnected:
|
||||||
continue
|
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(
|
hb_client.send_command_no_wait(
|
||||||
["set_property_string", READY_PROP, str(int(time.time()))]
|
["set_property_string", READY_PROP, str(int(time.time()))]
|
||||||
)
|
)
|
||||||
|
if mark_alive is not None:
|
||||||
|
mark_alive("heartbeat-send")
|
||||||
except Exception:
|
except Exception:
|
||||||
|
if note_ipc_unavailable is not None:
|
||||||
|
note_ipc_unavailable("heartbeat-send")
|
||||||
try:
|
try:
|
||||||
hb_client.disconnect()
|
hb_client.disconnect()
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -1207,6 +1222,8 @@ def _start_request_poll_loop(
|
|||||||
ipc_path: str,
|
ipc_path: str,
|
||||||
stop_event: threading.Event,
|
stop_event: threading.Event,
|
||||||
handle_request: Callable[[Any, str], bool],
|
handle_request: Callable[[Any, str], bool],
|
||||||
|
mark_alive: Optional[Callable[[str], None]] = None,
|
||||||
|
note_ipc_unavailable: Optional[Callable[[str], None]] = None,
|
||||||
) -> threading.Thread:
|
) -> threading.Thread:
|
||||||
"""Poll the request property on a separate IPC connection.
|
"""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)
|
poll_client = MPVIPCClient(socket_path=ipc_path, timeout=0.75, silent=True)
|
||||||
while not stop_event.is_set():
|
while not stop_event.is_set():
|
||||||
try:
|
try:
|
||||||
if poll_client.sock is None and not poll_client.connect():
|
was_disconnected = poll_client.sock is None
|
||||||
stop_event.wait(0.10)
|
if was_disconnected:
|
||||||
continue
|
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])
|
resp = poll_client.send_command(["get_property", REQUEST_PROP])
|
||||||
if not resp:
|
if not resp:
|
||||||
|
if note_ipc_unavailable is not None:
|
||||||
|
note_ipc_unavailable("request-poll-read")
|
||||||
try:
|
try:
|
||||||
poll_client.disconnect()
|
poll_client.disconnect()
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -1232,10 +1257,14 @@ def _start_request_poll_loop(
|
|||||||
stop_event.wait(0.10)
|
stop_event.wait(0.10)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
if mark_alive is not None:
|
||||||
|
mark_alive("request-poll-read")
|
||||||
if resp.get("error") == "success":
|
if resp.get("error") == "success":
|
||||||
handle_request(resp.get("data"), "poll")
|
handle_request(resp.get("data"), "poll")
|
||||||
stop_event.wait(0.05)
|
stop_event.wait(0.05)
|
||||||
except Exception:
|
except Exception:
|
||||||
|
if note_ipc_unavailable is not None:
|
||||||
|
note_ipc_unavailable("request-poll-exception")
|
||||||
try:
|
try:
|
||||||
poll_client.disconnect()
|
poll_client.disconnect()
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -1386,9 +1415,48 @@ def main(argv: Optional[list[str]] = None) -> int:
|
|||||||
seen_request_ttl_seconds = 180.0
|
seen_request_ttl_seconds = 180.0
|
||||||
request_processing_lock = threading.Lock()
|
request_processing_lock = threading.Lock()
|
||||||
command_client_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
|
_send_helper_command = lambda _command, _label='': False
|
||||||
_publish_store_choices_cached_property = lambda _choices: None
|
_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]:
|
def _write_error_log(text: str, *, req_id: str) -> Optional[str]:
|
||||||
try:
|
try:
|
||||||
error_log_dir.mkdir(parents=True, exist_ok=True)
|
error_log_dir.mkdir(parents=True, exist_ok=True)
|
||||||
@@ -1593,6 +1661,7 @@ def main(argv: Optional[list[str]] = None) -> int:
|
|||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
if client.connect():
|
if client.connect():
|
||||||
|
_mark_ipc_alive("startup-connect")
|
||||||
break
|
break
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
last_connect_error = f"{type(exc).__name__}: {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:
|
def _send_helper_command(command: Any, label: str = "") -> bool:
|
||||||
with command_client_lock:
|
with command_client_lock:
|
||||||
try:
|
try:
|
||||||
if command_client.sock is None and not command_client.connect():
|
if command_client.sock is None:
|
||||||
_append_helper_log(
|
if not command_client.connect():
|
||||||
f"[helper-ipc] connect failed label={label or '?'}"
|
_append_helper_log(
|
||||||
)
|
f"[helper-ipc] connect failed label={label or '?'}"
|
||||||
return False
|
)
|
||||||
|
_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)
|
rid = command_client.send_command_no_wait(command)
|
||||||
if rid is None:
|
if rid is None:
|
||||||
_append_helper_log(
|
_append_helper_log(
|
||||||
f"[helper-ipc] send failed label={label or '?'}"
|
f"[helper-ipc] send failed label={label or '?'}"
|
||||||
)
|
)
|
||||||
|
_note_ipc_unavailable(f"helper-command-send:{label or '?'}")
|
||||||
try:
|
try:
|
||||||
command_client.disconnect()
|
command_client.disconnect()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
return False
|
return False
|
||||||
|
_mark_ipc_alive(f"helper-command-send:{label or '?'}")
|
||||||
return True
|
return True
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
_append_helper_log(
|
_append_helper_log(
|
||||||
f"[helper-ipc] exception label={label or '?'} error={type(exc).__name__}: {exc}"
|
f"[helper-ipc] exception label={label or '?'} error={type(exc).__name__}: {exc}"
|
||||||
)
|
)
|
||||||
|
_note_ipc_unavailable(f"helper-command-exception:{label or '?'}")
|
||||||
try:
|
try:
|
||||||
command_client.disconnect()
|
command_client.disconnect()
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -1740,9 +1815,19 @@ def main(argv: Optional[list[str]] = None) -> int:
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
heartbeat_stop = threading.Event()
|
_start_ready_heartbeat(
|
||||||
_start_ready_heartbeat(str(args.ipc), heartbeat_stop)
|
str(args.ipc),
|
||||||
_start_request_poll_loop(str(args.ipc), heartbeat_stop, _process_request)
|
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
|
# 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).
|
# 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:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
while True:
|
try:
|
||||||
msg = client.read_message(timeout=0.25)
|
while not stop_event.is_set():
|
||||||
if msg is None:
|
msg = client.read_message(timeout=0.25)
|
||||||
# Keep READY fresh even when idle (Lua may clear it on timeouts).
|
if msg is None:
|
||||||
_touch_ready()
|
if client.sock is None:
|
||||||
time.sleep(0.02)
|
_note_ipc_unavailable("main-read")
|
||||||
continue
|
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__":
|
_mark_ipc_alive("main-read")
|
||||||
try:
|
|
||||||
_flush_mpv_repeat()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
heartbeat_stop.set()
|
|
||||||
return 0
|
|
||||||
|
|
||||||
if msg.get("event") == "log-message":
|
if msg.get("event") == "__eof__":
|
||||||
try:
|
_request_shutdown("mpv closed ipc stream")
|
||||||
level = str(msg.get("level") or "")
|
break
|
||||||
prefix = str(msg.get("prefix") or "")
|
|
||||||
text = str(msg.get("text") or "").rstrip()
|
|
||||||
|
|
||||||
if not text:
|
if msg.get("event") == "log-message":
|
||||||
continue
|
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 text:
|
||||||
if not debug_enabled:
|
|
||||||
lower_prefix = prefix.lower()
|
|
||||||
if "quic" in lower_prefix and "DEBUG:" in text:
|
|
||||||
continue
|
continue
|
||||||
# Suppress progress-bar style lines (keep true errors).
|
|
||||||
if ("ETA" in text or "%" in text) and ("ERROR:" not in text
|
# Filter excessive noise unless debug is enabled.
|
||||||
and "WARNING:" not in text):
|
if not debug_enabled:
|
||||||
# Typical yt-dlp progress bar line.
|
lower_prefix = prefix.lower()
|
||||||
if text.lstrip().startswith("["):
|
if "quic" in lower_prefix and "DEBUG:" in text:
|
||||||
continue
|
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()
|
now = time.time()
|
||||||
if last_mpv_line == line and (now - last_mpv_ts) < 2.0:
|
if last_mpv_line == line and (now - last_mpv_ts) < 2.0:
|
||||||
last_mpv_count += 1
|
last_mpv_count += 1
|
||||||
|
last_mpv_ts = now
|
||||||
|
continue
|
||||||
|
|
||||||
|
_flush_mpv_repeat()
|
||||||
|
last_mpv_line = line
|
||||||
|
last_mpv_count = 1
|
||||||
last_mpv_ts = now
|
last_mpv_ts = now
|
||||||
continue
|
_append_helper_log(line)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
continue
|
||||||
|
|
||||||
_flush_mpv_repeat()
|
if msg.get("event") != "property-change":
|
||||||
last_mpv_line = line
|
continue
|
||||||
last_mpv_count = 1
|
|
||||||
last_mpv_ts = now
|
if msg.get("id") != OBS_ID_REQUEST:
|
||||||
_append_helper_log(line)
|
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:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
continue
|
try:
|
||||||
|
command_client.disconnect()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
client.disconnect()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
if msg.get("event") != "property-change":
|
return 0
|
||||||
continue
|
|
||||||
|
|
||||||
if msg.get("id") != OBS_ID_REQUEST:
|
|
||||||
continue
|
|
||||||
|
|
||||||
_process_request(msg.get("data"), "observe")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
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).
|
without triggering backend initialization (which may perform network calls).
|
||||||
|
|
||||||
Behaviour:
|
Behaviour:
|
||||||
- For each configured store type, returns the per-instance NAME override (case-insensitive)
|
- Only includes store types that map to a discovered save backend class.
|
||||||
when present, otherwise the instance key.
|
- 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:
|
try:
|
||||||
store_cfg = (config or {}).get("store") or {}
|
store_cfg = (config or {}).get("store") or {}
|
||||||
if not isinstance(store_cfg, dict):
|
if not isinstance(store_cfg, dict):
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
classes_by_type = _discover_store_classes()
|
||||||
names: list[str] = []
|
names: list[str] = []
|
||||||
for raw_store_type, instances in store_cfg.items():
|
for raw_store_type, instances in store_cfg.items():
|
||||||
if not isinstance(instances, dict):
|
if not isinstance(instances, dict):
|
||||||
continue
|
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():
|
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):
|
if isinstance(instance_config, dict):
|
||||||
override_name = _get_case_insensitive(dict(instance_config), "NAME")
|
override_name = _get_case_insensitive(dict(instance_config), "NAME")
|
||||||
if override_name:
|
if override_name:
|
||||||
|
|||||||
@@ -180,6 +180,7 @@ class Add_File(Cmdlet):
|
|||||||
arg=[
|
arg=[
|
||||||
SharedArgs.PATH,
|
SharedArgs.PATH,
|
||||||
SharedArgs.STORE,
|
SharedArgs.STORE,
|
||||||
|
SharedArgs.URL,
|
||||||
SharedArgs.PROVIDER,
|
SharedArgs.PROVIDER,
|
||||||
CmdletArg(
|
CmdletArg(
|
||||||
name="delete",
|
name="delete",
|
||||||
@@ -220,6 +221,7 @@ class Add_File(Cmdlet):
|
|||||||
|
|
||||||
path_arg = parsed.get("path")
|
path_arg = parsed.get("path")
|
||||||
location = parsed.get("store")
|
location = parsed.get("store")
|
||||||
|
source_url_arg = parsed.get("url")
|
||||||
provider_name = parsed.get("provider")
|
provider_name = parsed.get("provider")
|
||||||
delete_after = parsed.get("delete", False)
|
delete_after = parsed.get("delete", False)
|
||||||
|
|
||||||
@@ -519,6 +521,29 @@ class Add_File(Cmdlet):
|
|||||||
for idx, item in enumerate(items_to_process, 1):
|
for idx, item in enumerate(items_to_process, 1):
|
||||||
pipe_obj = coerce_to_pipe_object(item, path_arg)
|
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:
|
try:
|
||||||
label = pipe_obj.title
|
label = pipe_obj.title
|
||||||
if not label and pipe_obj.path:
|
if not label and pipe_obj.path:
|
||||||
|
|||||||
Reference in New Issue
Block a user