From 1e10fedafee0db9009d26638ba23a09c2ac5aebf Mon Sep 17 00:00:00 2001 From: blank X Date: Wed, 22 Jun 2022 10:03:35 +0700 Subject: [PATCH] Initial commit --- LICENSE | 19 +++ README.md | 44 +++++++ sponsorblock_lm.lua | 278 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 341 insertions(+) create mode 100644 LICENSE create mode 100644 README.md create mode 100644 sponsorblock_lm.lua diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..db5ef43 --- /dev/null +++ b/LICENSE @@ -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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..5d415af --- /dev/null +++ b/README.md @@ -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`. diff --git a/sponsorblock_lm.lua b/sponsorblock_lm.lua new file mode 100644 index 0000000..f8c61d9 --- /dev/null +++ b/sponsorblock_lm.lua @@ -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)