added mpv and jellyfin-mpv-shim

This commit is contained in:
2025-09-19 15:32:17 +02:00
parent 8ff8982d82
commit 51f5f7453e
60 changed files with 6362 additions and 0 deletions

View File

@@ -0,0 +1,114 @@
{
"allow_transcode_to_h265": true,
"always_transcode": false,
"audio_output": "hdmi",
"auto_play": false,
"check_updates": true,
"client_uuid": "a39904c0-f818-41d4-ab53-d72cb01bee16",
"connect_retry_mins": 0,
"direct_paths": false,
"discord_presence": false,
"display_mirroring": false,
"enable_gui": true,
"enable_osc": true,
"force_audio_codec": null,
"force_set_played": false,
"force_video_codec": "['av1', 'h265', 'h264']",
"fullscreen": true,
"health_check_interval": 300,
"idle_cmd": null,
"idle_cmd_delay": 60,
"idle_ended_cmd": null,
"idle_when_paused": false,
"ignore_ssl_cert": false,
"kb_debug": "~",
"kb_fullscreen": "f",
"kb_kill_shader": "k",
"kb_menu": "c",
"kb_menu_down": "down",
"kb_menu_esc": "esc",
"kb_menu_left": "left",
"kb_menu_ok": "enter",
"kb_menu_right": "right",
"kb_menu_up": "up",
"kb_next": ">",
"kb_pause": "space",
"kb_prev": "<",
"kb_stop": "q",
"kb_unwatched": "u",
"kb_watched": "w",
"lang": null,
"lang_filter": "und,eng,jpn,mis,mul,zxx",
"lang_filter_audio": false,
"lang_filter_sub": false,
"local_kbps": 2147483,
"log_decisions": false,
"media_ended_cmd": null,
"media_key_seek": false,
"media_keys": true,
"menu_mouse": true,
"mpv_ext": false,
"mpv_ext_ipc": null,
"mpv_ext_no_ovr": false,
"mpv_ext_path": null,
"mpv_ext_start": true,
"mpv_log_level": "info",
"notify_updates": true,
"play_cmd": null,
"playback_timeout": 30,
"player_name": "cn-arch",
"pre_media_cmd": null,
"prefer_transcode_to_h265": true,
"raise_mpv": true,
"remote_direct_paths": false,
"remote_kbps": 25000,
"sanitize_output": true,
"screenshot_dir": null,
"screenshot_menu": true,
"seek_down": -60,
"seek_h_exact": false,
"seek_left": -5,
"seek_right": 5,
"seek_up": 60,
"seek_v_exact": false,
"shader_pack_custom": false,
"shader_pack_enable": true,
"shader_pack_profile": null,
"shader_pack_remember": true,
"shader_pack_subtype": "lq",
"skip_credits_always": false,
"skip_credits_enable": true,
"skip_intro_always": true,
"skip_intro_enable": true,
"stop_cmd": null,
"stop_idle": false,
"subtitle_color": "#FFFFFFFF",
"subtitle_position": "bottom",
"subtitle_size": 100,
"svp_enable": false,
"svp_socket": null,
"svp_url": "http://127.0.0.1:9901/",
"sync_attempts": 5,
"sync_max_delay_skip": 300,
"sync_max_delay_speed": 50,
"sync_method_thresh": 2000,
"sync_osd_message": true,
"sync_revert_seek": true,
"sync_speed_attempts": 3,
"sync_speed_time": 1000,
"thumbnail_enable": true,
"thumbnail_osc_builtin": true,
"thumbnail_preferred_size": 320,
"tls_client_cert": null,
"tls_client_key": null,
"tls_server_ca": null,
"transcode_4k": false,
"transcode_av1": false,
"transcode_dolby_vision": true,
"transcode_hdr": true,
"transcode_hevc": false,
"transcode_hi10p": false,
"transcode_warning": true,
"use_web_seek": false,
"write_logs": false
}

View File

@@ -0,0 +1 @@
[{"address": "https://j.someho.me", "Name": "IdleJelly", "Id": "cef99817a4124734937e2fb674ce2065", "Version": "10.10.7", "DateLastAccessed": "2025-05-10T01:31:14Z", "UserId": "0eb92b7cdbeb489b9ea54be1757579b8", "AccessToken": "1ebec612c3a64a61889fc4926a057cfd", "Users": [{"Id": "0eb92b7cdbeb489b9ea54be1757579b8", "IsSignedInOffline": true}], "uuid": "72aeed57-a0eb-4cf3-bf1f-3601b70036ce", "username": "vikingowl", "connected": true}]

View File

@@ -0,0 +1 @@
/usr/share/mpv/fonts/uosc_icons.otf

View File

@@ -0,0 +1 @@
/usr/share/mpv/fonts/uosc_textures.ttf

View File

@@ -0,0 +1,4 @@
# Press 'n' to toggle the default audio filter on/off
n af toggle "dynaudnorm=f=150:g=15:p=0.9"
# Press 'Shift+N' to toggle the default compressor/normalizer chain on/off
N af toggle "lavfi=[acompressor=threshold=-18dB:ratio=5:attack=20:release=250,loudnorm=I=-16:LRA=11:TP=-1.5]"

View File

@@ -0,0 +1,50 @@
### Allgemeine Video-Einstellungen
volume=80
profile=gpu-hq
fs
video-sync=display-resample
scale=ewa_lanczossharp
cscale=ewa_lanczossharp
# Vollständiges Deaktivieren aller mpv Standard-UI
osc=no
osd-level=0
osd-bar=no
border=no
# Keine OSD-Nachrichten
osd-playing-msg=
osd-status-msg=
osd-msg1=
osd-msg2=
osd-msg3=
# Terminal-Output minimieren
quiet
msg-level=all=no
# uosc Konfiguration
script-opts=uosc-scroll-text=no
### Rest deiner Konfiguration...
hwdec=auto-copy
gpu-context=wayland
gpu-api=auto
# Streaming & Netzwerk...
stream-lavf-o-append=reconnect_on_http_error=4xx,5xx
stream-lavf-o-append=reconnect_on_network_error=yes
stream-lavf-o-append=reconnect_streamed=yes
stream-lavf-o-append=reconnect_delay_max=30
# Farbprofile...
#[HDR]
#profile-cond=get("video-params/primaries") == "bt.2020" and get("video-params/gamma") == "pq"
#profile-restore=copy
#target-colorspace-hint=yes
#[SDR]
#profile-cond=get("video-params/primaries") == "bt.709"
#profile-restore=copy
#target-trc=srgb

View File

@@ -0,0 +1 @@
/usr/share/mpv/script-opts

View File

@@ -0,0 +1 @@
/usr/share/mpv/scripts/README.md

View File

@@ -0,0 +1 @@
/usr/share/mpv/scripts/acompressor.lua

View File

@@ -0,0 +1 @@
/usr/share/mpv/scripts/ao-null-reload.lua

View File

@@ -0,0 +1 @@
/usr/share/mpv/scripts/autocrop.lua

View File

@@ -0,0 +1 @@
/usr/share/mpv/scripts/autodeint.lua

View File

@@ -0,0 +1 @@
/usr/share/mpv/scripts/autoload.lua

View File

@@ -0,0 +1 @@
/usr/share/mpv/scripts/cycle-deinterlace-pullup.lua

View File

