313 lines
10 KiB
Lua
313 lines
10 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
|
|
if current_pos >= segment.segment[1] and segment.segment[2] > current_pos
|
|
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 " .. segment.segment[2] - current_pos
|
|
.. "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)
|