mpv: Replace sponsorblock with minimal version

Use a more minimal version of sponsorblock which neither caches the
results locally, nor allows many of the fancy things that the python
plugin allows.
On the other hand, it is lua-only and does not depend on python, and
fulfills the same basic functionality: skipping sponsorship segments.

You can toggle the plugin on and off by pressing `b`.
Additionally, it also works on files played locally, if those carry a
yt identifier in their filename.
This commit is contained in:
Marty Oehme 2021-04-05 15:45:05 +02:00
parent c751df31ad
commit 4ade691441
Signed by: Marty
GPG key ID: B7538B8F50A1C800
5 changed files with 91 additions and 626 deletions

View file

@ -1,506 +0,0 @@
-- 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",
-- Whether or not to automatically skip sponsors
skip = true,
-- 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 = true,
-- Update database on first run, does nothing if local_database is false
auto_update = true,
-- 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 = true,
-- 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 with skip=false
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,
-- Pattern for video id in local files, ignored if blank
-- Recommended value for base youtube-dl is "-([%a%d%-_]+)%.[mw][kpe][v4b][m]?$"
local_pattern = ""
}
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
local utils = require "mp.utils"
if mp.get_script_directory == nil then
scripts_dir = mp.find_config_file("scripts/sponsorblock")
else
scripts_dir = mp.get_script_directory()
end
local sponsorblock = utils.join_path(scripts_dir, "shared/sponsorblock.py")
local uid_path = utils.join_path(scripts_dir, "shared/sponsorblock.txt")
local database_file = options.local_database and
utils.join_path(scripts_dir, "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")
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) return a.time < b.time 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 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
}
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
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
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
goto continue
end
end
if end_time - start_time >= options.min_duration then
new_ranges[uuid] = {
start_time = start_time,
end_time = end_time,
skipped = false
}
end
if options.make_chapters then
create_chapter("Sponsor start (" .. string.sub(uuid, 1, 6) ..
")", start_time)
create_chapter("Sponsor end (" .. string.sub(uuid, 1, 6) .. ")",
end_time)
end
end
::continue::
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()
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] sponsor skipped")
mp.set_property("time-pos", t.end_time)
else
mp.osd_message("[sponsorblock] skipping sponsor")
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
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()
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}
local video_path = mp.get_property("path")
local youtube_id1 = string.match(video_path,
"https?://youtu%.be/([%a%d%-_]+).*")
local youtube_id2 = string.match(video_path,
"https?://w?w?w?%.?youtube%.com/v/([%a%d%-_]+).*")
local youtube_id3 = string.match(video_path, "/watch%?v=([%a%d%-_]+).*")
local youtube_id4 = string.match(video_path, "/embed/([%a%d%-_]+).*")
local local_pattern = nil
if options.local_pattern ~= "" then
local_pattern = string.match(video_path, options.local_pattern)
end
youtube_id = youtube_id1 or youtube_id2 or youtube_id3 or youtube_id4 or
local_pattern
if not youtube_id then return end
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
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 submit_segment()
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
mp.osd_message(string.format(
"[sponsorblock] press Shift+G again to confirm: %.2d:%.2d:%.2d to %.2d:%.2d:%.2d",
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)), 5)
segment.progress = segment.progress + 2
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
}
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("e", "sponsorblock_set_segment", set_segment)
mp.add_key_binding("E", "sponsorblock_submit_segment", submit_segment)
mp.add_key_binding("w", "sponsorblock_upvote", function() return vote("1") end)
mp.add_key_binding("W", "sponsorblock_downvote", function() return vote("0") end)

View file

@ -1,110 +0,0 @@
import urllib.request
import urllib.parse
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])):
times = []
try:
response = urllib.request.urlopen(sys.argv[3] + "/api/getVideoSponsorTimes?videoID=" + sys.argv[4])
data = json.load(response)
for i, time in enumerate(data["sponsorTimes"]):
times.append(str(time[0]) + "," + str(time[1]) + "," + data["UUIDs"][i])
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()
c.execute("SELECT startTime, endTime, votes, UUID FROM sponsorTimes WHERE videoID = ? AND shadowHidden = 0 AND votes > -1 AND category = 'sponsor'", (sys.argv[4],))
times = []
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"])
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)
exit(1)
except ConnectionResetError:
print("database update failed, connection reset", file=sys.stderr)
exit(1)
except TimeoutError:
print("database update failed, timed out", file=sys.stderr)
exit(1)
except urllib.error.URLError:
print("database update failed", file=sys.stderr)
exit(1)
elif sys.argv[1] == "submit":
try:
response = urllib.request.urlopen(sys.argv[3] + "/api/postVideoSponsorTimes?videoID=" + sys.argv[4] + "&startTime=" + sys.argv[5] + "&endTime=" + sys.argv[6] + "&userID=" + uid)
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

