mpv_sponsorblock_lm/sponsorblock_lm.lua

314 lines
11 KiB
Lua

local utils = require "mp.utils"
local opts = require "mp.options"
local bit = require "bit"
-- https://wiki.sponsor.ajay.app/w/Types#Category
-- See README to configure options
local options = {
api_url = "https://sponsor.ajay.app/api",
categories = "sponsor interaction music_offtopic exclusive_access",
whitelisted_channels = "",
find_id_from_filename = true,
find_id_from_title = false
}
local disabled = false
local video_data = nil
local fvl_pretty = {
sponsor = "is sponsored",
selfpromo = "is a self-promotion",
interaction = "is an interaction reminder",
intro = "is an intro",
outro = "is an outro",
preview = "is a preview",
music_offtopic = "is offtopic content",
filler = "is filler",
exclusive_access = "features exclusive access"
}
local category_pretty = {
sponsor = "sponsor",
selfpromo = "self-promotion",
interaction = "interaction reminder",
intro = "intro",
outro = "outro",
preview = "preview",
music_offtopic = "offtopic content",
filler = "filler"
}
function startFile()
local ids = getIDs()
if ids == nil then
return
end
if ids.channel_id ~= nil and inTableValue(split(options.whitelisted_channels), ids.channel_id) then
return
end
video_data = getSegments(ids.video_id)
if video_data == nil or disabled then
return
end
for _, segment in pairs(video_data.segments) do
if segment.actionType == "full" and inTableValue(split(options.categories), segment.category) then
local message = "This entire video "
if fvl_pretty[segment.category] ~= nil then
message = message .. fvl_pretty[segment.category]
else
message = message .. "is " .. segment.category
end
mp.msg.info(message)
mp.osd_message(message, 5)
break
end
end
end
function observePlaybackTime(_, current_pos)
if video_data == nil or current_pos == nil or disabled then
return
end
for _, segment in pairs(video_data.segments) do
local seek_offset = segment.segment[2] - current_pos
if current_pos >= segment.segment[1] and segment.segment[2] > current_pos
and seek_offset >= 0.01
and segment.category ~= "full"
and inTableValue(split(options.categories), segment.category) then
local category_name = segment.category
if category_pretty[category_name] ~= nil then
category_name = category_pretty[category_name]
end
if segment.actionType == "skip" then
mp.osd_message("[sponsorblock] Skipping " .. seek_offset .. "s " .. category_name)
mp.set_property_number("playback-time", segment.segment[2])
current_pos = segment.segment[2]
elseif segment.actionType == "mute" then
-- if mute is false, then it's not muted, which causes it to be muted
local caused_mute = mp.get_property_bool("mute") == false
-- if subs are visible, they'll be hidden
local caused_hidden_subs = mp.get_property_bool("sub-visibility") == true
if (caused_mute or caused_hidden_subs) and (video_data.current_mute_segment == nil
or segment.segment[1] ~= video_data.current_mute_segment.segment[1]
or segment.segment[2] ~= video_data.current_mute_segment.segment[2]) then
mp.osd_message("[sponsorblock] Muting " .. segment.segment[2] - current_pos
.. "s " .. category_name)
video_data.current_mute_segment = {
segment = segment.segment,
caused_mute = caused_mute,
caused_hidden_subs = caused_hidden_subs
}
if caused_mute then
mp.set_property_bool("mute", true)
end
if caused_hidden_subs then
mp.set_property_bool("sub-visibility", false)
end
end
else
mp.msg.warn("Unknown action type:", segment.actionType)
end
end
end
if video_data.current_mute_segment ~= nil and
(current_pos < video_data.current_mute_segment.segment[1] or
video_data.current_mute_segment.segment[2] <= current_pos) then
if video_data.current_mute_segment.caused_mute then
mp.set_property_bool("mute", false)
end
if video_data.current_mute_segment.caused_hidden_subs then
mp.set_property_bool("sub-visibility", true)
end
video_data.current_mute_segment = nil
end
end
function endFile()
if video_data ~= nil and video_data.current_mute_segment ~= nil then
if video_data.current_mute_segment.caused_mute then
mp.set_property_bool("mute", false)
end
if video_data.current_mute_segment.caused_hidden_subs then
mp.set_property_bool("sub-visibility", true)
end
end
video_data = nil
end
function toggleSponsorblock()
local message = "[sponsorblock] "
if disabled then
disabled = false
message = message .. "Enabled"
else
disabled = true
if video_data ~= nil and video_data.current_mute_segment ~= nil then
if video_data.current_mute_segment.caused_mute then
mp.set_property_bool("mute", false)
end
if video_data.current_mute_segment.caused_hidden_subs then
mp.set_property_bool("sub-visibility", true)
end
video_data.current_mute_segment = nil
end
message = message .. "Disabled"
end
local segments_available = 0
if video_data ~= nil then
for _, i in pairs(video_data.segments) do
if i.actionType ~= "full" and inTableValue(split(options.categories), i.category) then
segments_available = segments_available + 1
end
end
end
if segments_available == 0 then
message = message .. " (but no segments available)"
end
mp.osd_message(message)
end
function getSegments(video_id)
local proc = mp.command_native({
name = "subprocess",
args = {"sha256sum"},
playback_only = true,
capture_stdout = true,
capture_stderr = false,
stdin_data = video_id
})
if proc.killed_by_us then
return
end
if proc.status ~= 0 then
mp.msg.error("Failed to hash video id: process exited with", proc.status)
return
end
local hashed_id = string.lower(string.sub(proc.stdout, 0, 4))
if string.len(hashed_id) ~= 4 then
mp.msg.error("Failed to hash video id: hashed id length is not 4:", proc.stdout)
return
end
if string.match(hashed_id, "[^0-9a-f]") then
mp.msg.error("Failed to hash video id: hashed id is not hex:", proc.stdout)
return
end
local url = options.api_url .. "/skipSegments/" .. hashed_id .. "?actionTypes="
.. percentEncodeComponent("[\"skip\",\"mute\",\"full\"]") .. "&categories="
.. percentEncodeComponent(utils.format_json(split(options.categories)))
proc = mp.command_native({
name = "subprocess",
args = {"curl", "--silent", "--show-error", "--globoff", url},
playback_only = true,
capture_stdout = true,
capture_stderr = false
})
if proc.killed_by_us then
return
end
if proc.status ~= 0 then
mp.msg.error("Failed to retrieve segments: process exited with", proc.status)
return
end
local segments = utils.parse_json(proc.stdout)
if segments == nil then
mp.msg.error("Failed to parse segments: stdout:", proc.stdout)
return
end
for _, i in pairs(segments) do
if i.videoID == video_id then
return i
end
end
end
function getIDs()
local path = mp.get_property("path")
if path == nil then
mp.msg.error("Failed to get path")
path = ""
end
local title = mp.get_property("media-title")
if title == nil then
mp.msg.error("Failed to get title")
path = ""
end
local result = {
video_id = nil,
channel_id = nil
}
path = string.gsub(path, "^https?://", "")
path = string.gsub(path, "^www%.", "")
result.video_id = string.match(path, "^youtu%.be/([%w%-_]+)")
if result.video_id ~= nil and string.len(result.video_id) >= 11 then
return result
end
result.video_id = string.match(path, "^youtube%.com/watch%?v=([%w%-_]+)")
if result.video_id ~= nil and string.len(result.video_id) >= 11 then
return result
end
result.video_id = string.match(path, "^youtube%.com/embed/([%w%-_]+)")
if result.video_id ~= nil and string.len(result.video_id) >= 11 then
return result
end
result.video_id = string.match(path, "^youtube-nocookies?%.com/embed/([%w%-_]+)")
if result.video_id ~= nil and string.len(result.video_id) >= 11 then
return result
end
if options.find_id_from_filename then
-- Regex exists.
result.video_id = string.match(path,
".+-([%w%-_][%w%-_][%w%-_][%w%-_][%w%-_][%w%-_][%w%-_][%w%-_][%w%-_][%w%-_][%w%-_]+)%.%w%w%w%w?$")
if result.video_id ~= nil then
return result
end
end
if options.find_id_from_title then
for video_id, channel_id in string.gmatch(title, ".+ %(([%w%-_]+) by ([%w%-_]+)%)$") do
if string.len(video_id) >= 11 and string.len(channel_id) >= 24 then
result.video_id = video_id
result.channel_id = channel_id
return result
end
end
end
end
function percentEncodeComponent(s)
-- https://url.spec.whatwg.org/#component-percent-encode-set
return string.gsub(s, "[^!'()*-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz~]", function (char)
local byte = string.byte(char)
return "%" .. bit.tohex(byte, -2)
end)
end
function split(s)
local arr = {}
for i in string.gmatch(s, "%S+") do
table.insert(arr, i)
end
return arr
end
function inTableValue(tab, item)
for _, i in pairs(tab) do
if i == item then
return true
end
end
return false
end
opts.read_options(options, "sponsorblock_lm")
mp.add_key_binding("b", "toggle", toggleSponsorblock)
mp.register_event("start-file", startFile)
mp.observe_property("playback-time", "number", observePlaybackTime)
mp.register_event("end-file", endFile)