summaryrefslogtreecommitdiff
path: root/ar/.config/mpv/scripts/integrity-check.lua
diff options
context:
space:
mode:
authorTheSiahxyz <164138827+TheSiahxyz@users.noreply.github.com>2026-06-20 13:08:37 +0900
committerTheSiahxyz <164138827+TheSiahxyz@users.noreply.github.com>2026-06-20 13:08:37 +0900
commit364c09d11fb1a8e319d8d08d9fd804f7b44317bf (patch)
tree4a18d500f83d75dc3c3468f2b466ce2136a2bec8 /ar/.config/mpv/scripts/integrity-check.lua
parent6a0348deb81c8295e7a64ee04fe1f1bfa898543d (diff)
modified scripts/integrity-check.lua, modified scripts/keep-window-size.lua, modified scripts/slicing.lua, modified scripts/thumbfast.lua
Diffstat (limited to 'ar/.config/mpv/scripts/integrity-check.lua')
-rw-r--r--ar/.config/mpv/scripts/integrity-check.lua842
1 files changed, 513 insertions, 329 deletions
diff --git a/ar/.config/mpv/scripts/integrity-check.lua b/ar/.config/mpv/scripts/integrity-check.lua
index 4af1325..0cb7bc6 100644
--- a/ar/.config/mpv/scripts/integrity-check.lua
+++ b/ar/.config/mpv/scripts/integrity-check.lua
@@ -22,145 +22,168 @@
-- problems, but not cases where the container is fine and only the picture is
-- slightly broken. For that, set deep_scan=yes (much slower).
-local mp = require 'mp'
-local utils = require 'mp.utils'
-local msg = require 'mp.msg'
+local mp = require("mp")
+local utils = require("mp.utils")
+local msg = require("mp.msg")
----------------------------------------------------------------------
-- Options (override via script-opts/integrity-check.conf)
----------------------------------------------------------------------
local opts = {
- enabled = true, -- feature on/off
- 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,
+ enabled = true, -- feature on/off
+ 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 = 10, -- 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")
+require("mp.options").read_options(opts, "integrity-check")
----------------------------------------------------------------------
-- Paths
----------------------------------------------------------------------
-local HOME = os.getenv("HOME") or ""
+local HOME = os.getenv("HOME") or ""
local CONFIG_DIR = HOME .. "/.config/mpv"
local CACHE_FILE = CONFIG_DIR .. "/integrity_cache.tsv"
-local LOG_FILE = CONFIG_DIR .. "/corrupted.log"
+local LOG_FILE = CONFIG_DIR .. "/corrupted.log"
----------------------------------------------------------------------
-- State
----------------------------------------------------------------------
-local cache = {} -- path -> {mtime=, size=, status=, errors=}
-local overlay = mp.create_osd_overlay("ass-events")
+local cache = {} -- path -> {mtime=, size=, status=, errors=}
+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
+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
+local scanning = {} -- path -> {prog=, dur=} for every in-flight check (current + playlist)
----------------------------------------------------------------------
-- Cache I/O
----------------------------------------------------------------------
local function load_cache()
- local f = io.open(CACHE_FILE, "r")
- if not f then return end
- for line in f:lines() do
- local mtime, size, status, errors, path =
- line:match("^(%d+)\t(%d+)\t(%S+)\t(%d+)\t(.+)$")
- if path then
- cache[path] = {
- mtime = tonumber(mtime),
- size = tonumber(size),
- status = status,
- errors = tonumber(errors),
- }
- end
- end
- f:close()
+ local f = io.open(CACHE_FILE, "r")
+ if not f then
+ return
+ end
+ for line in f:lines() do
+ local mtime, size, status, errors, path = line:match("^(%d+)\t(%d+)\t(%S+)\t(%d+)\t(.+)$")
+ if path then
+ cache[path] = {
+ mtime = tonumber(mtime),
+ size = tonumber(size),
+ status = status,
+ errors = tonumber(errors),
+ }
+ end
+ end
+ f:close()
end
-- Append a single entry. So that concurrent mpv instances writing the cache
-- don't overwrite each other's results. On load, the last line for a path wins.
local function append_cache(path, e)
- if not opts.use_cache then return end
- local f = io.open(CACHE_FILE, "a")
- if not f then return end
- f:write(string.format("%d\t%d\t%s\t%d\t%s\n",
- e.mtime or 0, e.size or 0, e.status, e.errors or 0, path))
- f:close()
+ if not opts.use_cache then
+ return
+ end
+ local f = io.open(CACHE_FILE, "a")
+ if not f then
+ return
+ end
+ f:write(string.format("%d\t%d\t%s\t%d\t%s\n", e.mtime or 0, e.size or 0, e.status, e.errors or 0, path))
+ f:close()
end
-- On startup, compact duplicate lines (keep only the latest, rewrite).
-- During a session only append is used.
local function compact_cache()
- if not opts.use_cache then return end
- local f = io.open(CACHE_FILE, "w")
- if not f then return end
- for path, e in pairs(cache) do
- f:write(string.format("%d\t%d\t%s\t%d\t%s\n",
- e.mtime or 0, e.size or 0, e.status, e.errors or 0, path))
- end
- f:close()
+ if not opts.use_cache then
+ return
+ end
+ local f = io.open(CACHE_FILE, "w")
+ if not f then
+ return
+ end
+ for path, e in pairs(cache) do
+ f:write(string.format("%d\t%d\t%s\t%d\t%s\n", e.mtime or 0, e.size or 0, e.status, e.errors or 0, path))
+ end
+ f:close()
end
local function log_corrupted(path)
- local f = io.open(LOG_FILE, "a")
- if not f then return end
- f:write(string.format("%s\t%s\n", os.date("%Y-%m-%d %H:%M:%S"), path))
- f:close()
+ local f = io.open(LOG_FILE, "a")
+ if not f then
+ return
+ end
+ f:write(string.format("%s\t%s\n", os.date("%Y-%m-%d %H:%M:%S"), path))
+ f:close()
end
----------------------------------------------------------------------
-- Badge (indicator) - shown only when corrupt
----------------------------------------------------------------------
local function hide_badge()
- overlay:remove()
+ overlay:remove()
end
local function show_corrupt()
- overlay.res_x = 1280
- overlay.res_y = 720
- overlay.data = string.format(
- "{\\an9\\pos(1268,10)\\fs%d\\bord2\\shad1\\1c&H0000E0&\\3c&H000000&}%s",
- opts.font_size, "■ Corrupt")
- overlay:update()
+ overlay.res_x = 1280
+ overlay.res_y = 720
+ overlay.data = string.format(
+ "{\\an9\\pos(1268,10)\\fs%d\\bord2\\shad1\\1c&H0000E0&\\3c&H000000&}%s",
+ opts.font_size,
+ "■ Corrupt"
+ )
+ overlay:update()
end
local function show_scanning()
- if not opts.show_scanning then hide_badge(); return end
- overlay.res_x = 1280
- overlay.res_y = 720
- overlay.data = string.format(
- "{\\an9\\pos(1268,10)\\fs%d\\bord2\\shad1\\1c&H00D7FF&\\3c&H000000&}%s",
- opts.font_size, "● Checking...")
- overlay:update()
+ if not opts.show_scanning then
+ hide_badge()
+ return
+ end
+ overlay.res_x = 1280
+ overlay.res_y = 720
+ overlay.data = string.format(
+ "{\\an9\\pos(1268,10)\\fs%d\\bord2\\shad1\\1c&H00D7FF&\\3c&H000000&}%s",
+ opts.font_size,
+ "● Checking..."
+ )
+ overlay:update()
end
----------------------------------------------------------------------
-- Utils
----------------------------------------------------------------------
local function scannable(path)
- if not path then return false end
- if path:find("^%a[%w%+%-%.]*://") then return false end -- exclude network/stream
- return true
+ if not path then
+ return false
+ end
+ if path:find("^%a[%w%+%-%.]*://") then
+ return false
+ end -- exclude network/stream
+ return true
end
local function file_sig(path)
- local info = utils.file_info(path)
- if not info or not info.is_file then return nil end
- return math.floor(info.mtime), info.size
+ local info = utils.file_info(path)
+ if not info or not info.is_file then
+ return nil
+ end
+ return math.floor(info.mtime), info.size
end
----------------------------------------------------------------------
@@ -168,93 +191,192 @@ end
----------------------------------------------------------------------
-- 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
- add("-map", "0", "-f", "null", "-")
- return args
+local function build_args(path, read_rate, progfile)
+ 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
+ add("-map", "0")
+ if progfile then
+ add("-progress", progfile)
+ end -- machine-readable progress
+ add("-f", "null", "-")
+ return args
end
local function apply_result(path, corrupt, from_cache)
- local mtime, size = file_sig(path)
- cache[path] = {
- mtime = mtime or 0,
- size = size or 0,
- status = corrupt and "corrupt" or "ok",
- errors = 0,
- }
- if corrupt then
- show_corrupt()
- if not from_cache then
- log_corrupted(path)
- msg.warn("Corrupt: " .. path)
- end
- else
- -- healthy files show nothing
- hide_badge()
- end
- if not from_cache then append_cache(path, cache[path]) end
+ local mtime, size = file_sig(path)
+ cache[path] = {
+ mtime = mtime or 0,
+ size = size or 0,
+ status = corrupt and "corrupt" or "ok",
+ errors = 0,
+ }
+ if corrupt then
+ show_corrupt()
+ if not from_cache then
+ log_corrupted(path)
+ msg.warn("Corrupt: " .. path)
+ end
+ else
+ -- healthy files show nothing
+ hide_badge()
+ end
+ if not from_cache then
+ append_cache(path, cache[path])
+ end
end
-- Convert the subprocess result into a corrupt/ok verdict
local function determine_corrupt(result)
- local stderr = result.stderr or ""
- local status = result.status or 0
- return (stderr:gsub("%s+", "") ~= "") or (status ~= 0)
+ local stderr = result.stderr or ""
+ local status = result.status or 0
+ return (stderr:gsub("%s+", "") ~= "") or (status ~= 0)
end
-- Return the cache entry if it matches the file's current mtime/size
local function cached_fresh(path)
- if not (opts.use_cache and cache[path]) then return nil end
- local mtime, size = file_sig(path)
- local e = cache[path]
- if mtime and e.mtime == mtime and e.size == size then
- return e
- end
- return nil
+ if not (opts.use_cache and cache[path]) then
+ return nil
+ end
+ local mtime, size = file_sig(path)
+ local e = cache[path]
+ if mtime and e.mtime == mtime and e.size == size then
+ return e
+ end
+ return nil
+end
+
+-- In-flight scan tracking (for the status display: filename + % progress)
+local function basename(p)
+ return (p:gsub("^.*/", ""))
+end
+
+-- Register a path as being scanned; returns the -progress temp file to use.
+local function begin_scanning(path, dur)
+ local prog = os.tmpname()
+ scanning[path] = { prog = prog, dur = dur }
+ return prog
+end
+
+local function clear_scanning(path)
+ local rec = scanning[path]
+ if rec and rec.prog then
+ os.remove(rec.prog)
+ end
+ scanning[path] = nil
+end
+
+-- Probe a file's duration in the background so progress can be shown as %.
+local function probe_duration(path)
+ mp.command_native_async({
+ name = "subprocess",
+ args = {
+ "ffprobe",
+ "-v",
+ "error",
+ "-show_entries",
+ "format=duration",
+ "-of",
+ "default=nokey=1:noprint_wrappers=1",
+ path,
+ },
+ playback_only = false,
+ capture_stdout = true,
+ }, function(success, result, err)
+ local rec = scanning[path]
+ if not rec or not result or not result.stdout then
+ return
+ end
+ local d = tonumber((result.stdout:gsub("%s+", "")))
+ if d and d > 0 then
+ rec.dur = d
+ end
+ end)
+end
+
+-- Latest progress (%) for an in-flight scan, read from its -progress file.
+local function scan_progress(rec)
+ if not (rec and rec.prog and rec.dur and rec.dur > 0) then
+ return nil
+ end
+ local f = io.open(rec.prog, "r")
+ if not f then
+ return nil
+ end
+ local data = f:read("*a") or ""
+ f:close()
+ local last
+ for v in data:gmatch("out_time_us=(%d+)") do
+ last = v
+ end
+ if not last then
+ return nil
+ end
+ local pct = math.floor(tonumber(last) / 1e6 / rec.dur * 100 + 0.5)
+ if pct < 0 then
+ pct = 0
+ elseif pct > 100 then
+ pct = 100
+ end
+ return pct
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",
- args = build_args(path),
- playback_only = false,
- capture_stdout = true,
- 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
- msg.error("ffmpeg failed to run - check PATH")
- hide_badge()
- return
- end
- apply_result(path, determine_corrupt(result), false)
- if on_done then on_done() end
- end)
+ scan_token = scan_token + 1
+ local token = scan_token
+ current_scanning = true
+ local dur = (path == current_path) and mp.get_property_number("duration") or nil
+ local prog = begin_scanning(path, dur)
+ if not dur then
+ probe_duration(path)
+ end
+ show_scanning()
+ mp.command_native_async({
+ name = "subprocess",
+ args = build_args(path, nil, prog),
+ playback_only = false,
+ capture_stdout = true,
+ capture_stderr = true,
+ }, function(success, result, err)
+ clear_scanning(path)
+ 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
+ msg.error("ffmpeg failed to run - check PATH")
+ hide_badge()
+ return
+ end
+ apply_result(path, determine_corrupt(result), false)
+ if on_done then
+ on_done()
+ end
+ end)
end
----------------------------------------------------------------------
@@ -262,136 +384,161 @@ end
----------------------------------------------------------------------
-- Invalidate an in-progress playlist scan (on file change / toggle)
local function cancel_bg()
- bg_token = bg_token + 1
+ bg_token = bg_token + 1
end
-- Convert a playlist entry's filename into a checkable path
local function resolve_playlist_path(filename)
- if not filename then return nil end
- if filename:find("^%a[%w%+%-%.]*://") then return filename end -- keep URLs as-is
- if filename:find("^/") then return filename end -- absolute path
- local wd = mp.get_property("working-directory")
- if wd then return utils.join_path(wd, filename) end
- return filename
+ if not filename then
+ return nil
+ end
+ if filename:find("^%a[%w%+%-%.]*://") then
+ return filename
+ end -- keep URLs as-is
+ if filename:find("^/") then
+ return filename
+ end -- absolute path
+ local wd = mp.get_property("working-directory")
+ if wd then
+ return utils.join_path(wd, filename)
+ end
+ return filename
end
-- Update cache/log only, no badge (for background entries)
local function record_result_quiet(path, corrupt)
- local mtime, size = file_sig(path)
- cache[path] = {
- mtime = mtime or 0,
- size = size or 0,
- status = corrupt and "corrupt" or "ok",
- errors = 0,
- }
- if corrupt then
- log_corrupted(path)
- msg.warn("Corrupt (playlist): " .. path)
- end
- append_cache(path, cache[path])
+ local mtime, size = file_sig(path)
+ cache[path] = {
+ mtime = mtime or 0,
+ size = size or 0,
+ status = corrupt and "corrupt" or "ok",
+ errors = 0,
+ }
+ if corrupt then
+ log_corrupted(path)
+ msg.warn("Corrupt (playlist): " .. path)
+ end
+ append_cache(path, cache[path])
end
-- Aggregate the playlist's check status from the cache
local function playlist_stats()
- local count = mp.get_property_number("playlist-count", 0)
- local s = { total = 0, ok = 0, corrupt = 0, pending = 0 }
- for i = 0, count - 1 do
- local path = resolve_playlist_path(
- mp.get_property("playlist/" .. i .. "/filename"))
- if scannable(path) then
- s.total = s.total + 1
- local e = cached_fresh(path)
- if not e then s.pending = s.pending + 1
- elseif e.status == "corrupt" then s.corrupt = s.corrupt + 1
- else s.ok = s.ok + 1 end
- end
- end
- return s
+ local count = mp.get_property_number("playlist-count", 0)
+ local s = { total = 0, ok = 0, corrupt = 0, pending = 0 }
+ for i = 0, count - 1 do
+ local path = resolve_playlist_path(mp.get_property("playlist/" .. i .. "/filename"))
+ if scannable(path) then
+ s.total = s.total + 1
+ local e = cached_fresh(path)
+ if not e then
+ s.pending = s.pending + 1
+ elseif e.status == "corrupt" then
+ s.corrupt = s.corrupt + 1
+ else
+ s.ok = s.ok + 1
+ end
+ end
+ end
+ return s
end
-- 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 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)
- and string.format("Playlist check done | %d corrupt (total %d)", s.corrupt, s.total)
- or string.format("Playlist check done | no issues (total %d)", s.total)
- mp.osd_message(txt, 4)
- msg.info(txt)
- end
-
- 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
- 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
-
- pump()
+ 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 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) and string.format("Playlist check done | %d corrupt (total %d)", s.corrupt, s.total)
+ or string.format("Playlist check done | no issues (total %d)", s.total)
+ mp.osd_message(txt, 4)
+ msg.info(txt)
+ end
+
+ local pump -- forward declaration
+
+ local function on_done(path, result)
+ active = active - 1
+ clear_scanning(path)
+ -- 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
+ if not path then -- nothing left to dispatch
+ if active == 0 then
+ finish()
+ end
+ return
+ end
+ active = active + 1
+ did_scan = true
+ local prog = begin_scanning(path, nil)
+ probe_duration(path)
+ mp.command_native_async({
+ name = "subprocess",
+ args = build_args(path, opts.bg_read_rate, prog),
+ playback_only = false,
+ capture_stdout = true,
+ capture_stderr = true,
+ }, function(success, result, err)
+ on_done(path, result)
+ end)
+ end
+ end
+
+ pump()
end
-- If a playlist exists, start the background scan from the entry after the current position
local function maybe_scan_playlist()
- if not (opts.enabled and opts.scan_playlist) then return end
- local count = mp.get_property_number("playlist-count", 0)
- if count <= 1 then return end -- no playlist
- local pos = mp.get_property_number("playlist-pos", 0)
- scan_playlist_from(pos + 1)
+ if not (opts.enabled and opts.scan_playlist) then
+ return
+ end
+ local count = mp.get_property_number("playlist-count", 0)
+ if count <= 1 then
+ return
+ end -- no playlist
+ local pos = mp.get_property_number("playlist-pos", 0)
+ scan_playlist_from(pos + 1)
end
----------------------------------------------------------------------
@@ -399,29 +546,35 @@ 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
- apply_result(current_path, e.status == "corrupt", true)
- maybe_scan_playlist()
- return
- end
- start_scan(current_path, maybe_scan_playlist) -- after the current check, scan the list
+ 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
+ apply_result(current_path, e.status == "corrupt", true)
+ maybe_scan_playlist()
+ return
+ end
+ 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
+ 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
----------------------------------------------------------------------
@@ -430,65 +583,96 @@ end
-- 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()
+ 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, maybe_scan_playlist)
- mp.osd_message("Integrity: re-checking...")
- else
- mp.osd_message("Integrity: file cannot be checked")
- end
+ if current_path and scannable(current_path) then
+ cache[current_path] = nil
+ start_scan(current_path, maybe_scan_playlist)
+ mp.osd_message("Integrity: re-checking...")
+ else
+ mp.osd_message("Integrity: file cannot be checked")
+ end
end
local function toggle()
- opts.enabled = not opts.enabled
- if not opts.enabled then
- cancel_bg()
- hide_badge()
- else
- on_file_loaded()
- end
- mp.osd_message("Integrity check: " .. (opts.enabled and "on" or "off"))
+ opts.enabled = not opts.enabled
+ if not opts.enabled then
+ cancel_bg()
+ hide_badge()
+ else
+ on_file_loaded()
+ end
+ mp.osd_message("Integrity check: " .. (opts.enabled and "on" or "off"))
end
-- Current file's status, independent of the use_cache option (checks freshness)
local function current_status_text()
- if not current_path then return "Integrity: no file" end
- if not scannable(current_path) then return "Integrity: not checkable (stream)" end
- local e = cache[current_path]
- local mtime, size = file_sig(current_path)
- if e and mtime and e.mtime == mtime and e.size == size then
- return (e.status == "corrupt") and "Integrity: corrupt" or "Integrity: OK"
- end
- if current_scanning then return "Integrity: checking..." end
- return "Integrity: not checked"
+ if not current_path then
+ return "Integrity: no file"
+ end
+ if not scannable(current_path) then
+ return "Integrity: not checkable (stream)"
+ end
+ local e = cache[current_path]
+ local mtime, size = file_sig(current_path)
+ if e and mtime and e.mtime == mtime and e.size == size then
+ return (e.status == "corrupt") and "Integrity: corrupt" or "Integrity: OK"
+ end
+ if current_scanning then
+ return "Integrity: checking..."
+ end
+ return "Integrity: not checked"
+end
+
+-- Names (+ % progress) of files currently being checked.
+local function scanning_list()
+ local out = {}
+ for p, rec in pairs(scanning) do
+ local pct = scan_progress(rec)
+ out[#out + 1] = pct and string.format("%s (%d%%)", basename(p), pct) or basename(p)
+ end
+ table.sort(out)
+ return out
end
-- Show the check status immediately (single file, or whole playlist)
local function status()
- local count = mp.get_property_number("playlist-count", 0)
- if count <= 1 then
- mp.osd_message(current_status_text(), 4)
- return
- end
- local s = playlist_stats()
- local state = (s.pending == 0) and "done" or ("scanning (" .. s.pending .. " left)")
- mp.osd_message(string.format(
- "Integrity %s | ok %d | corrupt %d | pending %d (total %d)",
- state, s.ok, s.corrupt, s.pending, s.total), 4)
+ local count = mp.get_property_number("playlist-count", 0)
+ local lines = {}
+ if count <= 1 then
+ lines[#lines + 1] = current_status_text()
+ else
+ local s = playlist_stats()
+ local state = (s.pending == 0) and "done" or ("scanning (" .. s.pending .. " left)")
+ lines[#lines + 1] = string.format(
+ "Integrity %s | ok %d | corrupt %d | pending %d (total %d)",
+ state,
+ s.ok,
+ s.corrupt,
+ s.pending,
+ s.total
+ )
+ end
+ local checking = scanning_list()
+ if #checking > 0 then
+ lines[#lines + 1] = "Checking:"
+ for _, name in ipairs(checking) do
+ lines[#lines + 1] = " " .. name -- one file per line for readability
+ end
+ end
+ mp.osd_message(table.concat(lines, "\n"), 5)
end
mp.add_key_binding(nil, "scan", scan_now)