@@ -0,0 +1,837 @@
--[[
This script uses the lavfi cropdetect filter to automatically insert a crop filter with appropriate parameters
for the currently playing video, the script run continuously by default (mode 4).
To use this script, "hwdec=no" (mpv default/recommended) or any "-copy" variant like "hwdec=auto-copy" is required,
consider editing "mpv.conf" to an appropriate value.
The workflow is as follows: We observe ffmpeg log to collect metadata and process it.
Collected metadata are stored sequentially in s.buffer, then process to check and
store trusted values to speed up future change for the current video.
It will automatically crop the video as soon as a change is validated.
The default options can be overridden by adding a line into "mpv.conf" with:
script-opts-append=<script_name>-<parameter>=<value>
script-opts-append=dynamic_crop-mode=0
script-opts-append=dynamic_crop-ratios=2.4 2.39 2 4/3 (quotes aren't needed like below)
Extended descriptions for some parameters (For default values, see <options>):
mode: [0-4] 0 disable, 1 on-demand, 2 one-shot, 3 dynamic-manual, 4 dynamic-auto
Mode 1 and 3 requires using the shortcut to start, 2 and 4 have an automatic start.
Shortcut "C" (shift+c) to control the script.
Cycle between ENABLE / DISABLE_WITH_CROP / DISABLE
prevent_change_mode: [0-3] 0 disable 1 keep-largest, 2 keep-lowest, 3 keep-latest
The prevent_change_timer is trigger after a change.
fix_windowed_behavior: [0-3] Avoid the default behavior that resizes the window to the source size
when the crop filter changes in windowed/maximized mode by adjusting geometry.
limit_timer: Only used if the cropdetect filter doesn't handle limit changes with a command (patch 01/2023).
Extend the time between each limit change to reduce the impact on performance caused by re-initializing the
full filter.
read_ahead_mode: Linked to the associated timer and tells how much time in advance to collect the metadata.
This feature is useful for videos with multiple aspect ratio changes for "fast_change_timer".
Note: because this function is in sync with the playback, a delay equivalent to the timer used is
added/reset every time you seek before you get a reaction, so setting 1 is recommanded.
Required at least https://github.com/FFmpeg/FFmpeg/commit/69c060bea21d3b4ce63b5fff40d37e98c70ab88f
and optionally https://github.com/mpv-player/mpv/pull/11182, until mpv patch is being merged to master,
considered this feature experimental because of the errors generated in logs/console by vf-command and
the filter used to sync the filter chain (psnr).
read_ahead_sync: Compensates for the delay when applying the crop filter and the visible result.
Must be adjusted to your tastes and each MPV client depending on their reaction time.
Note: Perfect adjustment is not really possible but generally <= 1 frame, sometimes more in
dark/ambiguous scenes.
segmentation: e.g. 0.5 for 50% - Extra time to allow new metadata to be segmented instead of being continuous.
This is used with ratio_timer, offset_timer and fallback_timer.
e.g. ratio_timer is validated with 5 sec accumulated over 7.5 sec elapsed.
]] --
require "mp.options"
-- options
local options = {
-- behavior
mode = 4, -- [0-4] more details above
start_delay = 0, -- delay in seconds used to skip intro (usefull with mode 2)
prevent_change_timer = 30, -- seconds
prevent_change_mode = 0, -- [0-3], more details above
fix_windowed_behavior = 1, -- [0-3], 0 no-fix, 1 fix-no-resize, 2 fix-keep-width, 3 fix-keep-height
limit_timer = 0.5, -- seconds, 0 disable, more details above
fast_change_timer = 0.2, -- seconds, recommanded to keep default or > 0 if read_ahead is supported by mpv
ratio_timer = 2, -- seconds, meta in ratios list
offset_timer = 20, -- seconds, >= 'ratio_timer', new offset for asymmetric video
fallback_timer = 40, -- seconds, >= 'offset_timer', not in ratios list and possibly with new offset
linked_tolerance = 2, -- int, scale with detect_round to match against source width/height
ratios = "2.76 2.55 24/9 2.4 2.39 2.35 2.2 2.1 2 1.9 1.85 16/9 5/3 1.5 1.43 4/3 1.25 9/16", -- list
ratio_tolerance = 2, -- int (even number), adjust in order to match more easly the ratios list
read_ahead_mode = 0, -- [0-2], 0 disable, 1 fast_change_timer, 2 ratio_timer, more details above
read_ahead_sync = 0, -- int/frame, increase for advance, more details above
segmentation = 0.5, -- [0.0-1] %, 0 will approved only a continuous metadata (strict)
crop_method = 1, -- 0 lavfi-crop (ffmpeg/filter), 1 video-crop (mpv/VO)
-- filter, see https://ffmpeg.org/ffmpeg-filters.html#cropdetect for details
detect_limit = 26, -- is the maximum use, increase it slowly if lighter black are present
detect_round = 2, -- even number
-- verbose
debug = false
}
read_options(options)
if options.mode == 0 then
mp.msg.info("mode = 0, disable script.")
return
end
-- forward declarations
local cleanup, on_toggle
local s = {}
-- labels
local label_prefix = mp.get_script_name()
local labels = {
crop = string.format("%s-crop", label_prefix), cropdetect = string.format("%s-cropdetect", label_prefix)
}
-- shifting decimal to
local LEFT, RIGHT = true, false
local function shifting_to(left, value)
local shift = 1e3
return left and (value / shift) or value >= 1 and math.ceil(value * shift) or value * shift
end
-- options: compute timer and other stuff
for k, v in pairs(options) do
local timer = string.match(tostring(k), "_timer")
if timer then options[k] = shifting_to(RIGHT, v) end
end
options.read_ahead_timer =
options.read_ahead_mode == 1 and options.fast_change_timer or options.read_ahead_mode == 2 and options.ratio_timer *
(1 + options.segmentation) or nil
options.read_ahead_cropdetect = options.read_ahead_timer and shifting_to(LEFT, options.read_ahead_timer) or nil
options.reverse_segmentation = 1 / (1 * (1 + options.segmentation))
options.crop_method_sync = options.crop_method == 0 and 1 or 0 -- lavfi-crop is slower, so give it some advance for read_ahead
local function print_debug(msg_type, meta, label)
if not options.debug then
return
elseif msg_type == "pre_format" then
mp.msg.info(meta)
elseif msg_type == "metadata" then
mp.msg.info(string.format("%s, %-29s | offX:%3s offY:%3s | limit:%-2s", label, meta.whxy, meta.offset.x,
meta.offset.y, s.limit.current))
elseif msg_type == "buffer" and s.stats.buffer then
mp.msg.info("Buffer stats:")
for whxy, ref in pairs(s.stats.buffer) do
mp.msg.info(string.format(
"\\ %-29s | offX=%4s offY=%4s | time=%6ss linked_source=%-4s known_ratio=%-4s trusted_offsets=%s", whxy,
ref.offset.x, ref.offset.y, shifting_to(LEFT, ref.time.buffer), ref.is_linked_to_source or false,
ref.is_known_ratio or false, ref.is_trusted_offsets))
end
mp.msg.info("Buffer list:")
for i, v in ipairs(s.buffer.indexed_list) do
local new_ref = v.new_ref and v.new_ref.whxy or ""
local pts = shifting_to(RIGHT, v.pts)
mp.msg.info(string.format("\\ %3s %-29s %4sms pts:%d new_ref:%s", i, v.ref.whxy, v.t_elapsed, pts, new_ref))
end
mp.msg.info("i_fallback", s.candidate.i_fallback)
mp.msg.info("i_offset", s.candidate.i_offset)
elseif msg_type == "applied" and s.stats.indexed_applied then
mp.msg.info("Applied list:")
for i, v in ipairs(s.stats.indexed_applied) do
mp.msg.info(string.format("\\ %3s %-29s pts:%d", i, v.ref.whxy, shifting_to(RIGHT, v.pts)))
end
end
end
local function print_stats()
if not s.stats and not s.stats.trusted then return end
mp.msg.info("Meta Stats:")
local offsets_list = {x = "", y = ""}
for axis, _ in pairs(offsets_list) do
for _, v in pairs(s.stats.trusted_offset[axis]) do offsets_list[axis] = offsets_list[axis] .. v .. " " end
end
mp.msg.info(
string.format("Limit - min/max: %s/%s | counter: %s", s.limit.min, options.detect_limit, s.limit.counter))
mp.msg.info(string.format("Trusted - unique: %s | offset: X:%sY:%s", s.stats.trusted_unique, offsets_list.x,
offsets_list.y))
for whxy, ref in pairs(s.stats.trusted) do
if s.stats.trusted[whxy] then
mp.msg.info(string.format("\\ %-29s | offX=%3s offY=%3s | applied=%s overall=%ss accumulated=%ss", whxy,
ref.offset.x, ref.offset.y, ref.applied, shifting_to(LEFT, ref.time.overall),
shifting_to(LEFT, ref.time.accumulated)))
end
end
mp.msg.info("Buffer - unique: " .. s.stats.buffer_unique .. " | total: " .. s.buffer.i_total,
shifting_to(LEFT, s.buffer.t_total) .. "s | known_ratio:", s.buffer.i_ratio,
shifting_to(LEFT, s.buffer.t_ratio) .. "s")
end
local function is_trusted_offset(offset, axis)
local trusted_offset = s.stats.trusted_offset[axis]
for _, v in ipairs(trusted_offset) do if math.abs(offset - v) <= 1 then return true end end
return false
end
local function is_cropable()
for _, track in pairs(mp.get_property_native('track-list')) do
if track.type == 'video' and track.selected then return not track.albumart end
end
return false
end
local function filter_state(label, key, value)
local filters = mp.get_property_native("vf")
for _, filter in pairs(filters) do
if filter["label"] == label and
((not key or key ~= "graph" and filter[key] == value or key == "graph" and
string.find(filter.params.graph, value))) then return true end
end
return false
end
local function command_filter(label, command, argument, target)
if not s.f_vfcommand then
local res, reason = mp.commandv("vf-command", label, command, argument, target)
if not res and reason == "invalid parameter" then
s.f_vfcommand = true -- if mpv doesn't handle target parameter
end
end
if s.f_vfcommand then
-- fallback and send to all filters inside the graph
mp.commandv("vf-command", label, command, argument)
end
end
local function insert_cropdetect_filter(limit, change)
if s.toggled > 1 or s.paused then return end
local function insert_filter()
local cropdetect = string.format("cropdetect@dyn_cd=limit=%d/255:round=%d:reset=1", limit, options.detect_round)
if s.f_limit_runtime and change then
command_filter(labels.cropdetect, "limit", string.format("%d/255", limit), "cropdetect")
return true
elseif s.f_limit_runtime and options.read_ahead_mode > 0 then
return mp.commandv("vf", "pre",
string.format("@%s:lavfi=[split[a][b];[b]setpts=PTS-%s/TB,%s[b];%s]", labels.cropdetect,
options.read_ahead_cropdetect, cropdetect, s.f_sync))
else
return mp.commandv("vf", "pre", string.format("@%s:lavfi=[split[a][b];[b]%s,nullsink;[a]null]",
labels.cropdetect, cropdetect))
end
end
if not insert_filter() then
mp.msg.error("Does vf=help as #1 line in mvp.conf return libavfilter list with crop/cropdetect in log?")
s.f_missing = true
cleanup()
return
end
if not s.f_limit_runtime then
s.f_inserted = true -- skip process and wait for new s.collected
end
s.f_limit_change = change -- filter is updated for limit change
end
local function apply_crop(ref, pts)
-- osd size change
-- TODO add auto/smart mode
local prop_fullscreen = mp.get_property("fullscreen")
if prop_fullscreen ~= "yes" and options.fix_windowed_behavior ~= 0 then
local prop_maximized = mp.get_property("window-maximized")
local osd = mp.get_property_native("osd-dimensions")
local prop_auto_window_resize = mp.get_property("auto-window-resize")
if prop_auto_window_resize == "yes" and options.fix_windowed_behavior == 1 then
-- disable auto resize to avoid resizing at the original size of the video
mp.set_property("auto-window-resize", "no")
end
if prop_maximized ~= "yes" then
if options.fix_windowed_behavior == 2 then
mp.set_property("geometry", string.format("%s", osd.w))
elseif options.fix_windowed_behavior == 3 then
mp.set_property("geometry", string.format("x%s", osd.h))
end
end
end
-- crop filter insertion/update
if s.f_video_crop then
mp.set_property("video-crop", string.format("%sx%s+%s+%s", ref.w, ref.h, ref.x, ref.y))
elseif filter_state(labels.crop) and not s.seeking then
for _, axis in ipairs({"w", "x", "h", "y"}) do -- "w""x" then "h""y" to reduce visual glitch
if s.applied[axis] ~= ref[axis] then command_filter(labels.crop, axis, ref[axis], "crop") end
end
else
mp.commandv("vf", "append", string.format("@%s:lavfi-crop=%s", labels.crop, ref.whxy))
end
ref.applied = ref.applied + 1
s.applied = ref
print_debug("pre_format", string.format("- Apply: %s", ref.whxy))
if options.debug and pts then table.insert(s.stats.indexed_applied, {ref = ref, pts = pts}) end
end
local function compute_metadata(meta)
meta.whxy = string.format("w=%s:h=%s:x=%s:y=%s", meta.w, meta.h, meta.x, meta.y)
meta.offset = {x = meta.x - (s.source.w - meta.w) / 2, y = meta.y - (s.source.h - meta.h) / 2}
meta.mt = meta.y
meta.mb = s.source.h - meta.h - meta.y
meta.ml = meta.x
meta.mr = s.source.w - meta.w - meta.x
meta.is_source = meta.whxy == s.source.whxy
meta.is_invalid = meta.h < 0 or meta.w < 0
meta.is_trusted_offsets = is_trusted_offset(meta.offset.x, "x") and is_trusted_offset(meta.offset.y, "y")
meta.time = {buffer = 0, overall = 0}
if options.read_ahead_mode > 0 then meta.pts = {} end
local margin = options.detect_round * options.linked_tolerance
meta.is_linked_to_source = meta.mt <= margin and meta.mb <= margin or meta.ml <= margin and meta.mr <= margin
if meta.is_linked_to_source and not meta.is_invalid and s.ratios.w[meta.w] or s.ratios.h[meta.h] then
meta.is_known_ratio = true
end
return meta
end
local function generate_ratios(list)
for ratio in string.gmatch(list, "%S+%s?") do
for a, b in string.gmatch(tostring(ratio), "(%d+)/(%d+)") do ratio = a / b end
local w, h = math.floor((s.source.h * ratio)), math.floor((s.source.w / ratio))
local margin = options.ratio_tolerance
for k, v in pairs({w = w, h = h}) do
if v < s.source[k] - options.linked_tolerance then
if v % 2 == 1 then
s.ratios[k][v + 1], s.ratios[k][v - 1] = true, true
if margin > 0 then
s.ratios[k][v + 1 + margin], s.ratios[k][v - 1 - margin] = true, true
end
else
s.ratios[k][v] = true
if margin > 0 then s.ratios[k][v + margin], s.ratios[k][v - margin] = true, true end
end
end
end
end
end
local function switch_hwdec(id, hwdec, error)
if hwdec ~= "no" and not string.match(hwdec, "-copy") then
local msg = "Switch to SW decoding or HW -copy variant."
mp.msg.info(msg)
mp.osd_message(string.format("%s: %s", label_prefix, msg), 5)
end
if s.hwdec and hwdec ~= s.hwdec and s.hwdec ~= "no" and not string.match(s.hwdec, "-copy") and
filter_state(labels.cropdetect) then mp.commandv("vf", "remove", string.format("@%s", labels.cropdetect)) end
s.hwdec = hwdec
end
local function process_metadata(collected, timestamp, elapsed_time)
s.in_progress = true -- prevent event race
print_debug("metadata", collected, "Collected")
local function cleanup_stat(whxy, ref, ref_i, index)
if ref[whxy] then
ref[whxy] = nil
ref_i[index] = ref_i[index] - 1
end
end
-- buffer: init
if not s.stats.buffer[collected.whxy] then
s.stats.buffer[collected.whxy] = collected
s.stats.buffer_unique = s.stats.buffer_unique + 1
end
-- buffer: add collected or increase it's timer
if s.buffer.i_total == 0 or s.buffer.indexed_list[s.buffer.i_total].ref ~= collected then
s.buffer.i_total = s.buffer.i_total + 1
s.buffer.i_ratio = s.buffer.i_ratio + 1
s.buffer.indexed_list[s.buffer.i_total] = {ref = collected, pts = timestamp, t_elapsed = elapsed_time}
if options.read_ahead_mode > 0 then table.insert(collected.pts, timestamp) end
elseif s.last_collected == collected then
s.buffer.indexed_list[s.buffer.i_total].t_elapsed = s.buffer.indexed_list[s.buffer.i_total].t_elapsed +
elapsed_time
end
collected.time.overall = collected.time.overall + elapsed_time
collected.time.buffer = collected.time.buffer + elapsed_time
s.buffer.t_total = s.buffer.t_total + elapsed_time
if s.buffer.i_ratio > 0 then s.buffer.t_ratio = s.buffer.t_ratio + elapsed_time end
-- candidate offset/fallback to later extend buffer size
if not s.stats.trusted[collected.whxy] and collected.time.buffer > options.ratio_timer and
collected.is_linked_to_source then
if not s.candidate.offset[collected.whxy] and not collected.is_trusted_offsets and collected.is_known_ratio then
s.candidate.offset[collected.whxy] = collected
s.candidate.i_offset = s.candidate.i_offset + 1
elseif not collected.is_known_ratio and not s.candidate.offset[collected.whxy] and
not s.candidate.fallback[collected.whxy] then
s.candidate.fallback[collected.whxy] = collected
s.candidate.i_fallback = s.candidate.i_fallback + 1
end
end
-- add new fallback ratio to the ratio list
if s.candidate.fallback[collected.whxy] and collected.time.buffer >= options.fallback_timer then
-- TODO eventually re-check the buffer list with new ratio
generate_ratios(collected.w .. "/" .. collected.h)
collected.is_known_ratio = true
cleanup_stat(collected.whxy, s.candidate.fallback, s.candidate, "i_fallback")
end
-- add new offset to the trusted_offsets list
if s.candidate.offset[collected.whxy] and collected.is_known_ratio and collected.is_linked_to_source and
collected.time.buffer >= options.offset_timer then
for _, axis in ipairs({"x", "y"}) do
if not is_trusted_offset(collected.offset[axis], axis) then
table.insert(s.stats.trusted_offset[axis], collected.offset[axis])
end
end
cleanup_stat(collected.whxy, s.candidate.offset, s.candidate, "i_offset")
collected.is_trusted_offsets = true
end
-- add collected ready to the trusted list
local new_ready =
not s.stats.trusted[collected.whxy] and collected.is_trusted_offsets and not collected.is_invalid and
collected.is_linked_to_source and collected.is_known_ratio and collected.time.buffer >= options.ratio_timer
if new_ready then
s.stats.trusted[collected.whxy] = collected
s.stats.trusted_unique = s.stats.trusted_unique + 1
collected.applied = 0
collected.time.accumulated = collected.time.buffer
end
-- use current as main metadata, override by corrected or stabilized if needed
local current = collected
-- correction with trusted metadata for fast change in dark/ambiguous scene
local corrected = {}
if not current.is_invalid and s.stats.trusted_unique > 1 and not s.stats.trusted[current.whxy] then
-- is_bigger than applied meta
corrected.is_bigger = current.mt < s.approved.mt or current.mb < s.approved.mb or current.ml < s.approved.ml or
current.mr < s.approved.mr
-- find closest trusted metadata
local closest = {}
local margin = options.detect_round * options.linked_tolerance
for _, ref in pairs(s.stats.trusted) do
local diff = {ref = ref, vs_current = 0, vs_applied = 0, total = 0}
for _, side in ipairs({"mt", "mb", "ml", "mr"}) do
diff[side] = current[side] - ref[side]
diff.total = diff.total + math.abs(diff[side])
if diff[side] > margin or diff[side] < -margin then diff.vs_current = diff.vs_current + 1 end
if ref[side] ~= s.approved[side] then diff.vs_applied = diff.vs_applied + 1 end
end
-- is_inside this trusted meta with tiny tolerance for being outside
diff.is_inside = not (diff.mt < -margin or diff.mb < -margin or diff.ml < -margin or diff.mr < -margin)
local pattern = diff.is_inside and
(diff.vs_current <= 1 or diff.vs_current == 2 and diff.vs_applied <= 2 or
diff.vs_current > 2 and corrected.is_bigger)
local set = closest.ref and
(diff.vs_current < closest.vs_current or diff.vs_current == closest.vs_current and
diff.vs_applied < closest.vs_applied or diff.vs_current == closest.vs_current and
diff.vs_applied == closest.vs_applied and diff.total < closest.total)
-- mp.msg.info(string.format("\\ %-5s %-29s curr:%s appl:%s | %-3s %-3s %-3s %-3s %-4s | is_in:%s ",
-- pattern and (not closest.ref or set), ref.whxy, diff.vs_current, diff.vs_applied, diff.mt, diff.mb,
-- diff.ml, diff.mr, diff.total, diff.is_inside))
if pattern and (not closest.ref or set) then closest = diff end
end
-- replace current with corrected
if closest.ref then
current = closest.ref
corrected.ref = closest.ref
s.buffer.indexed_list[s.buffer.i_total].new_ref = current
print_debug("metadata", current, "\\ Corrected")
else
print_debug("pre_format", "\\ Uncorrected")
end
end
-- stabilization of odd/unstable meta
local stabilized
if options.detect_round <= 4 and s.stats.trusted[current.whxy] then
local margin = options.detect_round * 4
local applied_in_margin = math.abs(current.w - s.approved.w) <= margin and math.abs(current.h - s.approved.h) <=
margin
for _, ref in pairs(s.stats.trusted) do
local in_margin = math.abs(current.w - ref.w) <= margin and math.abs(current.h - ref.h) <= margin
if in_margin then
local gt_applied = applied_in_margin and ref ~= s.approved and ref.time.overall >
s.approved.time.overall * 2
local applied_gt = applied_in_margin and ref == s.approved and ref.time.overall * 2 >
current.time.overall
local pattern = not applied_in_margin and ref.time.overall > current.time.overall or gt_applied or
applied_gt
local set = stabilized and ref.time.overall > stabilized.time.overall
-- mp.msg.info("\\", ref.whxy, ref.time.overall, current.time.overall, s.approved.time.overall)
if ref ~= current and pattern and (not stabilized or set) then stabilized = ref end
end
end
if stabilized then
current = stabilized
s.buffer.indexed_list[s.buffer.i_total].new_ref = current
print_debug("metadata", current, "\\ Stabilized")
end
end
-- cycle time.accumulated for fast_change_timer (reset if uncorrected)
for whxy, ref in pairs(s.stats.trusted) do
ref.time.accumulated = whxy ~= current.whxy and 0 or ref.time.accumulated < 0 and 0 + elapsed_time or
not new_ready and ref.time.accumulated + elapsed_time or ref.time.accumulated
end
-- crop: final validation then store or apply it
local detect_source = current == s.last_current and (current.is_source or collected.is_source) and s.limit.target >=
0
local confirmation = not current.is_source and s.stats.trusted[current.whxy] and current.time.accumulated >=
options.fast_change_timer and (not corrected.ref or current == s.last_current)
local crop_filter = s.approved ~= current and (confirmation or detect_source)
if crop_filter and (not s.timestamps.prevent or timestamp >= s.timestamps.prevent) then
s.approved = current -- reflect s.applied for read_head
if s.limit.current < s.limit.min then
s.limit.min = s.limit.current -- store minimum limit
end
if s.f_limit_runtime and options.read_ahead_mode > 0 then
local pts = current.time.accumulated < options.ratio_timer and timestamp - current.time.accumulated or
current.pts[1]
table.insert(s.indexed_read_ahead, {ref = current, pts = pts})
s.timestamps.read_ahead = nil
else
apply_crop(current, timestamp)
end
if options.prevent_change_mode > 0 then
s.timestamps.prevent = nil
if (options.prevent_change_mode == 1 and (current.w > s.approved.w or current.h > s.approved.h) or
options.prevent_change_mode == 2 and (current.w < s.approved.w or current.h < s.approved.h) or
options.prevent_change_mode == 3) then
s.timestamps.prevent = timestamp + options.prevent_change_timer
end
end
if options.mode <= 2 then on_toggle(true) end
end
local function is_time_to_cleanup_buffer(time, target_time)
return time > target_time * (1 + options.segmentation)
end
-- buffer: reduce size of known ratio stats
while is_time_to_cleanup_buffer(s.buffer.t_ratio, options.ratio_timer) do
local i = (s.buffer.i_total + 1) - s.buffer.i_ratio
s.buffer.t_ratio = s.buffer.t_ratio - s.buffer.indexed_list[i].t_elapsed
s.buffer.i_ratio = s.buffer.i_ratio - 1
end
-- buffer: check for candidate to extend it
local buffer_timer = s.candidate.i_offset > 0 and options.offset_timer or s.candidate.i_fallback > 0 and
options.fallback_timer or options.ratio_timer
-- buffer: cleanup fake candidate
local function is_proactive_cleanup_needed()
local test
if is_time_to_cleanup_buffer(s.buffer.t_total, options.ratio_timer) then
for _, cat in ipairs({"offset", "fallback"}) do
if s.candidate["i_" .. cat] > 0 then
test = true
for whxy, ref in pairs(s.candidate[cat]) do
if ref.time.buffer > s.buffer.t_total * options.reverse_segmentation then
return false -- if at least one is a proper candidate
end
end
end
end
end
return test
end
-- buffer: reduce total size
while is_time_to_cleanup_buffer(s.buffer.t_total, buffer_timer) or is_proactive_cleanup_needed() do
s.buffer.i_to_shift = s.buffer.i_to_shift + 1
local entry = s.buffer.indexed_list[s.buffer.i_to_shift]
entry.ref.time.buffer = entry.ref.time.buffer - entry.t_elapsed
if options.read_ahead_mode > 0 then table.remove(entry.ref.pts, 1) end
if s.stats.buffer[entry.ref.whxy] and entry.ref.time.buffer == 0 then
cleanup_stat(entry.ref.whxy, s.stats.buffer, s.stats, "buffer_unique")
cleanup_stat(entry.ref.whxy, s.candidate.offset, s.candidate, "i_offset")
cleanup_stat(entry.ref.whxy, s.candidate.fallback, s.candidate, "i_fallback")
end
s.buffer.t_total = s.buffer.t_total - entry.t_elapsed
end
-- buffer: shift the list to overwrite unused data
if s.buffer.i_to_shift >= 20 or s.buffer.i_to_shift == s.buffer.i_total then
for i = s.buffer.i_to_shift + 1, s.buffer.i_total do
s.buffer.indexed_list[i - s.buffer.i_to_shift] = s.buffer.indexed_list[i]
end
for i = 0, s.buffer.i_to_shift - 1 do s.buffer.indexed_list[s.buffer.i_total - i] = nil end
s.buffer.i_total = s.buffer.i_total - s.buffer.i_to_shift
s.buffer.i_to_shift = 0
collectgarbage("step")
end
-- limit: automatic adjustment
s.last_limit = s.limit.current
if s.f_limit_runtime or timestamp >= s.limit.timer then
s.limit.last_target = s.limit.target
if collected.is_source or current.is_source or corrected.is_bigger then
-- increase limit
s.limit.target = 1
if s.limit.current + s.limit.step * s.limit.up <= options.detect_limit then
s.limit.current = s.limit.current + s.limit.step * s.limit.up
else
s.limit.current = options.detect_limit
end
elseif not current.is_invalid and
(collected.is_trusted_offsets or collected == s.last_collected or current == s.last_current) then
-- stable limit
s.limit.target = 0
-- reset limit to help with different dark color
if not current.is_trusted_offsets then s.limit.current = options.detect_limit end
elseif s.limit.current > 0 then
-- decrease limit
s.limit.target = -1
if s.limit.min < s.limit.current and s.limit.last_target == -1 then
s.limit.current = s.limit.min
elseif s.limit.current - s.limit.step >= 0 then
s.limit.current = s.limit.current - s.limit.step
else
s.limit.current = 0
end
end
end
-- store for next process
s.last_current = current
s.last_collected = collected
s.last_timestamp = timestamp
-- limit: apply change
if s.last_limit ~= s.limit.current then
if not s.f_limit_runtime and options.limit_timer > 0 then s.limit.timer = timestamp + options.limit_timer end
s.limit.counter = s.limit.counter + 1
insert_cropdetect_filter(s.limit.current, true)
end
s.in_progress = false
end
local function time_pos(event, value, err)
if value and s.indexed_read_ahead[1] then
local time_pos = shifting_to(RIGHT, value)
local deviation = math.abs(time_pos - s.pts)
local crop_sync = s.frametime * (options.read_ahead_sync + options.crop_method_sync)
local time_pos_read_ahead = time_pos - (options.read_ahead_timer - deviation - crop_sync)
if time_pos_read_ahead >= s.indexed_read_ahead[1].pts then
apply_crop(s.indexed_read_ahead[1].ref, s.indexed_read_ahead[1].pts)
table.remove(s.indexed_read_ahead, 1)
end
end
end
local function collect_metadata(event)
if event.prefix == "ffmpeg" and event.level == "v" and string.find(event.text, "^.*dyn_cd: ") and
not (s.seeking or s.paused or s.toggled > 1) then
local tmp = {}
for k, v in string.gmatch(event.text, "(%w+):(%-?%d+%.?%d* )") do tmp[k] = tonumber(v) end
tmp.whxy = string.format("w=%d:h=%d:x=%d:y=%d", tmp.w, tmp.h, tmp.x, tmp.y)
s.pts = shifting_to(LEFT, tmp.pts)
if tmp.whxy ~= s.collected.whxy then
s.collected = s.stats.trusted[tmp.whxy] or s.stats.buffer[tmp.whxy] or compute_metadata(tmp)
end
s.limit.last_collect = s.limit.collect
s.limit.collect = tmp.limit or s.limit.collect
s.f_limit_runtime = tmp.limit ~= nil -- if ffmpeg is patch for limit change at runtime
s.timestamps.previous = s.timestamps.current
s.timestamps.current = s.pts
local wait_limit = s.f_limit_runtime and s.f_limit_change and s.limit.collect == s.limit.last_collect
if not wait_limit then s.f_limit_change = false end
if s.in_progress or not s.timestamps.previous or wait_limit or s.f_inserted or s.timestamps.current <
options.start_delay then
s.f_inserted = false
return
end
local elapsed_time = s.timestamps.current - s.timestamps.previous
if not s.frametime or elapsed_time < s.frametime and elapsed_time > 0 then s.frametime = elapsed_time end
process_metadata(s.collected, s.timestamps.current, elapsed_time)
end
end
local function seek(event)
if s.seek_done then return end
print_debug("pre_format", string.format("Stop by %s event.", event))
if event == "seek" or event == "toggle" then
s.timestamps = {}
s.limit.timer = 0
s.approved = s.applied -- re-sync
if event == "seek" then
if s.f_limit_runtime then insert_cropdetect_filter(s.limit.current) end
if not s.f_video_crop and
(filter_state(labels.crop, "enabled", true) or not filter_state(labels.crop) and s.applied ~= s.source) then
apply_crop(s.applied)
end
end
if s.f_limit_runtime then
s.indexed_read_ahead = {}
s.collected = {}
end
s.seek_done = true -- avoid seek() in loop until we resume()
end
end
local function resume(event)
s.seek_done = false
print_debug("pre_format", string.format("Resume by %s event.", event))
if event == "toggle" and s.f_limit_runtime or not filter_state(labels.cropdetect) then
insert_cropdetect_filter(s.limit.current)
end
end
local function playback_events(t, id, error)
if t.event == "seek" then
s.seeking = true
seek(t.event)
else
if not s.paused then resume(t.event) end
s.seeking = false
end
end
local ENABLE, DISABLE_WITH_CROP, DISABLE = 1, 2, 3
function on_toggle(auto)
if s.f_missing then
mp.osd_message("Libavfilter cropdetect missing", 3)
return
end
local EVENT = "toggle"
if s.toggled == ENABLE then
s.toggled = DISABLE_WITH_CROP
if filter_state(labels.cropdetect, "enabled", true) then
mp.commandv("vf", EVENT, string.format("@%s", labels.cropdetect))
end
seek(EVENT)
if not auto then mp.osd_message(string.format("%s: disabled, crop remains.", label_prefix), 3) end
elseif s.toggled == DISABLE_WITH_CROP then
s.toggled = DISABLE
if filter_state(labels.cropdetect, "enabled", false) then
if s.f_video_crop then
mp.set_property("video-crop", "")
elseif filter_state(labels.crop, "enabled", true) then
mp.commandv("vf", EVENT, string.format("@%s", labels.crop))
end
end
if not auto then mp.osd_message(string.format("%s: crop removed.", label_prefix), 3) end
else -- s.toggled == DISABLE
s.toggled = ENABLE
if filter_state(labels.cropdetect, "enabled", false) then
mp.commandv("vf", EVENT, string.format("@%s", labels.cropdetect))
end
if s.f_video_crop then
apply_crop(s.applied)
elseif filter_state(labels.crop, "enabled", false) then
mp.commandv("vf", EVENT, string.format("@%s", labels.crop))
end
resume(EVENT)
if not auto then mp.osd_message(string.format("%s: enabled.", label_prefix), 3) end
end
end
local function pause(event, is_paused)
s.paused = is_paused
if is_paused then
seek(event)
print_stats()
print_debug("buffer")
print_debug("applied")
print_debug("pre_format", "s.approved: " .. s.approved.whxy)
print_debug("pre_format", "s.applied: " .. s.applied.whxy)
if s.indexed_read_ahead[1] then
print_debug("pre_format", "s.indexed_read_ahead[1]: " .. s.indexed_read_ahead[1].ref.whxy)
end
else
if s.toggled == 1 then resume(event) end
end
end
function cleanup()
if not s.started then return end
if not s.paused then print_stats() end
mp.msg.info("Cleanup...")
mp.set_property("auto-window-resize", s.user_auto_window_resize)
mp.unregister_event(playback_events)
mp.unregister_event(collect_metadata)
mp.unobserve_property(time_pos)
mp.unobserve_property(switch_hwdec)
mp.unobserve_property(pause)
for _, label in pairs(labels) do
if filter_state(label) then mp.commandv("vf", "remove", string.format("@%s", label)) end
end
if s.f_video_crop then mp.set_property("video-crop", "") end
mp.msg.info("Done.")
s.started = false
end
local function on_start()
mp.msg.info("File loaded.")
if not is_cropable() then
mp.msg.warn("Exit, only works for videos.")
return
end
s.user_geometry = mp.get_property("geometry")
s.user_auto_window_resize = mp.get_property("auto-window-resize")
if options.fix_windowed_behavior == 1 and s.user_auto_window_resize == "yes" then
mp.set_property("auto-window-resize", "no")
end
-- init/re-init stored data
s.buffer = {i_to_shift = 0, i_total = 0, i_ratio = 0, indexed_list = {}, t_total = 0, t_ratio = 0}
s.candidate = {i_fallback = 0, i_offset = 0, fallback = {}, offset = {}}
s.collected = {}
s.indexed_read_ahead = {}
s.limit = {
counter = 0, current = options.detect_limit, min = options.detect_limit, step = 2, target = 0, timer = 0, up = 2
}
s.stats = {applied = {}, buffer = {}, buffer_unique = 0, trusted = {}, trusted_offset = {}, trusted_unique = 1}
s.stats.indexed_applied = {}
s.source = {w_untouched = mp.get_property_number("width"), h_untouched = mp.get_property_number("height")}
s.source.w = math.floor(s.source.w_untouched / options.detect_round) * options.detect_round
s.source.h = math.floor(s.source.h_untouched / options.detect_round) * options.detect_round
s.source.x = math.floor((s.source.w_untouched - s.source.w) / 2)
s.source.y = math.floor((s.source.h_untouched - s.source.h) / 2)
s.stats.trusted_offset = {x = {s.source.x}, y = {s.source.y}}
s.ratios = {w = {}, h = {}}
generate_ratios(options.ratios)
s.source = compute_metadata(s.source)
s.stats.trusted[s.source.whxy] = s.source
s.source.applied = 1
s.source.time.accumulated = 0
s.applied = s.source
s.approved = s.source
s.timestamps = {}
if options.read_ahead_mode > 0 then
-- assume cropdetect is patch for command "limit", fallback at the first collected metadata otherwise.
s.f_limit_runtime = true
-- quick test for dummysync filter
s.f_sync = mp.commandv("vf", "add", string.format("@%s:lavfi=[split[a][b];[a][b]dummysync]", label_prefix)) and
mp.commandv("vf", "remove", string.format("@%s", label_prefix)) and "[a][b]dummysync" or
"[a][b]psnr=eof_action=pass"
end
s.f_video_crop = options.crop_method == 1 and mp.get_property("video-crop") ~= nil -- true if supported
-- register events
mp.register_event("seek", playback_events)
mp.register_event("playback-restart", playback_events)
mp.observe_property("time-pos", "number", time_pos)
mp.observe_property("hwdec", "string", switch_hwdec)
mp.observe_property("pause", "bool", pause)
mp.enable_messages('v')
mp.register_event("log-message", collect_metadata)
s.toggled = (options.mode % 2 == 1) and DISABLE or ENABLE
s.started = true -- everything ready
end
mp.add_key_binding("C", "toggle_crop", on_toggle)
mp.register_event("end-file", cleanup)
mp.register_event("file-loaded", on_start)

View File

@@ -0,0 +1 @@
/usr/share/mpv/scripts/gamma-auto.lua

View File

@@ -0,0 +1 @@
/usr/share/mpv/scripts/idet.sh

View File

@@ -0,0 +1 @@
/usr/lib/mpv-mpris/mpris.so

View File

@@ -0,0 +1 @@
/usr/share/mpv/scripts/mpv_identify.sh

View File

@@ -0,0 +1 @@
/usr/share/mpv/scripts/observe-all.lua

View File

@@ -0,0 +1 @@
/usr/share/mpv/scripts/ontop-playback.lua

View File

@@ -0,0 +1 @@
/usr/share/mpv/scripts/pause-when-minimize.lua

View File

@@ -0,0 +1 @@
/usr/share/mpv/scripts/skip-logo.lua

View File

@@ -0,0 +1,569 @@
-- sponsorblock.lua
--
-- This script skips sponsored segments of YouTube videos
-- using data from https://github.com/ajayyy/SponsorBlock
local ON_WINDOWS = package.config:sub(1,1) ~= "/"
local options = {
server_address = "https://sponsor.ajay.app",
python_path = ON_WINDOWS and "python" or "python3",
-- Categories to fetch
categories = "sponsor,intro,outro,interaction,selfpromo,filler",
-- Categories to skip automatically
skip_categories = "sponsor",
-- If true, sponsored segments will only be skipped once
skip_once = true,
-- Note that sponsored segments may ocasionally be inaccurate if this is turned off
-- see https://blog.ajay.app/voting-and-pseudo-randomness-or-sponsorblock-or-youtube-sponsorship-segment-blocker
local_database = false,
-- Update database on first run, does nothing if local_database is false
auto_update = true,
-- How long to wait between local database updates
-- Format: "X[d,h,m]", leave blank to update on every mpv run
auto_update_interval = "6h",
-- User ID used to submit sponsored segments, leave blank for random
user_id = "",
-- Name to display on the stats page https://sponsor.ajay.app/stats/ leave blank to keep current name
display_name = "",
-- Tell the server when a skip happens
report_views = true,
-- Auto upvote skipped sponsors
auto_upvote = false,
-- Use sponsor times from server if they're more up to date than our local database
server_fallback = true,
-- Create chapters at sponsor boundaries for OSC display and manual skipping
make_chapters = true,
-- Minimum duration for sponsors (in seconds), segments under that threshold will be ignored
min_duration = 1,
-- Fade audio for smoother transitions
audio_fade = false,
-- Audio fade step, applied once every 100ms until cap is reached
audio_fade_step = 10,
-- Audio fade cap
audio_fade_cap = 0,
-- Fast forward through sponsors instead of skipping
fast_forward = false,
-- Playback speed modifier when fast forwarding, applied once every second until cap is reached
fast_forward_increase = .2,
-- Playback speed cap
fast_forward_cap = 2,
-- Length of the sha256 prefix (3-32) when querying server, 0 to disable
sha256_length = 4,
-- Pattern for video id in local files, ignored if blank
-- Recommended value for base youtube-dl is "-([%w-_]+)%.[mw][kpe][v4b]m?$"
local_pattern = "",
-- Legacy option, use skip_categories instead
skip = true
}
mp.options = require "mp.options"
mp.options.read_options(options, "sponsorblock")
local legacy = mp.command_native_async == nil
--[[
if legacy then
options.local_database = false
end
--]]
options.local_database = false
local utils = require "mp.utils"
scripts_dir = mp.find_config_file("scripts")
local sponsorblock = utils.join_path(scripts_dir, "sponsorblock_shared/sponsorblock.py")
local uid_path = utils.join_path(scripts_dir, "sponsorblock_shared/sponsorblock.txt")
local database_file = options.local_database and utils.join_path(scripts_dir, "sponsorblock_shared/sponsorblock.db") or ""
local youtube_id = nil
local ranges = {}
local init = false
local segment = {a = 0, b = 0, progress = 0, first = true}
local retrying = false
local last_skip = {uuid = "", dir = nil}
local speed_timer = nil
local fade_timer = nil
local fade_dir = nil
local volume_before = mp.get_property_number("volume")
local categories = {}
local all_categories = {"sponsor", "intro", "outro", "interaction", "selfpromo", "preview", "music_offtopic", "filler"}
local chapter_cache = {}
for category in string.gmatch(options.skip_categories, "([^,]+)") do
categories[category] = true
end
function file_exists(name)
local f = io.open(name,"r")
if f ~= nil then io.close(f) return true else return false end
end
function t_count(t)
local count = 0
for _ in pairs(t) do count = count + 1 end
return count
end
function time_sort(a, b)
if a.time == b.time then
return string.match(a.title, "segment end")
end
return a.time < b.time
end
function parse_update_interval()
local s = options.auto_update_interval
if s == "" then return 0 end -- Interval Disabled
local num, mod = s:match "^(%d+)([hdm])$"
if num == nil or mod == nil then
mp.osd_message("[sponsorblock] auto_update_interval " .. s .. " is invalid", 5)
return nil
end
local time_table = {
m = 60,
h = 60 * 60,
d = 60 * 60 * 24,
}
return num * time_table[mod]
end
function clean_chapters()
local chapters = mp.get_property_native("chapter-list")
local new_chapters = {}
for _, chapter in pairs(chapters) do
if chapter.title ~= "Preview segment start" and chapter.title ~= "Preview segment end" then
table.insert(new_chapters, chapter)
end
end
mp.set_property_native("chapter-list", new_chapters)
end
function create_chapter(chapter_title, chapter_time)
local chapters = mp.get_property_native("chapter-list")
local duration = mp.get_property_native("duration")
table.insert(chapters, {title=chapter_title, time=(duration == nil or duration > chapter_time) and chapter_time or duration - .001})
table.sort(chapters, time_sort)
mp.set_property_native("chapter-list", chapters)
end
function process(uuid, t, new_ranges)
start_time = tonumber(string.match(t, "[^,]+"))
end_time = tonumber(string.sub(string.match(t, ",[^,]+"), 2))
for o_uuid, o_t in pairs(ranges) do
if (start_time >= o_t.start_time and start_time <= o_t.end_time) or (o_t.start_time >= start_time and o_t.start_time <= end_time) then
new_ranges[o_uuid] = o_t
return
end
end
category = string.match(t, "[^,]+$")
if categories[category] and end_time - start_time >= options.min_duration then
new_ranges[uuid] = {
start_time = start_time,
end_time = end_time,
category = category,
skipped = false
}
end
if options.make_chapters and not chapter_cache[uuid] then
chapter_cache[uuid] = true
local category_title = (category:gsub("^%l", string.upper):gsub("_", " "))
create_chapter(category_title .. " segment start (" .. string.sub(uuid, 1, 6) .. ")", start_time)
create_chapter(category_title .. " segment end (" .. string.sub(uuid, 1, 6) .. ")", end_time)
end
end
function getranges(_, exists, db, more)
if type(exists) == "table" and exists["status"] == "1" then
if options.server_fallback then
mp.add_timeout(0, function() getranges(true, true, "") end)
else
return mp.osd_message("[sponsorblock] database update failed, gave up")
end
end
if db ~= "" and db ~= database_file then db = database_file end
if exists ~= true and not file_exists(db) then
if not retrying then
mp.osd_message("[sponsorblock] database update failed, retrying...")
retrying = true
end
return update()
end
if retrying then
mp.osd_message("[sponsorblock] database update succeeded")
retrying = false
end
local sponsors
local args = {
options.python_path,
sponsorblock,
"ranges",
db,
options.server_address,
youtube_id,
options.categories,
tostring(options.sha256_length)
}
if not legacy then
sponsors = mp.command_native({name = "subprocess", capture_stdout = true, playback_only = false, args = args})
else
sponsors = utils.subprocess({args = args})
end
mp.msg.debug("Got: " .. string.gsub(sponsors.stdout, "[\n\r]", ""))
if not string.match(sponsors.stdout, "^%s*(.*%S)") then return end
if string.match(sponsors.stdout, "error") then return getranges(true, true) end
local new_ranges = {}
local r_count = 0
if more then r_count = -1 end
for t in string.gmatch(sponsors.stdout, "[^:%s]+") do
uuid = string.match(t, "([^,]+),[^,]+$")
if ranges[uuid] then
new_ranges[uuid] = ranges[uuid]
else
process(uuid, t, new_ranges)
end
r_count = r_count + 1
end
local c_count = t_count(ranges)
if c_count == 0 or r_count >= c_count then
ranges = new_ranges
end
end
function fast_forward()
if options.fast_forward and options.fast_forward == true then
speed_timer = nil
mp.set_property("speed", 1)
end
local last_speed = mp.get_property_number("speed")
local new_speed = math.min(last_speed + options.fast_forward_increase, options.fast_forward_cap)
if new_speed <= last_speed then return end
mp.set_property("speed", new_speed)
end
function fade_audio(step)
local last_volume = mp.get_property_number("volume")
local new_volume = math.max(options.audio_fade_cap, math.min(last_volume + step, volume_before))
if new_volume == last_volume then
if step >= 0 then fade_dir = nil end
if fade_timer ~= nil then fade_timer:kill() end
fade_timer = nil
return
end
mp.set_property("volume", new_volume)
end
function skip_ads(name, pos)
if pos == nil then return end
local sponsor_ahead = false
for uuid, t in pairs(ranges) do
if (options.fast_forward == uuid or not options.skip_once or not t.skipped) and t.start_time <= pos and t.end_time > pos then
if options.fast_forward == uuid then return end
if options.fast_forward == false then
mp.osd_message("[sponsorblock] " .. t.category .. " skipped")
mp.set_property("time-pos", t.end_time)
else
mp.osd_message("[sponsorblock] skipping " .. t.category)
end
t.skipped = true
last_skip = {uuid = uuid, dir = nil}
if options.report_views or options.auto_upvote then
local args = {
options.python_path,
sponsorblock,
"stats",
database_file,
options.server_address,
youtube_id,
uuid,
options.report_views and "1" or "",
uid_path,
options.user_id,
options.auto_upvote and "1" or ""
}
if not legacy then
mp.command_native_async({name = "subprocess", playback_only = false, args = args}, function () end)
else
utils.subprocess_detached({args = args})
end
end
if options.fast_forward ~= false then
options.fast_forward = uuid
if speed_timer ~= nil then speed_timer:kill() end
speed_timer = mp.add_periodic_timer(1, fast_forward)
end
return
elseif (not options.skip_once or not t.skipped) and t.start_time <= pos + 1 and t.end_time > pos + 1 then
sponsor_ahead = true
end
end
if options.audio_fade then
if sponsor_ahead then
if fade_dir ~= false then
if fade_dir == nil then volume_before = mp.get_property_number("volume") end
if fade_timer ~= nil then fade_timer:kill() end
fade_dir = false
fade_timer = mp.add_periodic_timer(.1, function() fade_audio(-options.audio_fade_step) end)
end
elseif fade_dir == false then
fade_dir = true
if fade_timer ~= nil then fade_timer:kill() end
fade_timer = mp.add_periodic_timer(.1, function() fade_audio(options.audio_fade_step) end)
end
end
if options.fast_forward and options.fast_forward ~= true then
options.fast_forward = true
speed_timer:kill()
speed_timer = nil
mp.set_property("speed", 1)
end
end
function vote(dir)
if last_skip.uuid == "" then return mp.osd_message("[sponsorblock] no sponsors skipped, can't submit vote") end
local updown = dir == "1" and "up" or "down"
if last_skip.dir == dir then return mp.osd_message("[sponsorblock] " .. updown .. "vote already submitted") end
last_skip.dir = dir
local args = {
options.python_path,
sponsorblock,
"stats",
database_file,
options.server_address,
youtube_id,
last_skip.uuid,
"",
uid_path,
options.user_id,
dir
}
if not legacy then
mp.command_native_async({name = "subprocess", playback_only = false, args = args}, function () end)
else
utils.subprocess({args = args})
end
mp.osd_message("[sponsorblock] " .. updown .. "vote submitted")
end
function update()
mp.command_native_async({name = "subprocess", playback_only = false, args = {
options.python_path,
sponsorblock,
"update",
database_file,
options.server_address
}}, getranges)
end
function file_loaded()
local initialized = init
ranges = {}
segment = {a = 0, b = 0, progress = 0, first = true}
last_skip = {uuid = "", dir = nil}
chapter_cache = {}
local video_path = mp.get_property("path", "")
mp.msg.debug("Path: " .. video_path)
local video_referer = string.match(mp.get_property("http-header-fields", ""), "Referer:([^,]+)") or ""
mp.msg.debug("Referer: " .. video_referer)
local urls = {
"ytdl://([%w-_]+).*",
"https?://youtu%.be/([%w-_]+).*",
"https?://w?w?w?%.?youtube%.com/v/([%w-_]+).*",
"/watch.*[?&]v=([%w-_]+).*",
"/embed/([%w-_]+).*"
}
youtube_id = nil
for i, url in ipairs(urls) do
youtube_id = youtube_id or string.match(video_path, url) or string.match(video_referer, url)
if youtube_id then break end
end
youtube_id = youtube_id or string.match(video_path, options.local_pattern)
if not youtube_id or string.len(youtube_id) < 11 or (local_pattern and string.len(youtube_id) ~= 11) then return end
youtube_id = string.sub(youtube_id, 1, 11)
mp.msg.debug("Found YouTube ID: " .. youtube_id)
init = true
if not options.local_database then
getranges(true, true)
else
local exists = file_exists(database_file)
if exists and options.server_fallback then
getranges(true, true)
mp.add_timeout(0, function() getranges(true, true, "", true) end)
elseif exists then
getranges(true, true)
elseif options.server_fallback then
mp.add_timeout(0, function() getranges(true, true, "") end)
end
end
if initialized then return end
if options.skip then
mp.observe_property("time-pos", "native", skip_ads)
end
if options.display_name ~= "" then
local args = {
options.python_path,
sponsorblock,
"username",
database_file,
options.server_address,
youtube_id,
"",
"",
uid_path,
options.user_id,
options.display_name
}
if not legacy then
mp.command_native_async({name = "subprocess", playback_only = false, args = args}, function () end)
else
utils.subprocess_detached({args = args})
end
end
if not options.local_database or (not options.auto_update and file_exists(database_file)) then return end
if file_exists(database_file) then
local db_info = utils.file_info(database_file)
local cur_time = os.time(os.date("*t"))
local upd_interval = parse_update_interval()
if upd_interval == nil or os.difftime(cur_time, db_info.mtime) < upd_interval then return end
end
update()
end
function set_segment()
if not youtube_id then return end
local pos = mp.get_property_number("time-pos")
if pos == nil then return end
if segment.progress > 1 then
segment.progress = segment.progress - 2
end
if segment.progress == 1 then
segment.progress = 0
segment.b = pos
mp.osd_message("[sponsorblock] segment boundary B set, press again for boundary A", 3)
else
segment.progress = 1
segment.a = pos
mp.osd_message("[sponsorblock] segment boundary A set, press again for boundary B", 3)
end
if options.make_chapters and not segment.first then
local start_time = math.min(segment.a, segment.b)
local end_time = math.max(segment.a, segment.b)
if end_time - start_time ~= 0 and end_time ~= 0 then
clean_chapters()
create_chapter("Preview segment start", start_time)
create_chapter("Preview segment end", end_time)
end
end
segment.first = false
end
function select_category(selected)
for category in string.gmatch(options.categories, "([^,]+)") do
mp.remove_key_binding("select_category_"..category)
mp.remove_key_binding("kp_select_category_"..category)
end
submit_segment(selected)
end
function submit_segment(category)
if not youtube_id then return end
local start_time = math.min(segment.a, segment.b)
local end_time = math.max(segment.a, segment.b)
if end_time - start_time == 0 or end_time == 0 then
mp.osd_message("[sponsorblock] empty segment, not submitting")
elseif segment.progress <= 1 then
segment.progress = segment.progress + 2
local category_list = ""
for category_id, category in pairs(all_categories) do
local category_title = (category:gsub("^%l", string.upper):gsub("_", " "))
category_list = category_list .. category_id .. ": " .. category_title .. "\n"
mp.add_forced_key_binding(tostring(category_id), "select_category_"..category, function() select_category(category) end)
mp.add_forced_key_binding("KP"..tostring(category_id), "kp_select_category_"..category, function() select_category(category) end)
end
mp.osd_message(string.format("[sponsorblock] press a number to select category for segment: %.2d:%.2d:%.2d to %.2d:%.2d:%.2d\n\n" .. category_list .. "\nyou can press Shift+G again for default (Sponsor) or hide this message with g", math.floor(start_time/(60*60)), math.floor(start_time/60%60), math.floor(start_time%60), math.floor(end_time/(60*60)), math.floor(end_time/60%60), math.floor(end_time%60)), 30)
else
mp.osd_message("[sponsorblock] submitting segment...", 30)
local submit
local args = {
options.python_path,
sponsorblock,
"submit",
database_file,
options.server_address,
youtube_id,
tostring(start_time),
tostring(end_time),
uid_path,
options.user_id,
category or "sponsor"
}
if not legacy then
submit = mp.command_native({name = "subprocess", capture_stdout = true, playback_only = false, args = args})
else
submit = utils.subprocess({args = args})
end
if string.match(submit.stdout, "success") then
segment = {a = 0, b = 0, progress = 0, first = true}
mp.osd_message("[sponsorblock] segment submitted")
if options.make_chapters then
clean_chapters()
create_chapter("Submitted segment start", start_time)
create_chapter("Submitted segment end", end_time)
end
elseif string.match(submit.stdout, "error") then
mp.osd_message("[sponsorblock] segment submission failed, server may be down. try again", 5)
elseif string.match(submit.stdout, "502") then
mp.osd_message("[sponsorblock] segment submission failed, server is down. try again", 5)
elseif string.match(submit.stdout, "400") then
mp.osd_message("[sponsorblock] segment submission failed, impossible inputs", 5)
segment = {a = 0, b = 0, progress = 0, first = true}
elseif string.match(submit.stdout, "429") then
mp.osd_message("[sponsorblock] segment submission failed, rate limited. try again", 5)
elseif string.match(submit.stdout, "409") then
mp.osd_message("[sponsorblock] segment already submitted", 3)
segment = {a = 0, b = 0, progress = 0, first = true}
else
mp.osd_message("[sponsorblock] segment submission failed", 5)
end
end
end
mp.register_event("file-loaded", file_loaded)
mp.add_key_binding("g", "set_segment", set_segment)
mp.add_key_binding("G", "submit_segment", submit_segment)
mp.add_key_binding("h", "upvote_segment", function() return vote("1") end)
mp.add_key_binding("H", "downvote_segment", function() return vote("0") end)
-- Bindings below are for backwards compatibility and could be removed at any time
mp.add_key_binding(nil, "sponsorblock_set_segment", set_segment)
mp.add_key_binding(nil, "sponsorblock_submit_segment", submit_segment)
mp.add_key_binding(nil, "sponsorblock_upvote", function() return vote("1") end)
mp.add_key_binding(nil, "sponsorblock_downvote", function() return vote("0") end)

View File

@@ -0,0 +1,3 @@
-- This is a dummy main.lua
-- required for mpv 0.33
-- do not delete

View File

@@ -0,0 +1,122 @@
import urllib.request
import urllib.parse
import hashlib
import sqlite3
import random
import string
import json
import sys
import os
if sys.argv[1] in ["submit", "stats", "username"]:
if not sys.argv[8]:
if os.path.isfile(sys.argv[7]):
with open(sys.argv[7]) as f:
uid = f.read()
else:
uid = "".join(random.choices(string.ascii_letters + string.digits, k=36))
with open(sys.argv[7], "w") as f:
f.write(uid)
else:
uid = sys.argv[8]
opener = urllib.request.build_opener()
opener.addheaders = [("User-Agent", "mpv_sponsorblock/1.0 (https://github.com/po5/mpv_sponsorblock)")]
urllib.request.install_opener(opener)
if sys.argv[1] == "ranges" and (not sys.argv[2] or not os.path.isfile(sys.argv[2])):
sha = None
if 3 <= int(sys.argv[6]) <= 32:
sha = hashlib.sha256(sys.argv[4].encode()).hexdigest()[:int(sys.argv[6])]
times = []
try:
response = urllib.request.urlopen(sys.argv[3] + "/api/skipSegments" + ("/" + sha + "?" if sha else "?videoID=" + sys.argv[4] + "&") + urllib.parse.urlencode([("categories", json.dumps(sys.argv[5].split(",")))]))
segments = json.load(response)
for segment in segments:
if sha and sys.argv[4] != segment["videoID"]:
continue
if sha:
for s in segment["segments"]:
times.append(str(s["segment"][0]) + "," + str(s["segment"][1]) + "," + s["UUID"] + "," + s["category"])
else:
times.append(str(segment["segment"][0]) + "," + str(segment["segment"][1]) + "," + segment["UUID"] + "," + segment["category"])
print(":".join(times))
except (TimeoutError, urllib.error.URLError) as e:
print("error")
except urllib.error.HTTPError as e:
if e.code == 404:
print("")
else:
print("error")
elif sys.argv[1] == "ranges":
conn = sqlite3.connect(sys.argv[2])
conn.row_factory = sqlite3.Row
c = conn.cursor()
times = []
for category in sys.argv[5].split(","):
c.execute("SELECT startTime, endTime, votes, UUID, category FROM sponsorTimes WHERE videoID = ? AND shadowHidden = 0 AND votes > -1 AND category = ?", (sys.argv[4], category))
sponsors = c.fetchall()
best = list(sponsors)
dealtwith = []
similar = []
for sponsor_a in sponsors:
for sponsor_b in sponsors:
if sponsor_a is not sponsor_b and sponsor_a["startTime"] >= sponsor_b["startTime"] and sponsor_a["startTime"] <= sponsor_b["endTime"]:
similar.append([sponsor_a, sponsor_b])
if sponsor_a in best:
best.remove(sponsor_a)
if sponsor_b in best:
best.remove(sponsor_b)
for sponsors_a in similar:
if sponsors_a in dealtwith:
continue
group = set(sponsors_a)
for sponsors_b in similar:
if sponsors_b[0] in group or sponsors_b[1] in group:
group.add(sponsors_b[0])
group.add(sponsors_b[1])
dealtwith.append(sponsors_b)
best.append(max(group, key=lambda x:x["votes"]))
for time in best:
times.append(str(time["startTime"]) + "," + str(time["endTime"]) + "," + time["UUID"] + "," + time["category"])
print(":".join(times))
elif sys.argv[1] == "update":
try:
urllib.request.urlretrieve(sys.argv[3] + "/database.db", sys.argv[2] + ".tmp")
os.replace(sys.argv[2] + ".tmp", sys.argv[2])
except PermissionError:
print("database update failed, file currently in use", file=sys.stderr)
sys.exit(1)
except ConnectionResetError:
print("database update failed, connection reset", file=sys.stderr)
sys.exit(1)
except TimeoutError:
print("database update failed, timed out", file=sys.stderr)
sys.exit(1)
except urllib.error.URLError:
print("database update failed", file=sys.stderr)
sys.exit(1)
elif sys.argv[1] == "submit":
try:
req = urllib.request.Request(sys.argv[3] + "/api/skipSegments", data=json.dumps({"videoID": sys.argv[4], "segments": [{"segment": [float(sys.argv[5]), float(sys.argv[6])], "category": sys.argv[9]}], "userID": uid}).encode(), headers={"Content-Type": "application/json"})
response = urllib.request.urlopen(req)
print("success")
except urllib.error.HTTPError as e:
print(e.code)
except:
print("error")
elif sys.argv[1] == "stats":
try:
if sys.argv[6]:
urllib.request.urlopen(sys.argv[3] + "/api/viewedVideoSponsorTime?UUID=" + sys.argv[5])
if sys.argv[9]:
urllib.request.urlopen(sys.argv[3] + "/api/voteOnSponsorTime?UUID=" + sys.argv[5] + "&userID=" + uid + "&type=" + sys.argv[9])
except:
pass
elif sys.argv[1] == "username":
try:
data = urllib.parse.urlencode({"userID": uid, "userName": sys.argv[9]}).encode()
req = urllib.request.Request(sys.argv[3] + "/api/setUsername", data=data)
urllib.request.urlopen(req)
except:
pass

View File

@@ -0,0 +1 @@
2w0fKpa3ZP3LF1GOsREvGfMGUBYbyrJzf24U

View File

@@ -0,0 +1 @@
/usr/share/mpv/scripts/stats-conv.py

View File

@@ -0,0 +1 @@
/usr/share/mpv/scripts/status-line.lua

View File

@@ -0,0 +1 @@
/usr/share/mpv/scripts/umpv

View File

@@ -0,0 +1 @@
/usr/share/mpv/scripts/uosc

1
.config/mpv/fonts Symbolic link
View File

@@ -0,0 +1 @@
/usr/share/mpv/fonts

4
.config/mpv/input.conf Normal file
View File

@@ -0,0 +1,4 @@
# Press 'n' to toggle the default audio filter on/off
n af toggle "dynaudnorm=f=150:g=15:p=0.9"
# Press 'Shift+N' to toggle the default compressor/normalizer chain on/off
N af toggle "lavfi=[acompressor=threshold=-18dB:ratio=5:attack=20:release=250,loudnorm=I=-16:LRA=11:TP=-1.5]"

50
.config/mpv/mpv.conf Normal file
View File

@@ -0,0 +1,50 @@
### Allgemeine Video-Einstellungen
volume=80
profile=gpu-hq
fs
video-sync=display-resample
scale=ewa_lanczossharp
cscale=ewa_lanczossharp
# Vollständiges Deaktivieren aller mpv Standard-UI
osc=no
osd-level=0
osd-bar=no
border=no
# Keine OSD-Nachrichten
osd-playing-msg=
osd-status-msg=
osd-msg1=
osd-msg2=
osd-msg3=
# Terminal-Output minimieren
quiet
msg-level=all=no
# uosc Konfiguration
script-opts=uosc-scroll-text=no
### Rest deiner Konfiguration...
hwdec=auto-copy
gpu-context=wayland
gpu-api=auto
# Streaming & Netzwerk...
stream-lavf-o-append=reconnect_on_http_error=4xx,5xx
stream-lavf-o-append=reconnect_on_network_error=yes
stream-lavf-o-append=reconnect_streamed=yes
stream-lavf-o-append=reconnect_delay_max=30
# Farbprofile...
[HDR]
profile-cond=get("video-params/primaries") == "bt.2020" and get("video-params/gamma") == "pq"
profile-restore=copy
target-colorspace-hint=yes
[SDR]
profile-cond=get("video-params/primaries") == "bt.709"
profile-restore=copy
target-trc=srgb

1
.config/mpv/script-opts Symbolic link
View File

@@ -0,0 +1 @@
/usr/share/mpv/script-opts

View File

@@ -0,0 +1 @@
/usr/share/mpv/scripts/README.md

View File

@@ -0,0 +1 @@
/usr/share/mpv/scripts/acompressor.lua

View File

@@ -0,0 +1 @@
/usr/share/mpv/scripts/ao-null-reload.lua

View File

@@ -0,0 +1 @@
/usr/share/mpv/scripts/autocrop.lua

View File

@@ -0,0 +1 @@
/usr/share/mpv/scripts/autodeint.lua

View File

@@ -0,0 +1 @@
/usr/share/mpv/scripts/autoload.lua

View File

@@ -0,0 +1 @@
/usr/share/mpv/scripts/cycle-deinterlace-pullup.lua

View File

@@ -0,0 +1,837 @@
--[[
This script uses the lavfi cropdetect filter to automatically insert a crop filter with appropriate parameters
for the currently playing video, the script run continuously by default (mode 4).
To use this script, "hwdec=no" (mpv default/recommended) or any "-copy" variant like "hwdec=auto-copy" is required,
consider editing "mpv.conf" to an appropriate value.
The workflow is as follows: We observe ffmpeg log to collect metadata and process it.
Collected metadata are stored sequentially in s.buffer, then process to check and
store trusted values to speed up future change for the current video.
It will automatically crop the video as soon as a change is validated.
The default options can be overridden by adding a line into "mpv.conf" with:
script-opts-append=<script_name>-<parameter>=<value>
script-opts-append=dynamic_crop-mode=0
script-opts-append=dynamic_crop-ratios=2.4 2.39 2 4/3 (quotes aren't needed like below)
Extended descriptions for some parameters (For default values, see <options>):
mode: [0-4] 0 disable, 1 on-demand, 2 one-shot, 3 dynamic-manual, 4 dynamic-auto
Mode 1 and 3 requires using the shortcut to start, 2 and 4 have an automatic start.
Shortcut "C" (shift+c) to control the script.
Cycle between ENABLE / DISABLE_WITH_CROP / DISABLE
prevent_change_mode: [0-3] 0 disable 1 keep-largest, 2 keep-lowest, 3 keep-latest
The prevent_change_timer is trigger after a change.
fix_windowed_behavior: [0-3] Avoid the default behavior that resizes the window to the source size
when the crop filter changes in windowed/maximized mode by adjusting geometry.
limit_timer: Only used if the cropdetect filter doesn't handle limit changes with a command (patch 01/2023).
Extend the time between each limit change to reduce the impact on performance caused by re-initializing the
full filter.
read_ahead_mode: Linked to the associated timer and tells how much time in advance to collect the metadata.
This feature is useful for videos with multiple aspect ratio changes for "fast_change_timer".
Note: because this function is in sync with the playback, a delay equivalent to the timer used is
added/reset every time you seek before you get a reaction, so setting 1 is recommanded.
Required at least https://github.com/FFmpeg/FFmpeg/commit/69c060bea21d3b4ce63b5fff40d37e98c70ab88f
and optionally https://github.com/mpv-player/mpv/pull/11182, until mpv patch is being merged to master,
considered this feature experimental because of the errors generated in logs/console by vf-command and
the filter used to sync the filter chain (psnr).
read_ahead_sync: Compensates for the delay when applying the crop filter and the visible result.
Must be adjusted to your tastes and each MPV client depending on their reaction time.
Note: Perfect adjustment is not really possible but generally <= 1 frame, sometimes more in
dark/ambiguous scenes.
segmentation: e.g. 0.5 for 50% - Extra time to allow new metadata to be segmented instead of being continuous.
This is used with ratio_timer, offset_timer and fallback_timer.
e.g. ratio_timer is validated with 5 sec accumulated over 7.5 sec elapsed.
]] --
require "mp.options"
-- options
local options = {
-- behavior
mode = 4, -- [0-4] more details above
start_delay = 0, -- delay in seconds used to skip intro (usefull with mode 2)
prevent_change_timer = 30, -- seconds
prevent_change_mode = 0, -- [0-3], more details above
fix_windowed_behavior = 1, -- [0-3], 0 no-fix, 1 fix-no-resize, 2 fix-keep-width, 3 fix-keep-height
limit_timer = 0.5, -- seconds, 0 disable, more details above
fast_change_timer = 0.2, -- seconds, recommanded to keep default or > 0 if read_ahead is supported by mpv
ratio_timer = 2, -- seconds, meta in ratios list
offset_timer = 20, -- seconds, >= 'ratio_timer', new offset for asymmetric video
fallback_timer = 40, -- seconds, >= 'offset_timer', not in ratios list and possibly with new offset
linked_tolerance = 2, -- int, scale with detect_round to match against source width/height
ratios = "2.76 2.55 24/9 2.4 2.39 2.35 2.2 2.1 2 1.9 1.85 16/9 5/3 1.5 1.43 4/3 1.25 9/16", -- list
ratio_tolerance = 2, -- int (even number), adjust in order to match more easly the ratios list
read_ahead_mode = 0, -- [0-2], 0 disable, 1 fast_change_timer, 2 ratio_timer, more details above
read_ahead_sync = 0, -- int/frame, increase for advance, more details above
segmentation = 0.5, -- [0.0-1] %, 0 will approved only a continuous metadata (strict)
crop_method = 1, -- 0 lavfi-crop (ffmpeg/filter), 1 video-crop (mpv/VO)
-- filter, see https://ffmpeg.org/ffmpeg-filters.html#cropdetect for details
detect_limit = 26, -- is the maximum use, increase it slowly if lighter black are present
detect_round = 2, -- even number
-- verbose
debug = false
}
read_options(options)
if options.mode == 0 then
mp.msg.info("mode = 0, disable script.")
return
end
-- forward declarations
local cleanup, on_toggle
local s = {}
-- labels
local label_prefix = mp.get_script_name()
local labels = {
crop = string.format("%s-crop", label_prefix), cropdetect = string.format("%s-cropdetect", label_prefix)
}
-- shifting decimal to
local LEFT, RIGHT = true, false
local function shifting_to(left, value)
local shift = 1e3
return left and (value / shift) or value >= 1 and math.ceil(value * shift) or value * shift
end
-- options: compute timer and other stuff
for k, v in pairs(options) do
local timer = string.match(tostring(k), "_timer")
if timer then options[k] = shifting_to(RIGHT, v) end
end
options.read_ahead_timer =
options.read_ahead_mode == 1 and options.fast_change_timer or options.read_ahead_mode == 2 and options.ratio_timer *
(1 + options.segmentation) or nil
options.read_ahead_cropdetect = options.read_ahead_timer and shifting_to(LEFT, options.read_ahead_timer) or nil
options.reverse_segmentation = 1 / (1 * (1 + options.segmentation))
options.crop_method_sync = options.crop_method == 0 and 1 or 0 -- lavfi-crop is slower, so give it some advance for read_ahead
local function print_debug(msg_type, meta, label)
if not options.debug then
return
elseif msg_type == "pre_format" then
mp.msg.info(meta)
elseif msg_type == "metadata" then
mp.msg.info(string.format("%s, %-29s | offX:%3s offY:%3s | limit:%-2s", label, meta.whxy, meta.offset.x,
meta.offset.y, s.limit.current))
elseif msg_type == "buffer" and s.stats.buffer then
mp.msg.info("Buffer stats:")
for whxy, ref in pairs(s.stats.buffer) do
mp.msg.info(string.format(
"\\ %-29s | offX=%4s offY=%4s | time=%6ss linked_source=%-4s known_ratio=%-4s trusted_offsets=%s", whxy,
ref.offset.x, ref.offset.y, shifting_to(LEFT, ref.time.buffer), ref.is_linked_to_source or false,
ref.is_known_ratio or false, ref.is_trusted_offsets))
end
mp.msg.info("Buffer list:")
for i, v in ipairs(s.buffer.indexed_list) do
local new_ref = v.new_ref and v.new_ref.whxy or ""
local pts = shifting_to(RIGHT, v.pts)
mp.msg.info(string.format("\\ %3s %-29s %4sms pts:%d new_ref:%s", i, v.ref.whxy, v.t_elapsed, pts, new_ref))
end
mp.msg.info("i_fallback", s.candidate.i_fallback)
mp.msg.info("i_offset", s.candidate.i_offset)
elseif msg_type == "applied" and s.stats.indexed_applied then
mp.msg.info("Applied list:")
for i, v in ipairs(s.stats.indexed_applied) do
mp.msg.info(string.format("\\ %3s %-29s pts:%d", i, v.ref.whxy, shifting_to(RIGHT, v.pts)))
end
end
end
local function print_stats()
if not s.stats and not s.stats.trusted then return end
mp.msg.info("Meta Stats:")
local offsets_list = {x = "", y = ""}
for axis, _ in pairs(offsets_list) do
for _, v in pairs(s.stats.trusted_offset[axis]) do offsets_list[axis] = offsets_list[axis] .. v .. " " end
end
mp.msg.info(
string.format("Limit - min/max: %s/%s | counter: %s", s.limit.min, options.detect_limit, s.limit.counter))
mp.msg.info(string.format("Trusted - unique: %s | offset: X:%sY:%s", s.stats.trusted_unique, offsets_list.x,
offsets_list.y))
for whxy, ref in pairs(s.stats.trusted) do
if s.stats.trusted[whxy] then
mp.msg.info(string.format("\\ %-29s | offX=%3s offY=%3s | applied=%s overall=%ss accumulated=%ss", whxy,
ref.offset.x, ref.offset.y, ref.applied, shifting_to(LEFT, ref.time.overall),
shifting_to(LEFT, ref.time.accumulated)))
end
end
mp.msg.info("Buffer - unique: " .. s.stats.buffer_unique .. " | total: " .. s.buffer.i_total,
shifting_to(LEFT, s.buffer.t_total) .. "s | known_ratio:", s.buffer.i_ratio,
shifting_to(LEFT, s.buffer.t_ratio) .. "s")
end
local function is_trusted_offset(offset, axis)
local trusted_offset = s.stats.trusted_offset[axis]
for _, v in ipairs(trusted_offset) do if math.abs(offset - v) <= 1 then return true end end
return false
end
local function is_cropable()
for _, track in pairs(mp.get_property_native('track-list')) do
if track.type == 'video' and track.selected then return not track.albumart end
end
return false
end
local function filter_state(label, key, value)
local filters = mp.get_property_native("vf")
for _, filter in pairs(filters) do
if filter["label"] == label and
((not key or key ~= "graph" and filter[key] == value or key == "graph" and
string.find(filter.params.graph, value))) then return true end
end
return false
end
local function command_filter(label, command, argument, target)
if not s.f_vfcommand then
local res, reason = mp.commandv("vf-command", label, command, argument, target)
if not res and reason == "invalid parameter" then
s.f_vfcommand = true -- if mpv doesn't handle target parameter
end
end
if s.f_vfcommand then
-- fallback and send to all filters inside the graph
mp.commandv("vf-command", label, command, argument)
end
end
local function insert_cropdetect_filter(limit, change)
if s.toggled > 1 or s.paused then return end
local function insert_filter()
local cropdetect = string.format("cropdetect@dyn_cd=limit=%d/255:round=%d:reset=1", limit, options.detect_round)
if s.f_limit_runtime and change then
command_filter(labels.cropdetect, "limit", string.format("%d/255", limit), "cropdetect")
return true
elseif s.f_limit_runtime and options.read_ahead_mode > 0 then
return mp.commandv("vf", "pre",
string.format("@%s:lavfi=[split[a][b];[b]setpts=PTS-%s/TB,%s[b];%s]", labels.cropdetect,
options.read_ahead_cropdetect, cropdetect, s.f_sync))
else
return mp.commandv("vf", "pre", string.format("@%s:lavfi=[split[a][b];[b]%s,nullsink;[a]null]",
labels.cropdetect, cropdetect))
end
end
if not insert_filter() then
mp.msg.error("Does vf=help as #1 line in mvp.conf return libavfilter list with crop/cropdetect in log?")
s.f_missing = true
cleanup()
return
end
if not s.f_limit_runtime then
s.f_inserted = true -- skip process and wait for new s.collected
end
s.f_limit_change = change -- filter is updated for limit change
end
local function apply_crop(ref, pts)
-- osd size change
-- TODO add auto/smart mode
local prop_fullscreen = mp.get_property("fullscreen")
if prop_fullscreen ~= "yes" and options.fix_windowed_behavior ~= 0 then
local prop_maximized = mp.get_property("window-maximized")
local osd = mp.get_property_native("osd-dimensions")
local prop_auto_window_resize = mp.get_property("auto-window-resize")
if prop_auto_window_resize == "yes" and options.fix_windowed_behavior == 1 then
-- disable auto resize to avoid resizing at the original size of the video
mp.set_property("auto-window-resize", "no")
end
if prop_maximized ~= "yes" then
if options.fix_windowed_behavior == 2 then
mp.set_property("geometry", string.format("%s", osd.w))
elseif options.fix_windowed_behavior == 3 then
mp.set_property("geometry", string.format("x%s", osd.h))
end
end
end
-- crop filter insertion/update
if s.f_video_crop then
mp.set_property("video-crop", string.format("%sx%s+%s+%s", ref.w, ref.h, ref.x, ref.y))
elseif filter_state(labels.crop) and not s.seeking then
for _, axis in ipairs({"w", "x", "h", "y"}) do -- "w""x" then "h""y" to reduce visual glitch
if s.applied[axis] ~= ref[axis] then command_filter(labels.crop, axis, ref[axis], "crop") end
end
else
mp.commandv("vf", "append", string.format("@%s:lavfi-crop=%s", labels.crop, ref.whxy))
end
ref.applied = ref.applied + 1
s.applied = ref
print_debug("pre_format", string.format("- Apply: %s", ref.whxy))
if options.debug and pts then table.insert(s.stats.indexed_applied, {ref = ref, pts = pts}) end
end
local function compute_metadata(meta)
meta.whxy = string.format("w=%s:h=%s:x=%s:y=%s", meta.w, meta.h, meta.x, meta.y)
meta.offset = {x = meta.x - (s.source.w - meta.w) / 2, y = meta.y - (s.source.h - meta.h) / 2}
meta.mt = meta.y
meta.mb = s.source.h - meta.h - meta.y
meta.ml = meta.x
meta.mr = s.source.w - meta.w - meta.x
meta.is_source = meta.whxy == s.source.whxy
meta.is_invalid = meta.h < 0 or meta.w < 0
meta.is_trusted_offsets = is_trusted_offset(meta.offset.x, "x") and is_trusted_offset(meta.offset.y, "y")
meta.time = {buffer = 0, overall = 0}
if options.read_ahead_mode > 0 then meta.pts = {} end
local margin = options.detect_round * options.linked_tolerance
meta.is_linked_to_source = meta.mt <= margin and meta.mb <= margin or meta.ml <= margin and meta.mr <= margin
if meta.is_linked_to_source and not meta.is_invalid and s.ratios.w[meta.w] or s.ratios.h[meta.h] then
meta.is_known_ratio = true
end
return meta
end
local function generate_ratios(list)
for ratio in string.gmatch(list, "%S+%s?") do
for a, b in string.gmatch(tostring(ratio), "(%d+)/(%d+)") do ratio = a / b end
local w, h = math.floor((s.source.h * ratio)), math.floor((s.source.w / ratio))
local margin = options.ratio_tolerance
for k, v in pairs({w = w, h = h}) do
if v < s.source[k] - options.linked_tolerance then
if v % 2 == 1 then
s.ratios[k][v + 1], s.ratios[k][v - 1] = true, true
if margin > 0 then
s.ratios[k][v + 1 + margin], s.ratios[k][v - 1 - margin] = true, true
end
else
s.ratios[k][v] = true
if margin > 0 then s.ratios[k][v + margin], s.ratios[k][v - margin] = true, true end
end
end
end
end
end
local function switch_hwdec(id, hwdec, error)
if hwdec ~= "no" and not string.match(hwdec, "-copy") then
local msg = "Switch to SW decoding or HW -copy variant."
mp.msg.info(msg)
mp.osd_message(string.format("%s: %s", label_prefix, msg), 5)
end
if s.hwdec and hwdec ~= s.hwdec and s.hwdec ~= "no" and not string.match(s.hwdec, "-copy") and
filter_state(labels.cropdetect) then mp.commandv("vf", "remove", string.format("@%s", labels.cropdetect)) end
s.hwdec = hwdec
end
local function process_metadata(collected, timestamp, elapsed_time)
s.in_progress = true -- prevent event race
print_debug("metadata", collected, "Collected")
local function cleanup_stat(whxy, ref, ref_i, index)
if ref[whxy] then
ref[whxy] = nil
ref_i[index] = ref_i[index] - 1
end
end
-- buffer: init
if not s.stats.buffer[collected.whxy] then
s.stats.buffer[collected.whxy] = collected
s.stats.buffer_unique = s.stats.buffer_unique + 1
end
-- buffer: add collected or increase it's timer
if s.buffer.i_total == 0 or s.buffer.indexed_list[s.buffer.i_total].ref ~= collected then
s.buffer.i_total = s.buffer.i_total + 1
s.buffer.i_ratio = s.buffer.i_ratio + 1
s.buffer.indexed_list[s.buffer.i_total] = {ref = collected, pts = timestamp, t_elapsed = elapsed_time}
if options.read_ahead_mode > 0 then table.insert(collected.pts, timestamp) end
elseif s.last_collected == collected then
s.buffer.indexed_list[s.buffer.i_total].t_elapsed = s.buffer.indexed_list[s.buffer.i_total].t_elapsed +
elapsed_time
end
collected.time.overall = collected.time.overall + elapsed_time
collected.time.buffer = collected.time.buffer + elapsed_time
s.buffer.t_total = s.buffer.t_total + elapsed_time
if s.buffer.i_ratio > 0 then s.buffer.t_ratio = s.buffer.t_ratio + elapsed_time end
-- candidate offset/fallback to later extend buffer size
if not s.stats.trusted[collected.whxy] and collected.time.buffer > options.ratio_timer and
collected.is_linked_to_source then
if not s.candidate.offset[collected.whxy] and not collected.is_trusted_offsets and collected.is_known_ratio then
s.candidate.offset[collected.whxy] = collected
s.candidate.i_offset = s.candidate.i_offset + 1
elseif not collected.is_known_ratio and not s.candidate.offset[collected.whxy] and
not s.candidate.fallback[collected.whxy] then
s.candidate.fallback[collected.whxy] = collected
s.candidate.i_fallback = s.candidate.i_fallback + 1
end
end
-- add new fallback ratio to the ratio list
if s.candidate.fallback[collected.whxy] and collected.time.buffer >= options.fallback_timer then
-- TODO eventually re-check the buffer list with new ratio
generate_ratios(collected.w .. "/" .. collected.h)
collected.is_known_ratio = true
cleanup_stat(collected.whxy, s.candidate.fallback, s.candidate, "i_fallback")
end
-- add new offset to the trusted_offsets list
if s.candidate.offset[collected.whxy] and collected.is_known_ratio and collected.is_linked_to_source and
collected.time.buffer >= options.offset_timer then
for _, axis in ipairs({"x", "y"}) do
if not is_trusted_offset(collected.offset[axis], axis) then
table.insert(s.stats.trusted_offset[axis], collected.offset[axis])
end
end
cleanup_stat(collected.whxy, s.candidate.offset, s.candidate, "i_offset")
collected.is_trusted_offsets = true
end
-- add collected ready to the trusted list
local new_ready =
not s.stats.trusted[collected.whxy] and collected.is_trusted_offsets and not collected.is_invalid and
collected.is_linked_to_source and collected.is_known_ratio and collected.time.buffer >= options.ratio_timer
if new_ready then
s.stats.trusted[collected.whxy] = collected
s.stats.trusted_unique = s.stats.trusted_unique + 1
collected.applied = 0
collected.time.accumulated = collected.time.buffer
end
-- use current as main metadata, override by corrected or stabilized if needed
local current = collected
-- correction with trusted metadata for fast change in dark/ambiguous scene
local corrected = {}
if not current.is_invalid and s.stats.trusted_unique > 1 and not s.stats.trusted[current.whxy] then
-- is_bigger than applied meta
corrected.is_bigger = current.mt < s.approved.mt or current.mb < s.approved.mb or current.ml < s.approved.ml or
current.mr < s.approved.mr
-- find closest trusted metadata
local closest = {}
local margin = options.detect_round * options.linked_tolerance
for _, ref in pairs(s.stats.trusted) do
local diff = {ref = ref, vs_current = 0, vs_applied = 0, total = 0}
for _, side in ipairs({"mt", "mb", "ml", "mr"}) do
diff[side] = current[side] - ref[side]
diff.total = diff.total + math.abs(diff[side])
if diff[side] > margin or diff[side] < -margin then diff.vs_current = diff.vs_current + 1 end
if ref[side] ~= s.approved[side] then diff.vs_applied = diff.vs_applied + 1 end
end
-- is_inside this trusted meta with tiny tolerance for being outside
diff.is_inside = not (diff.mt < -margin or diff.mb < -margin or diff.ml < -margin or diff.mr < -margin)
local pattern = diff.is_inside and
(diff.vs_current <= 1 or diff.vs_current == 2 and diff.vs_applied <= 2 or
diff.vs_current > 2 and corrected.is_bigger)
local set = closest.ref and
(diff.vs_current < closest.vs_current or diff.vs_current == closest.vs_current and
diff.vs_applied < closest.vs_applied or diff.vs_current == closest.vs_current and
diff.vs_applied == closest.vs_applied and diff.total < closest.total)
-- mp.msg.info(string.format("\\ %-5s %-29s curr:%s appl:%s | %-3s %-3s %-3s %-3s %-4s | is_in:%s ",
-- pattern and (not closest.ref or set), ref.whxy, diff.vs_current, diff.vs_applied, diff.mt, diff.mb,
-- diff.ml, diff.mr, diff.total, diff.is_inside))
if pattern and (not closest.ref or set) then closest = diff end
end
-- replace current with corrected
if closest.ref then
current = closest.ref
corrected.ref = closest.ref
s.buffer.indexed_list[s.buffer.i_total].new_ref = current
print_debug("metadata", current, "\\ Corrected")
else
print_debug("pre_format", "\\ Uncorrected")
end
end
-- stabilization of odd/unstable meta
local stabilized
if options.detect_round <= 4 and s.stats.trusted[current.whxy] then
local margin = options.detect_round * 4
local applied_in_margin = math.abs(current.w - s.approved.w) <= margin and math.abs(current.h - s.approved.h) <=
margin
for _, ref in pairs(s.stats.trusted) do
local in_margin = math.abs(current.w - ref.w) <= margin and math.abs(current.h - ref.h) <= margin
if in_margin then
local gt_applied = applied_in_margin and ref ~= s.approved and ref.time.overall >
s.approved.time.overall * 2
local applied_gt = applied_in_margin and ref == s.approved and ref.time.overall * 2 >
current.time.overall
local pattern = not applied_in_margin and ref.time.overall > current.time.overall or gt_applied or
applied_gt
local set = stabilized and ref.time.overall > stabilized.time.overall
-- mp.msg.info("\\", ref.whxy, ref.time.overall, current.time.overall, s.approved.time.overall)
if ref ~= current and pattern and (not stabilized or set) then stabilized = ref end
end
end
if stabilized then
current = stabilized
s.buffer.indexed_list[s.buffer.i_total].new_ref = current
print_debug("metadata", current, "\\ Stabilized")
end
end
-- cycle time.accumulated for fast_change_timer (reset if uncorrected)
for whxy, ref in pairs(s.stats.trusted) do
ref.time.accumulated = whxy ~= current.whxy and 0 or ref.time.accumulated < 0 and 0 + elapsed_time or
not new_ready and ref.time.accumulated + elapsed_time or ref.time.accumulated
end
-- crop: final validation then store or apply it
local detect_source = current == s.last_current and (current.is_source or collected.is_source) and s.limit.target >=
0
local confirmation = not current.is_source and s.stats.trusted[current.whxy] and current.time.accumulated >=
options.fast_change_timer and (not corrected.ref or current == s.last_current)
local crop_filter = s.approved ~= current and (confirmation or detect_source)
if crop_filter and (not s.timestamps.prevent or timestamp >= s.timestamps.prevent) then
s.approved = current -- reflect s.applied for read_head
if s.limit.current < s.limit.min then
s.limit.min = s.limit.current -- store minimum limit
end
if s.f_limit_runtime and options.read_ahead_mode > 0 then
local pts = current.time.accumulated < options.ratio_timer and timestamp - current.time.accumulated or
current.pts[1]
table.insert(s.indexed_read_ahead, {ref = current, pts = pts})
s.timestamps.read_ahead = nil
else
apply_crop(current, timestamp)
end
if options.prevent_change_mode > 0 then
s.timestamps.prevent = nil
if (options.prevent_change_mode == 1 and (current.w > s.approved.w or current.h > s.approved.h) or
options.prevent_change_mode == 2 and (current.w < s.approved.w or current.h < s.approved.h) or
options.prevent_change_mode == 3) then
s.timestamps.prevent = timestamp + options.prevent_change_timer
end
end
if options.mode <= 2 then on_toggle(true) end
end
local function is_time_to_cleanup_buffer(time, target_time)
return time > target_time * (1 + options.segmentation)
end
-- buffer: reduce size of known ratio stats
while is_time_to_cleanup_buffer(s.buffer.t_ratio, options.ratio_timer) do
local i = (s.buffer.i_total + 1) - s.buffer.i_ratio
s.buffer.t_ratio = s.buffer.t_ratio - s.buffer.indexed_list[i].t_elapsed
s.buffer.i_ratio = s.buffer.i_ratio - 1
end
-- buffer: check for candidate to extend it
local buffer_timer = s.candidate.i_offset > 0 and options.offset_timer or s.candidate.i_fallback > 0 and
options.fallback_timer or options.ratio_timer
-- buffer: cleanup fake candidate
local function is_proactive_cleanup_needed()
local test
if is_time_to_cleanup_buffer(s.buffer.t_total, options.ratio_timer) then
for _, cat in ipairs({"offset", "fallback"}) do
if s.candidate["i_" .. cat] > 0 then
test = true
for whxy, ref in pairs(s.candidate[cat]) do
if ref.time.buffer > s.buffer.t_total * options.reverse_segmentation then
return false -- if at least one is a proper candidate
end
end
end
end
end
return test
end
-- buffer: reduce total size
while is_time_to_cleanup_buffer(s.buffer.t_total, buffer_timer) or is_proactive_cleanup_needed() do
s.buffer.i_to_shift = s.buffer.i_to_shift + 1
local entry = s.buffer.indexed_list[s.buffer.i_to_shift]
entry.ref.time.buffer = entry.ref.time.buffer - entry.t_elapsed
if options.read_ahead_mode > 0 then table.remove(entry.ref.pts, 1) end
if s.stats.buffer[entry.ref.whxy] and entry.ref.time.buffer == 0 then
cleanup_stat(entry.ref.whxy, s.stats.buffer, s.stats, "buffer_unique")
cleanup_stat(entry.ref.whxy, s.candidate.offset, s.candidate, "i_offset")
cleanup_stat(entry.ref.whxy, s.candidate.fallback, s.candidate, "i_fallback")
end
s.buffer.t_total = s.buffer.t_total - entry.t_elapsed
end
-- buffer: shift the list to overwrite unused data
if s.buffer.i_to_shift >= 20 or s.buffer.i_to_shift == s.buffer.i_total then
for i = s.buffer.i_to_shift + 1, s.buffer.i_total do
s.buffer.indexed_list[i - s.buffer.i_to_shift] = s.buffer.indexed_list[i]
end
for i = 0, s.buffer.i_to_shift - 1 do s.buffer.indexed_list[s.buffer.i_total - i] = nil end
s.buffer.i_total = s.buffer.i_total - s.buffer.i_to_shift
s.buffer.i_to_shift = 0
collectgarbage("step")
end
-- limit: automatic adjustment
s.last_limit = s.limit.current
if s.f_limit_runtime or timestamp >= s.limit.timer then
s.limit.last_target = s.limit.target
if collected.is_source or current.is_source or corrected.is_bigger then
-- increase limit
s.limit.target = 1
if s.limit.current + s.limit.step * s.limit.up <= options.detect_limit then
s.limit.current = s.limit.current + s.limit.step * s.limit.up
else
s.limit.current = options.detect_limit
end
elseif not current.is_invalid and
(collected.is_trusted_offsets or collected == s.last_collected or current == s.last_current) then
-- stable limit
s.limit.target = 0
-- reset limit to help with different dark color
if not current.is_trusted_offsets then s.limit.current = options.detect_limit end
elseif s.limit.current > 0 then
-- decrease limit
s.limit.target = -1
if s.limit.min < s.limit.current and s.limit.last_target == -1 then
s.limit.current = s.limit.min
elseif s.limit.current - s.limit.step >= 0 then
s.limit.current = s.limit.current - s.limit.step
else
s.limit.current = 0
end
end
end
-- store for next process
s.last_current = current
s.last_collected = collected
s.last_timestamp = timestamp
-- limit: apply change
if s.last_limit ~= s.limit.current then
if not s.f_limit_runtime and options.limit_timer > 0 then s.limit.timer = timestamp + options.limit_timer end
s.limit.counter = s.limit.counter + 1
insert_cropdetect_filter(s.limit.current, true)
end
s.in_progress = false
end
local function time_pos(event, value, err)
if value and s.indexed_read_ahead[1] then
local time_pos = shifting_to(RIGHT, value)
local deviation = math.abs(time_pos - s.pts)
local crop_sync = s.frametime * (options.read_ahead_sync + options.crop_method_sync)
local time_pos_read_ahead = time_pos - (options.read_ahead_timer - deviation - crop_sync)
if time_pos_read_ahead >= s.indexed_read_ahead[1].pts then
apply_crop(s.indexed_read_ahead[1].ref, s.indexed_read_ahead[1].pts)
table.remove(s.indexed_read_ahead, 1)
end
end
end
local function collect_metadata(event)
if event.prefix == "ffmpeg" and event.level == "v" and string.find(event.text, "^.*dyn_cd: ") and
not (s.seeking or s.paused or s.toggled > 1) then
local tmp = {}
for k, v in string.gmatch(event.text, "(%w+):(%-?%d+%.?%d* )") do tmp[k] = tonumber(v) end
tmp.whxy = string.format("w=%d:h=%d:x=%d:y=%d", tmp.w, tmp.h, tmp.x, tmp.y)
s.pts = shifting_to(LEFT, tmp.pts)
if tmp.whxy ~= s.collected.whxy then
s.collected = s.stats.trusted[tmp.whxy] or s.stats.buffer[tmp.whxy] or compute_metadata(tmp)
end
s.limit.last_collect = s.limit.collect
s.limit.collect = tmp.limit or s.limit.collect
s.f_limit_runtime = tmp.limit ~= nil -- if ffmpeg is patch for limit change at runtime
s.timestamps.previous = s.timestamps.current
s.timestamps.current = s.pts
local wait_limit = s.f_limit_runtime and s.f_limit_change and s.limit.collect == s.limit.last_collect
if not wait_limit then s.f_limit_change = false end
if s.in_progress or not s.timestamps.previous or wait_limit or s.f_inserted or s.timestamps.current <
options.start_delay then
s.f_inserted = false
return
end
local elapsed_time = s.timestamps.current - s.timestamps.previous
if not s.frametime or elapsed_time < s.frametime and elapsed_time > 0 then s.frametime = elapsed_time end
process_metadata(s.collected, s.timestamps.current, elapsed_time)
end
end
local function seek(event)
if s.seek_done then return end
print_debug("pre_format", string.format("Stop by %s event.", event))
if event == "seek" or event == "toggle" then
s.timestamps = {}
s.limit.timer = 0
s.approved = s.applied -- re-sync
if event == "seek" then
if s.f_limit_runtime then insert_cropdetect_filter(s.limit.current) end
if not s.f_video_crop and
(filter_state(labels.crop, "enabled", true) or not filter_state(labels.crop) and s.applied ~= s.source) then
apply_crop(s.applied)
end
end
if s.f_limit_runtime then
s.indexed_read_ahead = {}
s.collected = {}
end
s.seek_done = true -- avoid seek() in loop until we resume()
end
end
local function resume(event)
s.seek_done = false
print_debug("pre_format", string.format("Resume by %s event.", event))
if event == "toggle" and s.f_limit_runtime or not filter_state(labels.cropdetect) then
insert_cropdetect_filter(s.limit.current)
end
end
local function playback_events(t, id, error)
if t.event == "seek" then
s.seeking = true
seek(t.event)
else
if not s.paused then resume(t.event) end
s.seeking = false
end
end
local ENABLE, DISABLE_WITH_CROP, DISABLE = 1, 2, 3
function on_toggle(auto)
if s.f_missing then
mp.osd_message("Libavfilter cropdetect missing", 3)
return
end
local EVENT = "toggle"
if s.toggled == ENABLE then
s.toggled = DISABLE_WITH_CROP
if filter_state(labels.cropdetect, "enabled", true) then
mp.commandv("vf", EVENT, string.format("@%s", labels.cropdetect))
end
seek(EVENT)
if not auto then mp.osd_message(string.format("%s: disabled, crop remains.", label_prefix), 3) end
elseif s.toggled == DISABLE_WITH_CROP then
s.toggled = DISABLE
if filter_state(labels.cropdetect, "enabled", false) then
if s.f_video_crop then
mp.set_property("video-crop", "")
elseif filter_state(labels.crop, "enabled", true) then
mp.commandv("vf", EVENT, string.format("@%s", labels.crop))
end
end
if not auto then mp.osd_message(string.format("%s: crop removed.", label_prefix), 3) end
else -- s.toggled == DISABLE
s.toggled = ENABLE
if filter_state(labels.cropdetect, "enabled", false) then
mp.commandv("vf", EVENT, string.format("@%s", labels.cropdetect))
end
if s.f_video_crop then
apply_crop(s.applied)
elseif filter_state(labels.crop, "enabled", false) then
mp.commandv("vf", EVENT, string.format("@%s", labels.crop))
end
resume(EVENT)
if not auto then mp.osd_message(string.format("%s: enabled.", label_prefix), 3) end
end
end
local function pause(event, is_paused)
s.paused = is_paused
if is_paused then
seek(event)
print_stats()
print_debug("buffer")
print_debug("applied")
print_debug("pre_format", "s.approved: " .. s.approved.whxy)
print_debug("pre_format", "s.applied: " .. s.applied.whxy)
if s.indexed_read_ahead[1] then
print_debug("pre_format", "s.indexed_read_ahead[1]: " .. s.indexed_read_ahead[1].ref.whxy)
end
else
if s.toggled == 1 then resume(event) end
end
end
function cleanup()
if not s.started then return end
if not s.paused then print_stats() end
mp.msg.info("Cleanup...")
mp.set_property("auto-window-resize", s.user_auto_window_resize)
mp.unregister_event(playback_events)
mp.unregister_event(collect_metadata)
mp.unobserve_property(time_pos)
mp.unobserve_property(switch_hwdec)
mp.unobserve_property(pause)
for _, label in pairs(labels) do
if filter_state(label) then mp.commandv("vf", "remove", string.format("@%s", label)) end
end
if s.f_video_crop then mp.set_property("video-crop", "") end
mp.msg.info("Done.")
s.started = false
end
local function on_start()
mp.msg.info("File loaded.")
if not is_cropable() then
mp.msg.warn("Exit, only works for videos.")
return
end
s.user_geometry = mp.get_property("geometry")
s.user_auto_window_resize = mp.get_property("auto-window-resize")
if options.fix_windowed_behavior == 1 and s.user_auto_window_resize == "yes" then
mp.set_property("auto-window-resize", "no")
end
-- init/re-init stored data
s.buffer = {i_to_shift = 0, i_total = 0, i_ratio = 0, indexed_list = {}, t_total = 0, t_ratio = 0}
s.candidate = {i_fallback = 0, i_offset = 0, fallback = {}, offset = {}}
s.collected = {}
s.indexed_read_ahead = {}
s.limit = {
counter = 0, current = options.detect_limit, min = options.detect_limit, step = 2, target = 0, timer = 0, up = 2
}
s.stats = {applied = {}, buffer = {}, buffer_unique = 0, trusted = {}, trusted_offset = {}, trusted_unique = 1}
s.stats.indexed_applied = {}
s.source = {w_untouched = mp.get_property_number("width"), h_untouched = mp.get_property_number("height")}
s.source.w = math.floor(s.source.w_untouched / options.detect_round) * options.detect_round
s.source.h = math.floor(s.source.h_untouched / options.detect_round) * options.detect_round
s.source.x = math.floor((s.source.w_untouched - s.source.w) / 2)
s.source.y = math.floor((s.source.h_untouched - s.source.h) / 2)
s.stats.trusted_offset = {x = {s.source.x}, y = {s.source.y}}
s.ratios = {w = {}, h = {}}
generate_ratios(options.ratios)
s.source = compute_metadata(s.source)
s.stats.trusted[s.source.whxy] = s.source
s.source.applied = 1
s.source.time.accumulated = 0
s.applied = s.source
s.approved = s.source
s.timestamps = {}
if options.read_ahead_mode > 0 then
-- assume cropdetect is patch for command "limit", fallback at the first collected metadata otherwise.
s.f_limit_runtime = true
-- quick test for dummysync filter
s.f_sync = mp.commandv("vf", "add", string.format("@%s:lavfi=[split[a][b];[a][b]dummysync]", label_prefix)) and
mp.commandv("vf", "remove", string.format("@%s", label_prefix)) and "[a][b]dummysync" or
"[a][b]psnr=eof_action=pass"
end
s.f_video_crop = options.crop_method == 1 and mp.get_property("video-crop") ~= nil -- true if supported
-- register events
mp.register_event("seek", playback_events)
mp.register_event("playback-restart", playback_events)
mp.observe_property("time-pos", "number", time_pos)
mp.observe_property("hwdec", "string", switch_hwdec)
mp.observe_property("pause", "bool", pause)
mp.enable_messages('v')
mp.register_event("log-message", collect_metadata)
s.toggled = (options.mode % 2 == 1) and DISABLE or ENABLE
s.started = true -- everything ready
end
mp.add_key_binding("C", "toggle_crop", on_toggle)
mp.register_event("end-file", cleanup)
mp.register_event("file-loaded", on_start)

View File

@@ -0,0 +1 @@
/usr/share/mpv/scripts/gamma-auto.lua

1
.config/mpv/scripts/idet.sh Symbolic link
View File

@@ -0,0 +1 @@
/usr/share/mpv/scripts/idet.sh

View File

@@ -0,0 +1 @@
/usr/lib/mpv-mpris/mpris.so

View File

@@ -0,0 +1 @@
/usr/share/mpv/scripts/mpv_identify.sh

View File

@@ -0,0 +1 @@
/usr/share/mpv/scripts/observe-all.lua

View File

@@ -0,0 +1 @@
/usr/share/mpv/scripts/ontop-playback.lua

View File

@@ -0,0 +1 @@
/usr/share/mpv/scripts/pause-when-minimize.lua

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1 @@
/usr/share/mpv/scripts/skip-logo.lua

View File

@@ -0,0 +1,569 @@
-- sponsorblock.lua
--
-- This script skips sponsored segments of YouTube videos
-- using data from https://github.com/ajayyy/SponsorBlock
local ON_WINDOWS = package.config:sub(1,1) ~= "/"
local options = {
server_address = "https://sponsor.ajay.app",
python_path = ON_WINDOWS and "python" or "python3",
-- Categories to fetch
categories = "sponsor,intro,outro,interaction,selfpromo,filler",
-- Categories to skip automatically
skip_categories = "sponsor",
-- If true, sponsored segments will only be skipped once
skip_once = true,
-- Note that sponsored segments may ocasionally be inaccurate if this is turned off
-- see https://blog.ajay.app/voting-and-pseudo-randomness-or-sponsorblock-or-youtube-sponsorship-segment-blocker
local_database = false,
-- Update database on first run, does nothing if local_database is false
auto_update = true,
-- How long to wait between local database updates
-- Format: "X[d,h,m]", leave blank to update on every mpv run
auto_update_interval = "6h",
-- User ID used to submit sponsored segments, leave blank for random
user_id = "",
-- Name to display on the stats page https://sponsor.ajay.app/stats/ leave blank to keep current name
display_name = "",
-- Tell the server when a skip happens
report_views = true,
-- Auto upvote skipped sponsors
auto_upvote = false,
-- Use sponsor times from server if they're more up to date than our local database
server_fallback = true,
-- Create chapters at sponsor boundaries for OSC display and manual skipping
make_chapters = true,
-- Minimum duration for sponsors (in seconds), segments under that threshold will be ignored
min_duration = 1,
-- Fade audio for smoother transitions
audio_fade = false,
-- Audio fade step, applied once every 100ms until cap is reached
audio_fade_step = 10,
-- Audio fade cap
audio_fade_cap = 0,
-- Fast forward through sponsors instead of skipping
fast_forward = false,
-- Playback speed modifier when fast forwarding, applied once every second until cap is reached
fast_forward_increase = .2,
-- Playback speed cap
fast_forward_cap = 2,
-- Length of the sha256 prefix (3-32) when querying server, 0 to disable
sha256_length = 4,
-- Pattern for video id in local files, ignored if blank
-- Recommended value for base youtube-dl is "-([%w-_]+)%.[mw][kpe][v4b]m?$"
local_pattern = "",
-- Legacy option, use skip_categories instead
skip = true
}
mp.options = require "mp.options"
mp.options.read_options(options, "sponsorblock")
local legacy = mp.command_native_async == nil
--[[
if legacy then
options.local_database = false
end
--]]
options.local_database = false
local utils = require "mp.utils"
scripts_dir = mp.find_config_file("scripts")
local sponsorblock = utils.join_path(scripts_dir, "sponsorblock_shared/sponsorblock.py")
local uid_path = utils.join_path(scripts_dir, "sponsorblock_shared/sponsorblock.txt")
local database_file = options.local_database and utils.join_path(scripts_dir, "sponsorblock_shared/sponsorblock.db") or ""
local youtube_id = nil
local ranges = {}
local init = false
local segment = {a = 0, b = 0, progress = 0, first = true}
local retrying = false
local last_skip = {uuid = "", dir = nil}
local speed_timer = nil
local fade_timer = nil
local fade_dir = nil
local volume_before = mp.get_property_number("volume")
local categories = {}
local all_categories = {"sponsor", "intro", "outro", "interaction", "selfpromo", "preview", "music_offtopic", "filler"}
local chapter_cache = {}
for category in string.gmatch(options.skip_categories, "([^,]+)") do
categories[category] = true
end
function file_exists(name)
local f = io.open(name,"r")
if f ~= nil then io.close(f) return true else return false end
end
function t_count(t)
local count = 0
for _ in pairs(t) do count = count + 1 end
return count
end
function time_sort(a, b)
if a.time == b.time then
return string.match(a.title, "segment end")
end
return a.time < b.time
end
function parse_update_interval()
local s = options.auto_update_interval
if s == "" then return 0 end -- Interval Disabled
local num, mod = s:match "^(%d+)([hdm])$"
if num == nil or mod == nil then
mp.osd_message("[sponsorblock] auto_update_interval " .. s .. " is invalid", 5)
return nil
end
local time_table = {
m = 60,
h = 60 * 60,
d = 60 * 60 * 24,
}
return num * time_table[mod]
end
function clean_chapters()
local chapters = mp.get_property_native("chapter-list")
local new_chapters = {}
for _, chapter in pairs(chapters) do
if chapter.title ~= "Preview segment start" and chapter.title ~= "Preview segment end" then
table.insert(new_chapters, chapter)
end
end
mp.set_property_native("chapter-list", new_chapters)
end
function create_chapter(chapter_title, chapter_time)
local chapters = mp.get_property_native("chapter-list")
local duration = mp.get_property_native("duration")
table.insert(chapters, {title=chapter_title, time=(duration == nil or duration > chapter_time) and chapter_time or duration - .001})
table.sort(chapters, time_sort)
mp.set_property_native("chapter-list", chapters)
end
function process(uuid, t, new_ranges)
start_time = tonumber(string.match(t, "[^,]+"))
end_time = tonumber(string.sub(string.match(t, ",[^,]+"), 2))
for o_uuid, o_t in pairs(ranges) do
if (start_time >= o_t.start_time and start_time <= o_t.end_time) or (o_t.start_time >= start_time and o_t.start_time <= end_time) then
new_ranges[o_uuid] = o_t
return
end
end
category = string.match(t, "[^,]+$")
if categories[category] and end_time - start_time >= options.min_duration then
new_ranges[uuid] = {
start_time = start_time,
end_time = end_time,
category = category,
skipped = false
}
end
if options.make_chapters and not chapter_cache[uuid] then
chapter_cache[uuid] = true
local category_title = (category:gsub("^%l", string.upper):gsub("_", " "))
create_chapter(category_title .. " segment start (" .. string.sub(uuid, 1, 6) .. ")", start_time)
create_chapter(category_title .. " segment end (" .. string.sub(uuid, 1, 6) .. ")", end_time)
end
end
function getranges(_, exists, db, more)
if type(exists) == "table" and exists["status"] == "1" then
if options.server_fallback then
mp.add_timeout(0, function() getranges(true, true, "") end)
else
return mp.osd_message("[sponsorblock] database update failed, gave up")
end
end
if db ~= "" and db ~= database_file then db = database_file end
if exists ~= true and not file_exists(db) then
if not retrying then
mp.osd_message("[sponsorblock] database update failed, retrying...")
retrying = true
end
return update()
end
if retrying then
mp.osd_message("[sponsorblock] database update succeeded")
retrying = false
end
local sponsors
local args = {
options.python_path,
sponsorblock,
"ranges",
db,
options.server_address,
youtube_id,
options.categories,
tostring(options.sha256_length)
}
if not legacy then
sponsors = mp.command_native({name = "subprocess", capture_stdout = true, playback_only = false, args = args})
else
sponsors = utils.subprocess({args = args})
end
mp.msg.debug("Got: " .. string.gsub(sponsors.stdout, "[\n\r]", ""))
if not string.match(sponsors.stdout, "^%s*(.*%S)") then return end
if string.match(sponsors.stdout, "error") then return getranges(true, true) end
local new_ranges = {}
local r_count = 0
if more then r_count = -1 end
for t in string.gmatch(sponsors.stdout, "[^:%s]+") do
uuid = string.match(t, "([^,]+),[^,]+$")
if ranges[uuid] then
new_ranges[uuid] = ranges[uuid]
else
process(uuid, t, new_ranges)
end
r_count = r_count + 1
end
local c_count = t_count(ranges)
if c_count == 0 or r_count >= c_count then
ranges = new_ranges
end
end
function fast_forward()
if options.fast_forward and options.fast_forward == true then
speed_timer = nil
mp.set_property("speed", 1)
end
local last_speed = mp.get_property_number("speed")
local new_speed = math.min(last_speed + options.fast_forward_increase, options.fast_forward_cap)
if new_speed <= last_speed then return end
mp.set_property("speed", new_speed)
end
function fade_audio(step)
local last_volume = mp.get_property_number("volume")
local new_volume = math.max(options.audio_fade_cap, math.min(last_volume + step, volume_before))
if new_volume == last_volume then
if step >= 0 then fade_dir = nil end
if fade_timer ~= nil then fade_timer:kill() end
fade_timer = nil
return
end
mp.set_property("volume", new_volume)
end
function skip_ads(name, pos)
if pos == nil then return end
local sponsor_ahead = false
for uuid, t in pairs(ranges) do
if (options.fast_forward == uuid or not options.skip_once or not t.skipped) and t.start_time <= pos and t.end_time > pos then
if options.fast_forward == uuid then return end
if options.fast_forward == false then
mp.osd_message("[sponsorblock] " .. t.category .. " skipped")
mp.set_property("time-pos", t.end_time)
else
mp.osd_message("[sponsorblock] skipping " .. t.category)
end
t.skipped = true
last_skip = {uuid = uuid, dir = nil}
if options.report_views or options.auto_upvote then
local args = {
options.python_path,
sponsorblock,
"stats",
database_file,
options.server_address,
youtube_id,
uuid,
options.report_views and "1" or "",
uid_path,
options.user_id,
options.auto_upvote and "1" or ""
}
if not legacy then
mp.command_native_async({name = "subprocess", playback_only = false, args = args}, function () end)
else
utils.subprocess_detached({args = args})
end
end
if options.fast_forward ~= false then
options.fast_forward = uuid
if speed_timer ~= nil then speed_timer:kill() end
speed_timer = mp.add_periodic_timer(1, fast_forward)
end
return
elseif (not options.skip_once or not t.skipped) and t.start_time <= pos + 1 and t.end_time > pos + 1 then
sponsor_ahead = true
end
end
if options.audio_fade then
if sponsor_ahead then
if fade_dir ~= false then
if fade_dir == nil then volume_before = mp.get_property_number("volume") end
if fade_timer ~= nil then fade_timer:kill() end
fade_dir = false
fade_timer = mp.add_periodic_timer(.1, function() fade_audio(-options.audio_fade_step) end)
end
elseif fade_dir == false then
fade_dir = true
if fade_timer ~= nil then fade_timer:kill() end
fade_timer = mp.add_periodic_timer(.1, function() fade_audio(options.audio_fade_step) end)
end
end
if options.fast_forward and options.fast_forward ~= true then
options.fast_forward = true
speed_timer:kill()
speed_timer = nil
mp.set_property("speed", 1)
end
end
function vote(dir)
if last_skip.uuid == "" then return mp.osd_message("[sponsorblock] no sponsors skipped, can't submit vote") end
local updown = dir == "1" and "up" or "down"
if last_skip.dir == dir then return mp.osd_message("[sponsorblock] " .. updown .. "vote already submitted") end
last_skip.dir = dir
local args = {
options.python_path,
sponsorblock,
"stats",
database_file,
options.server_address,
youtube_id,
last_skip.uuid,
"",
uid_path,
options.user_id,
dir
}
if not legacy then
mp.command_native_async({name = "subprocess", playback_only = false, args = args}, function () end)
else
utils.subprocess({args = args})
end
mp.osd_message("[sponsorblock] " .. updown .. "vote submitted")
end
function update()
mp.command_native_async({name = "subprocess", playback_only = false, args = {
options.python_path,
sponsorblock,
"update",
database_file,
options.server_address
}}, getranges)
end
function file_loaded()
local initialized = init
ranges = {}
segment = {a = 0, b = 0, progress = 0, first = true}
last_skip = {uuid = "", dir = nil}
chapter_cache = {}
local video_path = mp.get_property("path", "")
mp.msg.debug("Path: " .. video_path)
local video_referer = string.match(mp.get_property("http-header-fields", ""), "Referer:([^,]+)") or ""
mp.msg.debug("Referer: " .. video_referer)
local urls = {
"ytdl://([%w-_]+).*",
"https?://youtu%.be/([%w-_]+).*",
"https?://w?w?w?%.?youtube%.com/v/([%w-_]+).*",
"/watch.*[?&]v=([%w-_]+).*",
"/embed/([%w-_]+).*"
}
youtube_id = nil
for i, url in ipairs(urls) do
youtube_id = youtube_id or string.match(video_path, url) or string.match(video_referer, url)
if youtube_id then break end
end
youtube_id = youtube_id or string.match(video_path, options.local_pattern)
if not youtube_id or string.len(youtube_id) < 11 or (local_pattern and string.len(youtube_id) ~= 11) then return end
youtube_id = string.sub(youtube_id, 1, 11)
mp.msg.debug("Found YouTube ID: " .. youtube_id)
init = true
if not options.local_database then
getranges(true, true)
else
local exists = file_exists(database_file)
if exists and options.server_fallback then
getranges(true, true)
mp.add_timeout(0, function() getranges(true, true, "", true) end)
elseif exists then
getranges(true, true)
elseif options.server_fallback then
mp.add_timeout(0, function() getranges(true, true, "") end)
end
end
if initialized then return end
if options.skip then
mp.observe_property("time-pos", "native", skip_ads)
end
if options.display_name ~= "" then
local args = {
options.python_path,
sponsorblock,
"username",
database_file,
options.server_address,
youtube_id,
"",
"",
uid_path,
options.user_id,
options.display_name
}
if not legacy then
mp.command_native_async({name = "subprocess", playback_only = false, args = args}, function () end)
else
utils.subprocess_detached({args = args})
end
end
if not options.local_database or (not options.auto_update and file_exists(database_file)) then return end
if file_exists(database_file) then
local db_info = utils.file_info(database_file)
local cur_time = os.time(os.date("*t"))
local upd_interval = parse_update_interval()
if upd_interval == nil or os.difftime(cur_time, db_info.mtime) < upd_interval then return end
end
update()
end
function set_segment()
if not youtube_id then return end
local pos = mp.get_property_number("time-pos")
if pos == nil then return end
if segment.progress > 1 then
segment.progress = segment.progress - 2
end
if segment.progress == 1 then
segment.progress = 0
segment.b = pos
mp.osd_message("[sponsorblock] segment boundary B set, press again for boundary A", 3)
else
segment.progress = 1
segment.a = pos
mp.osd_message("[sponsorblock] segment boundary A set, press again for boundary B", 3)
end
if options.make_chapters and not segment.first then
local start_time = math.min(segment.a, segment.b)
local end_time = math.max(segment.a, segment.b)
if end_time - start_time ~= 0 and end_time ~= 0 then
clean_chapters()
create_chapter("Preview segment start", start_time)
create_chapter("Preview segment end", end_time)
end
end
segment.first = false
end
function select_category(selected)
for category in string.gmatch(options.categories, "([^,]+)") do
mp.remove_key_binding("select_category_"..category)
mp.remove_key_binding("kp_select_category_"..category)
end
submit_segment(selected)
end
function submit_segment(category)
if not youtube_id then return end
local start_time = math.min(segment.a, segment.b)
local end_time = math.max(segment.a, segment.b)
if end_time - start_time == 0 or end_time == 0 then
mp.osd_message("[sponsorblock] empty segment, not submitting")
elseif segment.progress <= 1 then
segment.progress = segment.progress + 2
local category_list = ""
for category_id, category in pairs(all_categories) do
local category_title = (category:gsub("^%l", string.upper):gsub("_", " "))
category_list = category_list .. category_id .. ": " .. category_title .. "\n"
mp.add_forced_key_binding(tostring(category_id), "select_category_"..category, function() select_category(category) end)
mp.add_forced_key_binding("KP"..tostring(category_id), "kp_select_category_"..category, function() select_category(category) end)
end
mp.osd_message(string.format("[sponsorblock] press a number to select category for segment: %.2d:%.2d:%.2d to %.2d:%.2d:%.2d\n\n" .. category_list .. "\nyou can press Shift+G again for default (Sponsor) or hide this message with g", math.floor(start_time/(60*60)), math.floor(start_time/60%60), math.floor(start_time%60), math.floor(end_time/(60*60)), math.floor(end_time/60%60), math.floor(end_time%60)), 30)
else
mp.osd_message("[sponsorblock] submitting segment...", 30)
local submit
local args = {
options.python_path,
sponsorblock,
"submit",
database_file,
options.server_address,
youtube_id,
tostring(start_time),
tostring(end_time),
uid_path,
options.user_id,
category or "sponsor"
}
if not legacy then
submit = mp.command_native({name = "subprocess", capture_stdout = true, playback_only = false, args = args})
else
submit = utils.subprocess({args = args})
end
if string.match(submit.stdout, "success") then
segment = {a = 0, b = 0, progress = 0, first = true}
mp.osd_message("[sponsorblock] segment submitted")
if options.make_chapters then
clean_chapters()
create_chapter("Submitted segment start", start_time)
create_chapter("Submitted segment end", end_time)
end
elseif string.match(submit.stdout, "error") then
mp.osd_message("[sponsorblock] segment submission failed, server may be down. try again", 5)
elseif string.match(submit.stdout, "502") then
mp.osd_message("[sponsorblock] segment submission failed, server is down. try again", 5)
elseif string.match(submit.stdout, "400") then
mp.osd_message("[sponsorblock] segment submission failed, impossible inputs", 5)
segment = {a = 0, b = 0, progress = 0, first = true}
elseif string.match(submit.stdout, "429") then
mp.osd_message("[sponsorblock] segment submission failed, rate limited. try again", 5)
elseif string.match(submit.stdout, "409") then
mp.osd_message("[sponsorblock] segment already submitted", 3)
segment = {a = 0, b = 0, progress = 0, first = true}
else
mp.osd_message("[sponsorblock] segment submission failed", 5)
end
end
end
mp.register_event("file-loaded", file_loaded)
mp.add_key_binding("g", "set_segment", set_segment)
mp.add_key_binding("G", "submit_segment", submit_segment)
mp.add_key_binding("h", "upvote_segment", function() return vote("1") end)
mp.add_key_binding("H", "downvote_segment", function() return vote("0") end)
-- Bindings below are for backwards compatibility and could be removed at any time
mp.add_key_binding(nil, "sponsorblock_set_segment", set_segment)
mp.add_key_binding(nil, "sponsorblock_submit_segment", submit_segment)
mp.add_key_binding(nil, "sponsorblock_upvote", function() return vote("1") end)
mp.add_key_binding(nil, "sponsorblock_downvote", function() return vote("0") end)

View File

@@ -0,0 +1,3 @@
-- This is a dummy main.lua
-- required for mpv 0.33
-- do not delete

View File

@@ -0,0 +1,122 @@
import urllib.request
import urllib.parse
import hashlib
import sqlite3
import random
import string
import json
import sys
import os
if sys.argv[1] in ["submit", "stats", "username"]:
if not sys.argv[8]:
if os.path.isfile(sys.argv[7]):
with open(sys.argv[7]) as f:
uid = f.read()
else:
uid = "".join(random.choices(string.ascii_letters + string.digits, k=36))
with open(sys.argv[7], "w") as f:
f.write(uid)
else:
uid = sys.argv[8]
opener = urllib.request.build_opener()
opener.addheaders = [("User-Agent", "mpv_sponsorblock/1.0 (https://github.com/po5/mpv_sponsorblock)")]
urllib.request.install_opener(opener)
if sys.argv[1] == "ranges" and (not sys.argv[2] or not os.path.isfile(sys.argv[2])):
sha = None
if 3 <= int(sys.argv[6]) <= 32:
sha = hashlib.sha256(sys.argv[4].encode()).hexdigest()[:int(sys.argv[6])]
times = []
try:
response = urllib.request.urlopen(sys.argv[3] + "/api/skipSegments" + ("/" + sha + "?" if sha else "?videoID=" + sys.argv[4] + "&") + urllib.parse.urlencode([("categories", json.dumps(sys.argv[5].split(",")))]))
segments = json.load(response)
for segment in segments:
if sha and sys.argv[4] != segment["videoID"]:
continue
if sha:
for s in segment["segments"]:
times.append(str(s["segment"][0]) + "," + str(s["segment"][1]) + "," + s["UUID"] + "," + s["category"])
else:
times.append(str(segment["segment"][0]) + "," + str(segment["segment"][1]) + "," + segment["UUID"] + "," + segment["category"])
print(":".join(times))
except (TimeoutError, urllib.error.URLError) as e:
print("error")
except urllib.error.HTTPError as e:
if e.code == 404:
print("")
else:
print("error")
elif sys.argv[1] == "ranges":
conn = sqlite3.connect(sys.argv[2])
conn.row_factory = sqlite3.Row
c = conn.cursor()
times = []
for category in sys.argv[5].split(","):
c.execute("SELECT startTime, endTime, votes, UUID, category FROM sponsorTimes WHERE videoID = ? AND shadowHidden = 0 AND votes > -1 AND category = ?", (sys.argv[4], category))
sponsors = c.fetchall()
best = list(sponsors)
dealtwith = []
similar = []
for sponsor_a in sponsors:
for sponsor_b in sponsors:
if sponsor_a is not sponsor_b and sponsor_a["startTime"] >= sponsor_b["startTime"] and sponsor_a["startTime"] <= sponsor_b["endTime"]:
similar.append([sponsor_a, sponsor_b])
if sponsor_a in best:
best.remove(sponsor_a)
if sponsor_b in best:
best.remove(sponsor_b)
for sponsors_a in similar:
if sponsors_a in dealtwith:
continue
group = set(sponsors_a)
for sponsors_b in similar:
if sponsors_b[0] in group or sponsors_b[1] in group:
group.add(sponsors_b[0])
group.add(sponsors_b[1])
dealtwith.append(sponsors_b)
best.append(max(group, key=lambda x:x["votes"]))
for time in best:
times.append(str(time["startTime"]) + "," + str(time["endTime"]) + "," + time["UUID"] + "," + time["category"])
print(":".join(times))
elif sys.argv[1] == "update":
try:
urllib.request.urlretrieve(sys.argv[3] + "/database.db", sys.argv[2] + ".tmp")
os.replace(sys.argv[2] + ".tmp", sys.argv[2])
except PermissionError:
print("database update failed, file currently in use", file=sys.stderr)
sys.exit(1)
except ConnectionResetError:
print("database update failed, connection reset", file=sys.stderr)
sys.exit(1)
except TimeoutError:
print("database update failed, timed out", file=sys.stderr)
sys.exit(1)
except urllib.error.URLError:
print("database update failed", file=sys.stderr)
sys.exit(1)
elif sys.argv[1] == "submit":
try:
req = urllib.request.Request(sys.argv[3] + "/api/skipSegments", data=json.dumps({"videoID": sys.argv[4], "segments": [{"segment": [float(sys.argv[5]), float(sys.argv[6])], "category": sys.argv[9]}], "userID": uid}).encode(), headers={"Content-Type": "application/json"})
response = urllib.request.urlopen(req)
print("success")
except urllib.error.HTTPError as e:
print(e.code)
except:
print("error")
elif sys.argv[1] == "stats":
try:
if sys.argv[6]:
urllib.request.urlopen(sys.argv[3] + "/api/viewedVideoSponsorTime?UUID=" + sys.argv[5])
if sys.argv[9]:
urllib.request.urlopen(sys.argv[3] + "/api/voteOnSponsorTime?UUID=" + sys.argv[5] + "&userID=" + uid + "&type=" + sys.argv[9])
except:
pass
elif sys.argv[1] == "username":
try:
data = urllib.parse.urlencode({"userID": uid, "userName": sys.argv[9]}).encode()
req = urllib.request.Request(sys.argv[3] + "/api/setUsername", data=data)
urllib.request.urlopen(req)
except:
pass

View File

@@ -0,0 +1 @@
2w0fKpa3ZP3LF1GOsREvGfMGUBYbyrJzf24U

View File

@@ -0,0 +1 @@
/usr/share/mpv/scripts/stats-conv.py

View File

@@ -0,0 +1 @@
/usr/share/mpv/scripts/status-line.lua

1
.config/mpv/scripts/umpv Symbolic link
View File

@@ -0,0 +1 @@
/usr/share/mpv/scripts/umpv

1
.config/mpv/scripts/uosc Symbolic link
View File

@@ -0,0 +1 @@
/usr/share/mpv/scripts/uosc