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:
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