@ -1,3 +0,0 @@
if mp.get_script_directory == nil then
dofile(mp.find_config_file("scripts/sponsorblock/main.lua"))
end

View file

@ -0,0 +1,88 @@
-- sponsorblock_minimal.lua
--
-- This script skips sponsored segments of YouTube videos
-- using data from https://github.com/ajayyy/SponsorBlock
--
-- original from https://codeberg.org/jouni/mpv_sponsorblock_minimal
-- adapted for local playback skipping and some refactoring by me
local options = {
API = "https://sponsor.ajay.app/api/skipSegments",
-- Categories to fetch and skip
categories = '"sponsor","intro","outro","interaction","selfpromo"'
}
local function getranges()
local args = {
"curl",
"-s",
"-d",
"videoID="..youtube_id,
"-d",
"categories=["..options.categories.."]",
"-G",
options.API}
local sponsors = mp.command_native({name = "subprocess", capture_stdout = true, playback_only = false, args = args})
if string.match(sponsors.stdout,"%[(.-)%]") then
ranges = {}
for i in string.gmatch(string.sub(sponsors.stdout,2,-2),"%[(.-)%]") do
k,v = string.match(i,"(%d+.?%d*),(%d+.?%d*)")
ranges[k] = v
end
end
return
end
local function skip_ads(name,pos)
if pos ~= nil then
for k,v in pairs(ranges) do
if tonumber(k) <= pos and tonumber(v) > pos then
--this message may sometimes be wrong
--it only seems to be a visual thing though
mp.osd_message("[sponsorblock] skipping forward "..math.floor(tonumber(v)-mp.get_property("time-pos")).."s")
--need to do the +0.01 otherwise mpv will start spamming skip sometimes
--example: https://www.youtube.com/watch?v=4ypMJzeNooo
mp.set_property("time-pos",tonumber(v)+0.01)
return
end
end
end
return
end
local function file_loaded()
local video_path = mp.get_property("path")
local youtube_id1 = string.match(video_path, "https?://youtu%.be/([%w-_]+).*")
local youtube_id2 = string.match(video_path, "https?://w?w?w?%.?youtube%.com/v/([%w-_]+).*")
local youtube_id3 = string.match(video_path, "/watch.*[?&]v=([%w-_]+).*")
local youtube_id4 = string.match(video_path, "/embed/([%w-_]+).*")
local localytfile = string.match(video_path, "-([%a%d%-_]+)%.[mw][kpe][v4b][m]?$")
youtube_id = youtube_id1 or youtube_id2 or youtube_id3 or youtube_id4 or localytfile
if not youtube_id or string.len(youtube_id) < 11 then return end
youtube_id = string.sub(youtube_id, 1, 11)
getranges()
if ranges then
ON = true
mp.add_key_binding("b","sponsorblock",toggle)
mp.observe_property("time-pos", "native", skip_ads)
end
return
end
local function toggle()
if ON then
mp.unobserve_property(skip_ads)
mp.osd_message("[sponsorblock] off")
ON = false
return
end
mp.observe_property("time-pos", "native", skip_ads)
mp.osd_message("[sponsorblock] on")
ON = true
return
end
mp.register_event("file-loaded", file_loaded)

View file

@ -49,12 +49,8 @@ It degrades gracefully, and simply keeps running in higher quality if the file i
## sponsorblock ## sponsorblock
The [mpv-sponsorblock](https://github.com/po5/mpv_sponsorblock) script is included to enable automatically skipping many sponsorship segments integrated within youtube videos. The [minimal mpv-sponsorblock](https://codeberg.org/jouni/mpv_sponsorblock_minimal) script is included to enable automatically skipping many sponsorship segments integrated within youtube videos.
This works mostly fully automated, it checks the database and marks affected segments in red on your timeline, which it automatically skips over. This works mostly fully automated, it checks the database and finds affected segments, which it then automatically skips over.
If you are watching a video with sponsorship segments that have not yet been marked, you can add your own segment with `e` To toggle sponsorblock on or off just hit `b`, it will confirm the choice via osd.
(once at the beginning of the segment, once at the end) and when that is done send it off to the database with `E`.
If you watch a video and it contains a well-functioning skip, simply hit `w` to upvote the previous segment.
If a segment did not work well, skipped over other content or similar issues, hit `W` to downvote it.