Initial commit

This commit is contained in:
blank X 2022-06-22 10:03:35 +07:00
commit 1e10fedafe
Signed by: blankie
GPG Key ID: CC15FC822C7F61F5
3 changed files with 341 additions and 0 deletions

19
LICENSE Normal file
View File

@ -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.

44
README.md Normal file
View File

@ -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`.

278
sponsorblock_lm.lua Normal file
View File

@ -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)