dotfiles

My personal shell configs and stuff
git clone git://git.alex.balgavy.eu/dotfiles.git
Log | Files | Refs | Submodules | README | LICENSE

commit 2b71623763aa4734bfb5587eec6121db3cf8991d
parent 632bd63e1f8ca35ab1b23c5bdfb23f97d805827b
Author: Alex Balgavy <a.balgavy@gmail.com>
Date:   Mon,  9 Mar 2020 12:06:24 +0100

mpv sponsorblock


Former-commit-id: 8ebac925e49fe6b9532081a2cc7329194ba7dc2d
Diffstat:
Ampv/scripts/README.md | 18++++++++++++++++++
Ampv/scripts/shared/sponsorblock.db.REMOVED.git-id | 2++
Ampv/scripts/shared/sponsorblock.py | 111+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ampv/scripts/shared/sponsorblock.txt | 2++
Ampv/scripts/sponsorblock.lua | 408+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dmpv/visualizer.lua | 2--
6 files changed, 541 insertions(+), 2 deletions(-)

diff --git a/mpv/scripts/README.md b/mpv/scripts/README.md @@ -0,0 +1,17 @@ +# mpv_sponsorblock +A fully-featured port of [SponsorBlock](https://github.com/ajayyy/SponsorBlock) for mpv. + +## Requirements +- Python 3 + +## Installation +Move this repo's contents into your mpv `scripts` folder. + +## Usage +Play a YouTube video, sponsors will be skipped automatically. + +Default key bindings: +- g to set segment boundaries +- G (shift+g) to submit a segment +- h to upvote the last segment +- H (shift+h) to downvote the last segment+ \ No newline at end of file diff --git a/mpv/scripts/shared/sponsorblock.db.REMOVED.git-id b/mpv/scripts/shared/sponsorblock.db.REMOVED.git-id @@ -0,0 +1 @@ +124ce7db36d9751207a8bd5fc4828423c1d2931f+ \ No newline at end of file diff --git a/mpv/scripts/shared/sponsorblock.py b/mpv/scripts/shared/sponsorblock.py @@ -0,0 +1,110 @@ +import urllib.request +import urllib.parse +import sqlite3 +import random +import string +import json +import sys +import os + +if sys.argv[1] in ["submit", "stats", "username"]: + if not sys.argv[8]: + if os.path.isfile(sys.argv[7]): + with open(sys.argv[7]) as f: + uid = f.read() + else: + uid = "".join(random.choices(string.ascii_letters + string.digits, k=36)) + with open(sys.argv[7], "w") as f: + f.write(uid) + else: + uid = sys.argv[8] + +opener = urllib.request.build_opener() +opener.addheaders = [("User-Agent", "mpv_sponsorblock/1.0 (https://github.com/po5/mpv_sponsorblock)")] +urllib.request.install_opener(opener) + +if sys.argv[1] == "ranges" and not sys.argv[2]: + times = [] + try: + response = urllib.request.urlopen(sys.argv[3] + "/api/getVideoSponsorTimes?videoID=" + sys.argv[4]) + data = json.load(response) + for i, time in enumerate(data["sponsorTimes"]): + times.append(str(time[0]) + "," + str(time[1]) + "," + data["UUIDs"][i]) + print(":".join(times)) + except (TimeoutError, urllib.error.URLError) as e: + print("error") + except urllib.error.HTTPError as e: + if e.code == 404: + print("") + else: + print("error") +elif sys.argv[1] == "ranges": + conn = sqlite3.connect(sys.argv[2]) + conn.row_factory = sqlite3.Row + c = conn.cursor() + c.execute("SELECT startTime, endTime, votes, UUID FROM sponsorTimes WHERE videoID = ? AND shadowHidden = 0 AND votes > -1", (sys.argv[4],)) + times = [] + sponsors = c.fetchall() + best = list(sponsors) + dealtwith = [] + similar = [] + for sponsor_a in sponsors: + for sponsor_b in sponsors: + if sponsor_a is not sponsor_b and sponsor_a["startTime"] >= sponsor_b["startTime"] and sponsor_a["startTime"] <= sponsor_b["endTime"]: + similar.append([sponsor_a, sponsor_b]) + if sponsor_a in best: + best.remove(sponsor_a) + if sponsor_b in best: + best.remove(sponsor_b) + for sponsors_a in similar: + if sponsors_a in dealtwith: + continue + group = set(sponsors_a) + for sponsors_b in similar: + if sponsors_b[0] in group or sponsors_b[1] in group: + group.add(sponsors_b[0]) + group.add(sponsors_b[1]) + dealtwith.append(sponsors_b) + best.append(max(group, key=lambda x:x["votes"])) + for time in best: + times.append(str(time["startTime"]) + "," + str(time["endTime"]) + "," + time["UUID"]) + print(":".join(times)) +elif sys.argv[1] == "update": + try: + urllib.request.urlretrieve(sys.argv[3] + "/database.db", sys.argv[2] + ".tmp") + os.replace(sys.argv[2] + ".tmp", sys.argv[2]) + except PermissionError: + print("database update failed, file currently in use", file=sys.stderr) + exit(1) + except ConnectionResetError: + print("database update failed, connection reset", file=sys.stderr) + exit(1) + except TimeoutError: + print("database update failed, timed out", file=sys.stderr) + exit(1) + except urllib.error.URLError: + print("database update failed", file=sys.stderr) + exit(1) +elif sys.argv[1] == "submit": + try: + response = urllib.request.urlopen(sys.argv[3] + "/api/postVideoSponsorTimes?videoID=" + sys.argv[4] + "&startTime=" + sys.argv[5] + "&endTime=" + sys.argv[6] + "&userID=" + uid) + print("success") + except urllib.error.HTTPError as e: + print(e.code) + except: + print("error") +elif sys.argv[1] == "stats": + try: + if sys.argv[6]: + urllib.request.urlopen(sys.argv[3] + "/api/viewedVideoSponsorTime?UUID=" + sys.argv[5]) + if sys.argv[9]: + urllib.request.urlopen(sys.argv[3] + "/api/voteOnSponsorTime?UUID=" + sys.argv[5] + "&userID=" + uid + "&type=" + sys.argv[9]) + except: + pass +elif sys.argv[1] == "username": + try: + data = urllib.parse.urlencode({"userID": uid, "userName": sys.argv[9]}).encode() + req = urllib.request.Request(sys.argv[3] + "/api/setUsername", data=data) + urllib.request.urlopen(req) + except: + pass+ \ No newline at end of file diff --git a/mpv/scripts/shared/sponsorblock.txt b/mpv/scripts/shared/sponsorblock.txt @@ -0,0 +1 @@ +HjUNZJ3q8rowUExdUH3jd4rxz4RqTzf0t39g+ \ No newline at end of file diff --git a/mpv/scripts/sponsorblock.lua b/mpv/scripts/sponsorblock.lua @@ -0,0 +1,408 @@ +-- sponsorblock.lua +-- +-- This script skips sponsored segments of YouTube videos +-- using data from https://github.com/ajayyy/SponsorBlock + +local ON_WINDOWS = package.config:sub(1,1) ~= '/' + +local options = { + server_address = "https://api.sponsor.ajay.app", + + python_path = ON_WINDOWS and "python" or "python3", + + -- If true, sponsored segments will only be skipped once + skip_once = true, + + -- Note that sponsored segments may ocasionally be inaccurate if this is turned off + -- see https://ajay.app/blog.html#voting-and-pseudo-randomness-or-sponsorblock-or-youtube-sponsorship-segment-blocker + local_database = true, + + -- Update database on first run, does nothing if local_database is false + auto_update = true, + + -- User ID used to submit sponsored segments, leave blank for random + user_id = "", + + -- Name to display on the stats page https://sponsor.ajay.app/stats/ leave blank to keep current name + display_name = "", + + -- Tell the server when a skip happens + report_views = true, + + -- Auto upvote skipped sponsors + auto_upvote = true, + + -- Use sponsor times from server if they're more up to date than our local database + server_fallback = true, + + -- Minimum duration for sponsors (in seconds), segments under that threshold will be ignored + min_duration = 1, + + -- Fade audio for smoother transitions + audio_fade = false, + + -- Audio fade step, applied once every 100ms until cap is reached + audio_fade_step = 10, + + -- Audio fade cap + audio_fade_cap = 0, + + -- Fast forward through sponsors instead of skipping + fast_forward = false, + + -- Playback speed modifier when fast forwarding, applied once every second until cap is reached + fast_forward_increase = .2, + + -- Playback speed cap + fast_forward_cap = 2, + + -- Pattern for video id in local files, ignored if blank + -- Recommended value for base youtube-dl is "-([%a%d%-_]+)%.[mw][kpe][v4b][m]?$" + local_pattern = "" +} + +mp.options = require "mp.options" +mp.options.read_options(options, "sponsorblock") + +local legacy = mp.command_native_async == nil +if legacy then + options.local_database = false +end + +local utils = require "mp.utils" +local scripts_dir = mp.find_config_file("scripts") +local sponsorblock = utils.join_path(scripts_dir, "shared/sponsorblock.py") +local uid_path = utils.join_path(scripts_dir, "shared/sponsorblock.txt") +local database_file = options.local_database and utils.join_path(scripts_dir, "shared/sponsorblock.db") or "" +local youtube_id = nil +local ranges = {} +local init = false +local segment = {a = 0, b = 0, progress = 0} +local retrying = false +local last_skip = {uuid = "", dir = nil} +local speed_timer = nil +local fade_timer = nil +local fade_dir = nil +local volume_before = mp.get_property_number("volume") + +function file_exists(name) + local f = io.open(name,"r") + if f ~= nil then io.close(f) return true else return false end +end + +function t_count(t) + local count = 0 + for _ in pairs(t) do count = count + 1 end + return count +end + +function getranges(_, exists, db, more) + if type(exists) == "table" and exists["status"] == "1" then + if options.server_fallback then + mp.add_timeout(0, function() getranges(true, true, "") end) + else + return mp.osd_message("[sponsorblock] database update failed, gave up") + end + end + if db ~= "" and db ~= database_file then db = database_file end + if exists ~= true and not file_exists(db) then + if not retrying then + mp.osd_message("[sponsorblock] database update failed, retrying...") + retrying = true + end + return update() + end + if retrying then + mp.osd_message("[sponsorblock] database update succeeded") + retrying = false + end + local sponsors + local args = { + options.python_path, + sponsorblock, + "ranges", + db, + options.server_address, + youtube_id + } + if not legacy then + sponsors = mp.command_native({name = "subprocess", capture_stdout = true, playback_only = false, args = args}) + else + sponsors = utils.subprocess({args = args}) + end + if not string.match(sponsors.stdout, "^%s*(.*%S)") then return end + if string.match(sponsors.stdout, "error") then return getranges(true, true) end + local new_ranges = {} + local r_count = 0 + if more then r_count = -1 end + for t in string.gmatch(sponsors.stdout, "[^:%s]+") do + uuid = string.match(t, '[^,]+$') + if ranges[uuid] then + new_ranges[uuid] = ranges[uuid] + else + start_time = tonumber(string.match(t, '[^,]+')) + end_time = tonumber(string.sub(string.match(t, ',[^,]+'), 2)) + if end_time - start_time >= options.min_duration then + new_ranges[uuid] = { + start_time = start_time, + end_time = end_time, + skipped = false + } + end + end + r_count = r_count + 1 + end + local c_count = t_count(ranges) + if c_count == 0 or r_count >= c_count then + ranges = new_ranges + end +end + +function fast_forward() + local last_speed = mp.get_property_number("speed") + local new_speed = math.min(last_speed + options.fast_forward_increase, options.fast_forward_cap) + if new_speed <= last_speed then return end + mp.set_property("speed", new_speed) +end + +function fade_audio(step) + local last_volume = mp.get_property_number("volume") + local new_volume = math.max(options.audio_fade_cap, math.min(last_volume + step, volume_before)) + if new_volume == last_volume then + if step >= 0 then fade_dir = nil end + if fade_timer ~= nil then fade_timer:kill() end + fade_timer = nil + return + end + mp.set_property("volume", new_volume) +end + +function skip_ads(name, pos) + if pos == nil then return end + local sponsor_ahead = false + for uuid, t in pairs(ranges) do + if (options.fast_forward == uuid or not options.skip_once or not t.skipped) and t.start_time <= pos and t.end_time > pos then + if options.fast_forward == uuid then return end + if options.fast_forward == false then + mp.osd_message("[sponsorblock] sponsor skipped") + mp.set_property("time-pos", t.end_time) + else + mp.osd_message("[sponsorblock] skipping sponsor") + end + t.skipped = true + last_skip = {uuid = uuid, dir = nil} + if options.report_views or options.auto_upvote then + local args = { + options.python_path, + sponsorblock, + "stats", + database_file, + options.server_address, + youtube_id, + uuid, + options.report_views and "1" or "", + uid_path, + options.user_id, + options.auto_upvote and "1" or "" + } + if not legacy then + mp.command_native_async({name = "subprocess", playback_only = false, args = args}, function () end) + else + utils.subprocess_detached({args = args}) + end + end + if options.fast_forward ~= false then + options.fast_forward = uuid + speed_timer = mp.add_periodic_timer(1, fast_forward) + end + return + elseif (not options.skip_once or not t.skipped) and t.start_time <= pos + 1 and t.end_time > pos + 1 then + sponsor_ahead = true + end + end + if options.audio_fade then + if sponsor_ahead then + if fade_dir ~= false then + if fade_dir == nil then volume_before = mp.get_property_number("volume") end + if fade_timer ~= nil then fade_timer:kill() end + fade_dir = false + fade_timer = mp.add_periodic_timer(.1, function() fade_audio(-options.audio_fade_step) end) + end + elseif fade_dir == false then + fade_dir = true + if fade_timer ~= nil then fade_timer:kill() end + fade_timer = mp.add_periodic_timer(.1, function() fade_audio(options.audio_fade_step) end) + end + end + if options.fast_forward and options.fast_forward ~= true then + options.fast_forward = true + speed_timer:kill() + mp.set_property("speed", 1) + end +end + +function vote(dir) + if last_skip.uuid == "" then return mp.osd_message("[sponsorblock] no sponsors skipped, can't submit vote") end + local updown = dir == "1" and "up" or "down" + if last_skip.dir == dir then return mp.osd_message("[sponsorblock] " .. updown .. "vote already submitted") end + last_skip.dir = dir + local args = { + options.python_path, + sponsorblock, + "stats", + database_file, + options.server_address, + youtube_id, + last_skip.uuid, + "", + uid_path, + options.user_id, + dir + } + if not legacy then + mp.command_native_async({name = "subprocess", playback_only = false, args = args}, function () end) + else + utils.subprocess({args = args}) + end + mp.osd_message("[sponsorblock] " .. updown .. "vote submitted") +end + +function update() + mp.command_native_async({name = "subprocess", playback_only = false, args = { + options.python_path, + sponsorblock, + "update", + database_file, + options.server_address + }}, getranges) +end + +function file_loaded() + local initialized = init + ranges = {} + segment = {a = 0, b = 0, progress = 0} + last_skip = {uuid = "", dir = nil} + local video_path = mp.get_property("path") + local youtube_id1 = string.match(video_path, "https?://youtu%.be/([%a%d%-_]+).*") + local youtube_id2 = string.match(video_path, "https?://w?w?w?%.?youtube%.com/v/([%a%d%-_]+).*") + local youtube_id3 = string.match(video_path, "/watch%?v=([%a%d%-_]+).*") + local youtube_id4 = string.match(video_path, "/embed/([%a%d%-_]+).*") + local local_pattern = nil + if options.local_pattern ~= "" then + local_pattern = string.match(video_path, options.local_pattern) + end + youtube_id = youtube_id1 or youtube_id2 or youtube_id3 or youtube_id4 or local_pattern + if not youtube_id then return end + init = true + if not options.local_database then + getranges(true, true) + else + local exists = file_exists(database_file) + if exists and options.server_fallback then + getranges(true, true) + mp.add_timeout(0, function() getranges(true, true, "", true) end) + elseif exists then + getranges(true, true) + elseif options.server_fallback then + mp.add_timeout(0, function() getranges(true, true, "") end) + end + end + if initialized then return end + mp.observe_property("time-pos", "native", skip_ads) + if options.display_name ~= "" then + local args = { + options.python_path, + sponsorblock, + "username", + database_file, + options.server_address, + youtube_id, + "", + "", + uid_path, + options.user_id, + options.display_name + } + if not legacy then + mp.command_native_async({name = "subprocess", playback_only = false, args = args}, function () end) + else + utils.subprocess_detached({args = args}) + end + end + if not options.local_database or (not options.auto_update and file_exists(database_file)) then return end + update() +end + +function set_segment() + if not youtube_id then return end + local pos = mp.get_property_number("time-pos") + if pos == nil then return end + if segment.progress > 1 then + segment.progress = segment.progress - 2 + end + if segment.progress == 1 then + segment.progress = 0 + segment.b = pos + mp.osd_message("[sponsorblock] segment boundary B set, press again for boundary A", 3) + else + segment.progress = 1 + segment.a = pos + mp.osd_message("[sponsorblock] segment boundary A set, press again for boundary B", 3) + end +end + +function submit_segment() + if not youtube_id then return end + local start_time = math.min(segment.a, segment.b) + local end_time = math.max(segment.a, segment.b) + if end_time - start_time == 0 or end_time == 0 then + mp.osd_message("[sponsorblock] empty segment, not submitting") + elseif segment.progress <= 1 then + mp.osd_message(string.format("[sponsorblock] press Shift+G again to confirm: %.2d:%.2d:%.2d to %.2d:%.2d:%.2d", start_time/(60*60), start_time/60%60, start_time%60, end_time/(60*60), end_time/60%60, end_time%60), 5) + segment.progress = segment.progress + 2 + else + mp.osd_message("[sponsorblock] submitting segment...", 30) + local submit + local args = { + options.python_path, + sponsorblock, + "submit", + database_file, + options.server_address, + youtube_id, + tostring(start_time), + tostring(end_time), + uid_path, + options.user_id + } + if not legacy then + submit = mp.command_native({name = "subprocess", capture_stdout = true, playback_only = false, args = args}) + else + submit = utils.subprocess({args = args}) + end + if string.match(submit.stdout, "success") then + segment = {a = 0, b = 0, progress = 0} + mp.osd_message("[sponsorblock] segment submitted") + elseif string.match(submit.stdout, "error") then + mp.osd_message("[sponsorblock] segment submission failed, server may be down. try again", 5) + elseif string.match(submit.stdout, "502") then + mp.osd_message("[sponsorblock] segment submission failed, server is down. try again", 5) + elseif string.match(submit.stdout, "400") then + mp.osd_message("[sponsorblock] segment submission failed, impossible inputs", 5) + segment = {a = 0, b = 0, progress = 0} + elseif string.match(submit.stdout, "429") then + mp.osd_message("[sponsorblock] segment submission failed, rate limited. try again", 5) + elseif string.match(submit.stdout, "409") then + mp.osd_message("[sponsorblock] segment already submitted", 3) + segment = {a = 0, b = 0, progress = 0} + else + mp.osd_message("[sponsorblock] segment submission failed", 5) + end + end +end + +mp.register_event("file-loaded", file_loaded) +mp.add_key_binding("g", "sponsorblock_set_segment", set_segment) +mp.add_key_binding("G", "sponsorblock_submit_segment", submit_segment) +mp.add_key_binding("h", "sponsorblock_upvote", function() return vote("1") end) +mp.add_key_binding("H", "sponsorblock_downvote", function() return vote("0") end) diff --git a/mpv/visualizer.lua b/mpv/visualizer.lua @@ -1 +0,0 @@ -/Users/alex/.dotfiles/other-scripts/visualizer.lua- \ No newline at end of file