Initial commit
This commit is contained in:
commit
1e10fedafe
|
@ -0,0 +1,19 @@
|
||||||
|
Copyright (c) 2022 blankie
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
|
@ -0,0 +1,44 @@
|
||||||
|
# mpv_sponsorblock_lm
|
||||||
|
|
||||||
|
Another Sponsorblock implementation for mpv, just a bit less minimal than
|
||||||
|
[mpv_sponsorblock_minimal](https://codeberg.org/jouni/mpv_sponsorblock_minimal).
|
||||||
|
`sha256sum` and `curl` are required.
|
||||||
|
|
||||||
|
### Supported
|
||||||
|
- K-Anonymity
|
||||||
|
- Skip and mute segments
|
||||||
|
- Full video labels
|
||||||
|
- Whitelisting channels
|
||||||
|
|
||||||
|
### Unsupported
|
||||||
|
- Point of interest/Highlight segments
|
||||||
|
- Local database
|
||||||
|
- Submitting segments or votes
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
The config file should be placed in `script-opts/sponsorblock_lm.conf` in mpv's
|
||||||
|
config folder (usually `$XDG_CONFIG_HOME/mpv` or `~/.config/mpv`). Example config:
|
||||||
|
|
||||||
|
```
|
||||||
|
# Enderman, LockPickingLawyer
|
||||||
|
whitelisted_channels=UCWb-66XSFCV5vgKEbl22R6Q UCm9K6rby98W8JigLoZOh6FQ
|
||||||
|
find_id_from_title=yes
|
||||||
|
```
|
||||||
|
|
||||||
|
### Available Options
|
||||||
|
- api_url: URL pointing to the Sponsorblock server's API. (default: `https://sponsor.ajay.app/api`)
|
||||||
|
- categories: Space-seperated list of [categories](https://wiki.sponsor.ajay.app/w/Types#Category)
|
||||||
|
to skip/mute/warn. (default: `sponsor interaction music_offtopic exclusive_access`)
|
||||||
|
- whitelisted_channels: Space-seperated list of channel IDs to ignore. Sponsor segments will not
|
||||||
|
be fetched. Channel IDs are only fetched based on the video's title (see find_id_from_title).
|
||||||
|
(default: none)
|
||||||
|
- find_id_from_filename: Finds video ID from a filename in the form of
|
||||||
|
`...-ID.three or four alphanumberic characters`. (default: yes)
|
||||||
|
- find_id_from_title: Finds video and channel ID from the video title in the form of
|
||||||
|
`... (ID by CHANNELID)`. (default: no)
|
||||||
|
|
||||||
|
By default, pressing `b` will toggle between enabling and disabling Sponsorblock
|
||||||
|
(segments will still be fetched) . To bind it to both `b` **and** `s`, add
|
||||||
|
`s script-binding sponsorblock_lm/toggle` to mpv's `input.conf`. To unbind `b`, add
|
||||||
|
`b ignore`.
|
|
@ -0,0 +1,278 @@
|
||||||
|
-- I would like to know why utils and options have different syntax.
|
||||||
|
local utils = require "mp.utils"
|
||||||
|
local opts = require "mp.options"
|
||||||
|
|
||||||
|
-- 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 content"
|
||||||
|
}
|
||||||
|
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
|
||||||
|
local muted = mp.get_property_bool("ao-mute") or mp.get_property_bool("mute")
|
||||||
|
if muted ~= true and (video_data.muted_due_to_segment == nil
|
||||||
|
or segment.segment[1] ~= video_data.muted_due_to_segment[1]
|
||||||
|
or segment.segment[2] ~= video_data.muted_due_to_segment[2]) then
|
||||||
|
mp.osd_message("[sponsorblock] Muting " .. segment.segment[2] - current_pos
|
||||||
|
.. "s " .. category_name)
|
||||||
|
video_data.muted_due_to_segment = segment.segment
|
||||||
|
mp.set_property_bool("mute", true)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
mp.msg.warn("Unknown action type:", segment.actionType)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if video_data.muted_due_to_segment ~= nil and
|
||||||
|
(current_pos < video_data.muted_due_to_segment[1] or
|
||||||
|
video_data.muted_due_to_segment[2] <= current_pos) then
|
||||||
|
mp.set_property_bool("mute", false)
|
||||||
|
video_data.muted_due_to_segment = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function endFile()
|
||||||
|
if video_data ~= nil and video_data.muted_due_to_segment ~= nil then
|
||||||
|
mp.set_property_bool("mute", false)
|
||||||
|
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.muted_due_to_segment ~= nil then
|
||||||
|
mp.set_property_bool("mute", false)
|
||||||
|
video_data.muted_due_to_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="
|
||||||
|
.. "[\"skip\",\"mute\",\"full\"]&categories="
|
||||||
|
.. utils.format_json(split(options.categories))
|
||||||
|
proc = mp.command_native({
|
||||||
|
name = "subprocess",
|
||||||
|
args = {"curl", "--no-progress-meter", "--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
|
||||||
|
i.muted_due_to_segment = nil
|
||||||
|
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 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)
|
Loading…
Reference in New Issue