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