From 6a0348deb81c8295e7a64ee04fe1f1bfa898543d Mon Sep 17 00:00:00 2001 From: TheSiahxyz <164138827+TheSiahxyz@users.noreply.github.com> Date: Sat, 20 Jun 2026 12:58:26 +0900 Subject: modified mpv/input.conf, modified scripts/integrity-check.lua --- ar/.config/mpv/scripts/integrity-check.lua | 177 +++++++++++++++++++++-------- 1 file changed, 132 insertions(+), 45 deletions(-) (limited to 'ar/.config/mpv/scripts/integrity-check.lua') diff --git a/ar/.config/mpv/scripts/integrity-check.lua b/ar/.config/mpv/scripts/integrity-check.lua index f6b7c27..4af1325 100644 --- a/ar/.config/mpv/scripts/integrity-check.lua +++ b/ar/.config/mpv/scripts/integrity-check.lua @@ -3,12 +3,17 @@ -- Shows a badge in the top-right ONLY when corrupt; healthy files show nothing. -- -- Behaviour: --- * On file-loaded, run an ffmpeg demux check in the background (playback continues). +-- * Checking starts on the "scan" key (script-message integrity-scan), or +-- automatically on file open if scan_on_load=yes. ffmpeg demux runs in the +-- background, so playback continues. -- * Abort on the first error with -xerror -> corrupt files are judged quickly. -- * Results are cached by path + mtime -> the same file shows instantly next time. -- * Corrupt file paths are recorded in corrupted.log. -- * If a playlist exists (scan_playlist), after the current file is checked, --- the following entries are pre-checked one at a time in the background. +-- the following entries are pre-checked in parallel in the background +-- (scan_concurrency workers). Background checks are read-rate throttled +-- (bg_read_rate x realtime) and run at low CPU priority so they don't +-- starve playback I/O. The current file is always checked at full speed. -- (The badge is only for the current file; pre-checks update cache/log only.) -- * When the playlist pre-check finishes, an OSD summary is shown (notify_done). -- script-message integrity-status shows the current progress at any time. @@ -26,13 +31,23 @@ local msg = require 'mp.msg' ---------------------------------------------------------------------- local opts = { enabled = true, -- feature on/off - scan_on_load = true, -- auto-check when a file opens + scan_on_load = false, -- auto-check on file open; if false, trigger with the "scan" key scan_playlist = true, -- after the current file, also background-check following playlist entries + scan_concurrency = 2, -- how many playlist entries to check in parallel notify_done = true, -- show an OSD summary when the playlist background check finishes show_scanning = false, -- show a "scanning" badge (default: off) deep_scan = false, -- if true, decode while checking (precise but slow) use_cache = true, -- use the result cache ffmpeg = "ffmpeg", + -- Throttle background playlist checks to N x realtime so they don't flood + -- the disk/cache and stutter playback (ionice is ignored by the `none` + -- scheduler, so rate-limiting is what actually protects playback). + -- 0 = unlimited. The current file is always checked at full speed. + bg_read_rate = 8, + bg_read_burst = 30, -- seconds to read at full speed first (headers/early errors) + -- Low-priority wrapper (helps userspace CPU; ionice is a no-op on `none`). + -- Set to "" to disable. (Linux: coreutils `nice`, util-linux `ionice`.) + priority_cmd = "nice -n 19 ionice -c 3", font_size = 22, } require('mp.options').read_options(opts, "integrity-check") @@ -53,6 +68,7 @@ local overlay = mp.create_osd_overlay("ass-events") local current_path = nil local scan_token = 0 -- to ignore stale callbacks local bg_token = 0 -- to invalidate the playlist background scan +local current_scanning = false -- true while the current file is being checked ---------------------------------------------------------------------- -- Cache I/O @@ -150,15 +166,30 @@ end ---------------------------------------------------------------------- -- Check ---------------------------------------------------------------------- -local function build_args(path) - if opts.deep_scan then - -- with decoding: precise but slow - return { opts.ffmpeg, "-hide_banner", "-v", "error", "-xerror", - "-i", path, "-map", "0", "-f", "null", "-" } +-- read_rate: nil/0 = full speed; >0 = throttle reading to that many x realtime +-- (input option, must precede -i) so background checks don't starve playback. +local function build_args(path, read_rate) + local args = {} + -- Prepend the low-priority wrapper (nice helps userspace CPU). + if opts.priority_cmd and opts.priority_cmd ~= "" then + for word in opts.priority_cmd:gmatch("%S+") do + args[#args + 1] = word + end + end + local function add(...) + for _, v in ipairs({ ... }) do args[#args + 1] = v end + end + add(opts.ffmpeg, "-hide_banner", "-v", "error", "-xerror") + if read_rate and read_rate > 0 then + add("-readrate", tostring(read_rate), + "-readrate_initial_burst", tostring(opts.bg_read_burst or 30)) + end + add("-i", path) + if not opts.deep_scan then -- demux only (fast); deep_scan decodes (slow) + add("-c", "copy") end - -- demux only: fast; detects container/truncation/cutoff - return { opts.ffmpeg, "-hide_banner", "-v", "error", "-xerror", - "-i", path, "-c", "copy", "-map", "0", "-f", "null", "-" } + add("-map", "0", "-f", "null", "-") + return args end local function apply_result(path, corrupt, from_cache) @@ -203,6 +234,7 @@ end local function start_scan(path, on_done) scan_token = scan_token + 1 local token = scan_token + current_scanning = true show_scanning() mp.command_native_async({ name = "subprocess", @@ -212,6 +244,7 @@ local function start_scan(path, on_done) capture_stderr = true, }, function(success, result, err) if token ~= scan_token then return end -- moved to another file -> ignore + current_scanning = false if not result then hide_badge(); return end if result.killed_by_us then return end if result.error_string == "init" then @@ -276,16 +309,24 @@ local function playlist_stats() return s end --- Scan sequentially, one at a time, from start_index to the end of the playlist +-- Scan playlist entries from start_index to the end, up to scan_concurrency at +-- a time (parallel). Each check is a low-priority external ffmpeg process, so +-- playback is never blocked even with several running at once. local function scan_playlist_from(start_index) cancel_bg() local token = bg_token local count = mp.get_property_number("playlist-count", 0) + local workers = math.max(1, opts.scan_concurrency or 1) + local next_i = start_index + local active = 0 local did_scan = false -- whether this run actually checked any new entry + local notified = false - -- Summary notification when reaching the end (only if something new was + -- Summary notification once all workers drain (only if something new was -- scanned - avoids duplicate notifications during autoplay) local function finish() + if notified then return end + notified = true if not (opts.notify_done and did_scan) then return end local s = playlist_stats() local txt = (s.corrupt > 0) @@ -295,33 +336,53 @@ local function scan_playlist_from(start_index) msg.info(txt) end - local function step(i) - if token ~= bg_token then return end -- file change / toggle -> stop - if i >= count then finish(); return end - local path = resolve_playlist_path( - mp.get_property("playlist/" .. i .. "/filename")) - if not scannable(path) then return step(i + 1) end - if path == current_path then return step(i + 1) end -- current file already checked - if cached_fresh(path) then return step(i + 1) end -- already checked, skip - - did_scan = true - mp.command_native_async({ - name = "subprocess", - args = build_args(path), - playback_only = false, - capture_stdout = true, - capture_stderr = true, - }, function(success, result, err) - if token ~= bg_token then return end -- invalidated midway -> ignore - if result and not result.killed_by_us - and result.error_string ~= "init" then - record_result_quiet(path, determine_corrupt(result)) + local pump -- forward declaration + + local function on_done(path, result) + active = active - 1 + -- Keep any finished result even if this sweep was superseded (file + -- switched): the ffmpeg work is already done, so persist it to cache. + if result and not result.killed_by_us + and result.error_string ~= "init" then + record_result_quiet(path, determine_corrupt(result)) + end + if token ~= bg_token then return end -- superseded -> don't dispatch more + pump() + end + + -- Fill free worker slots with the next eligible entries. + pump = function() + if token ~= bg_token then return end + while active < workers do + local path + while next_i < count do + local p = resolve_playlist_path( + mp.get_property("playlist/" .. next_i .. "/filename")) + next_i = next_i + 1 + if scannable(p) and p ~= current_path and not cached_fresh(p) then + path = p + break + end end - step(i + 1) - end) + if not path then -- nothing left to dispatch + if active == 0 then finish() end + return + end + active = active + 1 + did_scan = true + mp.command_native_async({ + name = "subprocess", + args = build_args(path, opts.bg_read_rate), + playback_only = false, + capture_stdout = true, + capture_stderr = true, + }, function(success, result, err) + on_done(path, result) + end) + end end - step(start_index) + pump() end -- If a playlist exists, start the background scan from the entry after the current position @@ -336,16 +397,13 @@ end ---------------------------------------------------------------------- -- Events ---------------------------------------------------------------------- -local function on_file_loaded() - hide_badge() - cancel_bg() -- stop the previous playlist scan - current_path = mp.get_property("path") - if not opts.enabled or not opts.scan_on_load then return end +-- Check the current file, then (if any) sweep the rest of the playlist. +local function check_current_and_playlist() + if not opts.enabled then return end if not scannable(current_path) then maybe_scan_playlist() -- even if current is a stream, still scan the list return end - -- On a cache hit (same mtime/size), show immediately local e = cached_fresh(current_path) if e then @@ -356,13 +414,39 @@ local function on_file_loaded() start_scan(current_path, maybe_scan_playlist) -- after the current check, scan the list end +local function on_file_loaded() + hide_badge() + cancel_bg() -- stop the previous playlist scan + scan_token = scan_token + 1 -- invalidate any in-flight current-file scan + current_scanning = false + current_path = mp.get_property("path") + if not opts.enabled then return end + if opts.scan_on_load then check_current_and_playlist() end +end + ---------------------------------------------------------------------- -- Key bindings / messages ---------------------------------------------------------------------- +-- Manually start a check: current file, then the rest of the playlist. +-- This is the trigger to use when scan_on_load = false. +local function scan_now() + if not opts.enabled then + mp.osd_message("Integrity check: off") + return + end + current_path = mp.get_property("path") + if not current_path then + mp.osd_message("Integrity: no file") + return + end + mp.osd_message("Integrity: scanning...") + check_current_and_playlist() +end + local function rescan() if current_path and scannable(current_path) then cache[current_path] = nil - start_scan(current_path) + start_scan(current_path, maybe_scan_playlist) mp.osd_message("Integrity: re-checking...") else mp.osd_message("Integrity: file cannot be checked") @@ -389,7 +473,8 @@ local function current_status_text() if e and mtime and e.mtime == mtime and e.size == size then return (e.status == "corrupt") and "Integrity: corrupt" or "Integrity: OK" end - return "Integrity: checking..." + if current_scanning then return "Integrity: checking..." end + return "Integrity: not checked" end -- Show the check status immediately (single file, or whole playlist) @@ -406,9 +491,11 @@ local function status() state, s.ok, s.corrupt, s.pending, s.total), 4) end +mp.add_key_binding(nil, "scan", scan_now) mp.add_key_binding(nil, "rescan", rescan) mp.add_key_binding(nil, "toggle", toggle) mp.add_key_binding(nil, "status", status) +mp.register_script_message("integrity-scan", scan_now) mp.register_script_message("integrity-rescan", rescan) mp.register_script_message("integrity-toggle", toggle) mp.register_script_message("integrity-status", status) -- cgit v1.2.3