diff --git a/.config/mpv/input.conf b/.config/mpv/input.conf new file mode 100644 index 0000000..f903b7a --- /dev/null +++ b/.config/mpv/input.conf @@ -0,0 +1,180 @@ +# mpv keybindings +# +# Location of user-defined bindings: ~/.config/mpv/input.conf +# + +# Lines starting with # are comments. Use SHARP to assign the # key. +# Copy this file and uncomment and edit the bindings you want to change. +# +# List of commands and further details: DOCS/man/input.rst +# List of special keys: --input-keylist +# Keybindings testing mode: mpv --input-test --force-window --idle +# +# Use 'ignore' to unbind a key fully (e.g. 'ctrl+a ignore'). +# +# Strings need to be quoted and escaped: +# KEY show-text "This is a single backslash: \\ and a quote: \" !" +# +# You can use modifier-key combinations like Shift+Left or Ctrl+Alt+x with +# the modifiers Shift, Ctrl, Alt and Meta (may not work on the terminal). +# +# The default keybindings are hardcoded into the mpv binary. +# You can disable them completely with: --no-input-default-bindings + +# Developer note: +# On compilation, this file is baked into the mpv binary, and all lines are +# uncommented (unless '#' is followed by a space) - thus this file defines the +# default key bindings. + +# If this is enabled, treat all the following bindings as default. +#default-bindings start + +#MBTN_LEFT ignore # don't do anything +#MBTN_LEFT_DBL cycle fullscreen # toggle fullscreen +#MBTN_RIGHT cycle pause # toggle pause/playback mode +#MBTN_BACK playlist-prev # skip to the previous file +#MBTN_FORWARD playlist-next # skip to the next file + +# Mouse wheels, touchpad or other input devices that have axes +# if the input devices supports precise scrolling it will also scale the +# numeric value accordingly +#WHEEL_UP seek 10 # seek 10 seconds forward +#WHEEL_DOWN seek -10 # seek 10 seconds backward +#WHEEL_LEFT add volume -2 +#WHEEL_RIGHT add volume 2 + +## Seek units are in seconds, but note that these are limited by keyframes +#RIGHT seek 5 # seek 5 seconds forward +#LEFT seek -5 # seek 5 seconds backward +#UP seek 60 # seek 1 minute forward +#DOWN seek -60 # seek 1 minute backward +# Do smaller, always exact (non-keyframe-limited), seeks with shift. +# Don't show them on the OSD (no-osd). +#Shift+RIGHT no-osd seek 1 exact # seek exactly 1 second forward +#Shift+LEFT no-osd seek -1 exact # seek exactly 1 second backward +#Shift+UP no-osd seek 5 exact # seek exactly 5 seconds forward +#Shift+DOWN no-osd seek -5 exact # seek exactly 5 seconds backward +#Ctrl+LEFT no-osd sub-seek -1 # seek to the previous subtitle +#Ctrl+RIGHT no-osd sub-seek 1 # seek to the next subtitle +#Ctrl+Shift+LEFT sub-step -1 # change subtitle timing such that the previous subtitle is displayed +#Ctrl+Shift+RIGHT sub-step 1 # change subtitle timing such that the next subtitle is displayed +#Alt+left add video-pan-x 0.1 # move the video right +#Alt+right add video-pan-x -0.1 # move the video left +#Alt+up add video-pan-y 0.1 # move the video down +#Alt+down add video-pan-y -0.1 # move the video up +#Alt++ add video-zoom 0.1 # zoom in +#Alt+- add video-zoom -0.1 # zoom out +#Alt+BS set video-zoom 0 ; set video-pan-x 0 ; set video-pan-y 0 # reset zoom and pan settings +#PGUP add chapter 1 # seek to the next chapter +#PGDWN add chapter -1 # seek to the previous chapter +#Shift+PGUP seek 600 # seek 10 minutes forward +#Shift+PGDWN seek -600 # seek 10 minutes backward +#[ multiply speed 1/1.1 # decrease the playback speed +#] multiply speed 1.1 # increase the playback speed +< multiply speed 1/1.5 +> multiply speed 1.5 +#BS set speed 1.0 # reset the speed to normal +#Shift+BS revert-seek # undo the previous (or marked) seek +#Shift+Ctrl+BS revert-seek mark # mark the position for revert-seek +#q quit +#Q quit-watch-later # exit and remember the playback position +#q {encode} quit 4 +#ESC set fullscreen no # leave fullscreen +#ESC {encode} quit 4 +#p cycle pause # toggle pause/playback mode +#. frame-step # advance one frame and pause +#, frame-back-step # go back by one frame and pause +#SPACE cycle pause # toggle pause/playback mode +#> playlist-next # skip to the next file +#ENTER playlist-next # skip to the next file +#< playlist-prev # skip to the previous file +#O no-osd cycle-values osd-level 3 1 # toggle displaying the OSD on user interaction or always +#o show-progress # show playback progress +#P show-progress # show playback progress +#i script-binding stats/display-stats # display information and statistics +#I script-binding stats/display-stats-toggle # toggle displaying information and statistics +#` script-binding console/enable # open the console +#z add sub-delay -0.1 # shift subtitles 100 ms earlier +#Z add sub-delay +0.1 # delay subtitles by 100 ms +#x add sub-delay +0.1 # delay subtitles by 100 ms +#ctrl++ add audio-delay 0.100 # change audio/video sync by delaying the audio +#ctrl+- add audio-delay -0.100 # change audio/video sync by shifting the audio earlier +#Shift+g add sub-scale +0.1 # increase the subtitle font size +#Shift+f add sub-scale -0.1 # decrease the subtitle font size +#9 add volume -2 +#/ add volume -2 +#0 add volume 2 +#* add volume 2 +#m cycle mute # toggle mute +#1 add contrast -1 +#2 add contrast 1 +#3 add brightness -1 +#4 add brightness 1 +#5 add gamma -1 +#6 add gamma 1 +#7 add saturation -1 +#8 add saturation 1 +#Alt+0 set current-window-scale 0.5 # halve the window size +#Alt+1 set current-window-scale 1.0 # reset the window size +#Alt+2 set current-window-scale 2.0 # double the window size +#d cycle deinterlace # toggle the deinterlacing filter +#r add sub-pos -1 # move subtitles up +#R add sub-pos +1 # move subtitles down +#t add sub-pos +1 # move subtitles down +#v cycle sub-visibility # hide or show the subtitles +#Alt+v cycle secondary-sub-visibility # hide or show the secondary subtitles +#V cycle sub-ass-vsfilter-aspect-compat # toggle stretching SSA/ASS subtitles with anamorphic videos to match the historical renderer +#u cycle-values sub-ass-override "force" "no" # toggle overriding SSA/ASS subtitle styles with the normal styles +#j cycle sub # switch subtitle track +#J cycle sub down # switch subtitle track backwards +#SHARP cycle audio # switch audio track +#_ cycle video # switch video track +#T cycle ontop # toggle placing the video on top of other windows +#f cycle fullscreen # toggle fullscreen +#s screenshot # take a screenshot of the video in its original resolution with subtitles +#S screenshot video # take a screenshot of the video in its original resolution without subtitles +#Ctrl+s screenshot window # take a screenshot of the window with OSD and subtitles +#Alt+s screenshot each-frame # automatically screenshot every frame; issue this command again to stop taking screenshots +#w add panscan -0.1 # decrease panscan +#W add panscan +0.1 # shrink black bars by cropping the video +#e add panscan +0.1 # shrink black bars by cropping the video +#A cycle-values video-aspect-override "16:9" "4:3" "2.35:1" "-1" # cycle the video aspect ratio ("-1" is the container aspect) +#POWER quit +#PLAY cycle pause # toggle pause/playback mode +#PAUSE cycle pause # toggle pause/playback mode +#PLAYPAUSE cycle pause # toggle pause/playback mode +#PLAYONLY set pause no # unpause +#PAUSEONLY set pause yes # pause +#STOP quit +#FORWARD seek 60 # seek 1 minute forward +#REWIND seek -60 # seek 1 minute backward +#NEXT playlist-next # skip to the next file +#PREV playlist-prev # skip to the previous file +#VOLUME_UP add volume 2 +#VOLUME_DOWN add volume -2 +#MUTE cycle mute # toggle mute +#CLOSE_WIN quit +#CLOSE_WIN {encode} quit 4 +#ctrl+w quit +#E cycle edition # switch edition +#l ab-loop # set/clear A-B loop points +#L cycle-values loop-file "inf" "no" # toggle infinite looping +#ctrl+c quit 4 +#DEL script-binding osc/visibility # cycle OSC visibility between never, auto (mouse-move) and always +#ctrl+h cycle-values hwdec "auto" "no" # toggle hardware decoding +#F8 show-text ${playlist} # show the playlist +#F9 show-text ${track-list} # show the list of video, audio and sub tracks + +# +# Legacy bindings (may or may not be removed in the future) +# +! add chapter -1 # seek to the previous chapter +@ add chapter 1 # seek to the next chapter + +# +# Not assigned by default +# (not an exhaustive list of unbound commands) +# + +# ? cycle sub-forced-only # toggle DVD forced subs +# ? stop # stop playback (quit or enter idle mode) diff --git a/.config/mpv/mpv.conf b/.config/mpv/mpv.conf new file mode 100644 index 0000000..9e8862e --- /dev/null +++ b/.config/mpv/mpv.conf @@ -0,0 +1,3 @@ +script-opts=ytdl_hook-ytdl_path=/usr/bin/yt-dlp +ytdl-format=bestvideo[height<=?480]+bestaudio/best + diff --git a/.config/mpv/scripts/autload.lua b/.config/mpv/scripts/autload.lua new file mode 100644 index 0000000..62b55c0 --- /dev/null +++ b/.config/mpv/scripts/autload.lua @@ -0,0 +1,212 @@ +-- This script automatically loads playlist entries before and after the +-- the currently played file. It does so by scanning the directory a file is +-- located in when starting playback. It sorts the directory entries +-- alphabetically, and adds entries before and after the current file to +-- the internal playlist. (It stops if it would add an already existing +-- playlist entry at the same position - this makes it "stable".) +-- Add at most 5000 * 2 files when starting a file (before + after). +--[[ +To configure this script use file autoload.conf in directory script-opts (the "script-opts" +directory must be in the mpv configuration directory, typically ~/.config/mpv/). + +Example configuration would be: + +disabled=no +images=no +videos=yes +audio=yes +ignore_hidden=yes + +--]] MAXENTRIES = 5000 + +local msg = require 'mp.msg' +local options = require 'mp.options' +local utils = require 'mp.utils' + +o = { + disabled = false, + images = true, + videos = true, + audio = true, + ignore_hidden = true +} +options.read_options(o) + +function Set(t) + local set = {} + for _, v in pairs(t) do set[v] = true end + return set +end + +function SetUnion(a, b) + local res = {} + for k in pairs(a) do res[k] = true end + for k in pairs(b) do res[k] = true end + return res +end + +EXTENSIONS_VIDEO = Set { + 'mkv', 'avi', 'mp4', 'ogv', 'webm', 'rmvb', 'flv', 'wmv', 'mpeg', 'mpg', + 'm4v', '3gp' +} + +EXTENSIONS_AUDIO = Set { + 'mp3', 'wav', 'ogm', 'flac', 'm4a', 'wma', 'ogg', 'opus' +} + +EXTENSIONS_IMAGES = Set { + 'jpg', 'jpeg', 'png', 'tif', 'tiff', 'gif', 'webp', 'svg', 'bmp' +} + +EXTENSIONS = Set {} +if o.videos then EXTENSIONS = SetUnion(EXTENSIONS, EXTENSIONS_VIDEO) end +if o.audio then EXTENSIONS = SetUnion(EXTENSIONS, EXTENSIONS_AUDIO) end +if o.images then EXTENSIONS = SetUnion(EXTENSIONS, EXTENSIONS_IMAGES) end + +function add_files_at(index, files) + index = index - 1 + local oldcount = mp.get_property_number('playlist-count', 1) + for i = 1, #files do + mp.commandv('loadfile', files[i], 'append') + mp.commandv('playlist-move', oldcount + i - 1, index + i - 1) + end +end + +function get_extension(path) + match = string.match(path, '%.([^%.]+)$') + if match == nil then + return 'nomatch' + else + return match + end +end + +table.filter = function(t, iter) + for i = #t, 1, -1 do if not iter(t[i]) then table.remove(t, i) end end +end + +-- splitbynum and alnumcomp from alphanum.lua (C) Andre Bogus +-- Released under the MIT License +-- http://www.davekoelle.com/files/alphanum.lua + +-- split a string into a table of number and string values +function splitbynum(s) + local result = {} + for x, y in (s or ''):gmatch('(%d*)(%D*)') do + if x ~= '' then table.insert(result, tonumber(x)) end + if y ~= '' then table.insert(result, y) end + end + return result +end + +function clean_key(k) + k = (' ' .. k .. ' '):gsub('%s+', ' '):sub(2, -2):lower() + return splitbynum(k) +end + +-- compare two strings +function alnumcomp(x, y) + local xt, yt = clean_key(x), clean_key(y) + for i = 1, math.min(#xt, #yt) do + local xe, ye = xt[i], yt[i] + if type(xe) == 'string' then + ye = tostring(ye) + elseif type(ye) == 'string' then + xe = tostring(xe) + end + if xe ~= ye then return xe < ye end + end + return #xt < #yt +end + +local autoloaded = nil + +function find_and_add_entries() + local path = mp.get_property('path', '') + local dir, filename = utils.split_path(path) + msg.trace(('dir: %s, filename: %s'):format(dir, filename)) + if o.disabled then + msg.verbose('stopping: autoload disabled') + return + elseif #dir == 0 then + msg.verbose('stopping: not a local path') + return + end + + local pl_count = mp.get_property_number('playlist-count', 1) + -- check if this is a manually made playlist + if (pl_count > 1 and autoloaded == nil) or + (pl_count == 1 and EXTENSIONS[string.lower(get_extension(filename))] == + nil) then + msg.verbose('stopping: manually made playlist') + return + else + autoloaded = true + end + + local pl = mp.get_property_native('playlist', {}) + local pl_current = mp.get_property_number('playlist-pos-1', 1) + msg.trace(('playlist-pos-1: %s, playlist: %s'):format(pl_current, + utils.to_string(pl))) + + local files = utils.readdir(dir, 'files') + if files == nil then + msg.verbose('no other files in directory') + return + end + table.filter(files, function(v, k) + -- The current file could be a hidden file, ignoring it doesn't load other + -- files from the current directory. + if (o.ignore_hidden and not (v == filename) and string.match(v, '^%.')) then + return false + end + local ext = get_extension(v) + if ext == nil then return false end + return EXTENSIONS[string.lower(ext)] + end) + table.sort(files, alnumcomp) + + if dir == '.' then dir = '' end + + -- Find the current pl entry (dir+"/"+filename) in the sorted dir list + local current + for i = 1, #files do + if files[i] == filename then + current = i + break + end + end + if current == nil then return end + msg.trace('current file position in files: ' .. current) + + local append = {[-1] = {}, [1] = {}} + for direction = -1, 1, 2 do -- 2 iterations, with direction = -1 and +1 + for i = 1, MAXENTRIES do + local file = files[current + i * direction] + local pl_e = pl[pl_current + i * direction] + if file == nil or file[1] == '.' then break end + + local filepath = dir .. file + if pl_e then + -- If there's a playlist entry, and it's the same file, stop. + msg.trace(pl_e.filename .. ' == ' .. filepath .. ' ?') + if pl_e.filename == filepath then break end + end + + if direction == -1 then + if pl_current == 1 then -- never add additional entries in the middle + msg.info('Prepending ' .. file) + table.insert(append[-1], 1, filepath) + end + else + msg.info('Adding ' .. file) + table.insert(append[1], filepath) + end + end + end + + add_files_at(pl_current + 1, append[1]) + add_files_at(pl_current, append[-1]) +end + +mp.register_event('start-file', find_and_add_entries) diff --git a/.config/mpv/scripts/mpv_chapters.js b/.config/mpv/scripts/mpv_chapters.js new file mode 100644 index 0000000..c89b8c3 --- /dev/null +++ b/.config/mpv/scripts/mpv_chapters.js @@ -0,0 +1,173 @@ +"use strict"; + +//display chapter on osd and easily switch between chapters by click on title of chapter +mp.register_event("file-loaded", init); +mp.observe_property("chapter", "number", onChapterChange); +mp.observe_property("chapter-list/count", "number", init); +var options = { + font_size: 16, + font_color: "00FFFF", + border_size: 1.0, + border_color: "000000", + font_color_currentChapter: "C27F1B", +}; +var playinfo = { + chapters: [], //array + chaptercount: "", // int + assinterface: [], //array(deprecated, use single assdraw instead) + currentChapter: "", //int + loaded:false, +}; +var toggle_switch = false; +var assdraw = mp.create_osd_overlay("ass-events"); +var autohidedelay = mp.get_property_number("cursor-autohide"); +//function +function init() { + playinfo.chapters = getChapters(); + playinfo.chaptercount = playinfo.chapters.length; + if(playinfo.chaptercount == 0){ + return; + } + while (playinfo.chaptercount * options.font_size > 1000 / 1.5) { + options.font_size = options.font_size - 1; + } + drawChapterList(); + mp.msg.info("initiated"); + playinfo.loaded = true; +} +function getChapters() { + var chapterCount = mp.get_property("chapter-list/count"); + if (chapterCount === 0) { + return ["null"]; + } else { + var chaptersArray = []; + for (var index = 0; index < chapterCount; index++) { + var chapterTitle = mp.get_property_native( + "chapter-list/" + index + "/title" + ); + + if (chapterTitle != undefined) { + chaptersArray.push(chapterTitle); + } + } + return chaptersArray; + } +} + +function drawChapterList() { + var resY = 0; + var resX = 0; + var assdrawdata = ""; + function setPos(str, _X, _Y) { + str = str + "{\\pos(" + _X + ", " + _Y + ")}"; + return str; + } + function setborderSize(str) { + str = str + "{\\bord" + options.border_size + "}"; + return str; + } + function setborderColor(str) { + str = str + "{\\3c&H" + options.border_color + "&}"; + return str; + } + function setFontColor(str, index) { + var _color; + if (playinfo.currentChapter == index) { + _color = options.font_color_currentChapter; + } else { + _color = options.font_color; + } + str = str + "{\\c&H" + _color + "&}"; + return str; + } + function setFont(str) { + str = str + "{\\fs" + options.font_size + "}"; + return str; + } + function setEndofmodifiers(str) { + str = str + "{\\p0}"; + return str; + } + function setEndofLine(str) { + str = str + "\n"; + return str; + } + playinfo.chapters.forEach(function (element, index) { + assdrawdata = setPos(assdrawdata, resX, resY); + assdrawdata = setborderSize(assdrawdata); + assdrawdata = setborderColor(assdrawdata); + assdrawdata = setFontColor(assdrawdata, index); + assdrawdata = setFont(assdrawdata); + assdrawdata = setEndofmodifiers(assdrawdata); + assdrawdata = assdrawdata + element; + assdrawdata = setEndofLine(assdrawdata); + resY += options.font_size; + }); + assdraw.data = assdrawdata + +} + +function toggleOverlay() { + if(!playinfo.loaded){ + return; + } + if (!toggle_switch) { + drawChapterList(); + assdraw.update(); + mp.set_property("cursor-autohide", "no"); + toggle_switch = !toggle_switch; + } else { + assdraw.remove(); + mp.set_property("cursor-autohide", autohidedelay); + toggle_switch = !toggle_switch; + } +} + +function onChapterChange() { + playinfo.currentChapter = mp.get_property_native("chapter"); + if (playinfo.currentChapter != undefined) { + drawChapterList(); + } + + if ((playinfo.currentChapter != undefined) & toggle_switch) { + assdraw.update(); + } +} +function pos2chapter(x, y, overallscale) { + var vectical = y / (options.font_size * overallscale); + if(vectical > playinfo.chaptercount){ + return null; + } + var intVectical = Math.floor(vectical); + var lengthofTitleClicked = playinfo.chapters[intVectical].length; + var lengthofTitleClicked_px = + (lengthofTitleClicked * options.font_size) / overallscale; + if (x < lengthofTitleClicked_px) { + return intVectical; + } else { + return null; + } +} +function getOverallScale() { + return mp.get_osd_size().height / 720; +} +function onMBTN_LEFT() { + //get mouse position + if(!playinfo.loaded){ + return; + } + if (toggle_switch) { + var overallscale = getOverallScale(); + var pos = mp.get_mouse_pos(); + var chapterClicked = pos2chapter(pos.x, pos.y, overallscale); + if (chapterClicked != null) { + mp.set_property_native("chapter", chapterClicked); + } + } +} +mp.add_key_binding("TAB", "tab", function () { + toggleOverlay(); +}); +mp.add_key_binding("MBTN_LEFT", "mbtn_left", function () { + onMBTN_LEFT(); +}); diff --git a/.config/mpv/scripts/sponsorblock.lua b/.config/mpv/scripts/sponsorblock.lua new file mode 100644 index 0000000..16999dc --- /dev/null +++ b/.config/mpv/scripts/sponsorblock.lua @@ -0,0 +1,597 @@ +-- 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://sponsor.ajay.app', + + python_path = ON_WINDOWS and 'python' or 'python3', + + -- Categories to fetch + categories = 'sponsor,intro,outro,interaction,selfpromo', + + -- Categories to skip automatically + skip_categories = 'sponsor,intro,outro,interaction,selfpromo', + + -- 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://blog.ajay.app/voting-and-pseudo-randomness-or-sponsorblock-or-youtube-sponsorship-segment-blocker + local_database = false, + + -- Update database on first run, does nothing if local_database is false + auto_update = true, + + -- How long to wait between local database updates + -- Format: "X[d,h,m]", leave blank to update on every mpv run + auto_update_interval = '6h', + + -- 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 = false, + + -- Use sponsor times from server if they're more up to date than our local database + server_fallback = true, + + -- Create chapters at sponsor boundaries for OSC display and manual skipping + make_chapters = 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, + + -- Length of the sha256 prefix (3-32) when querying server, 0 to disable + sha256_length = 4, + + -- Pattern for video id in local files, ignored if blank + -- Recommended value for base youtube-dl is "-([%w-_]+)%.[mw][kpe][v4b]m?$" + local_pattern = '', + + -- Legacy option, use skip_categories instead + skip = true +} + +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' +scripts_dir = mp.find_config_file('scripts') + +local sponsorblock = utils.join_path(scripts_dir, + 'sponsorblock_shared/sponsorblock.py') +local uid_path = utils.join_path(scripts_dir, + 'sponsorblock_shared/sponsorblock.txt') +local database_file = options.local_database and + utils.join_path(scripts_dir, + 'sponsorblock_shared/sponsorblock.db') or + '' +local youtube_id = nil +local ranges = {} +local init = false +local segment = {a = 0, b = 0, progress = 0, first = true} +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') +local categories = {} +local all_categories = { + 'sponsor', 'intro', 'outro', 'interaction', 'selfpromo', 'music_offtopic' +} +local chapter_cache = {} + +for category in string.gmatch(options.skip_categories, '([^,]+)') do + categories[category] = true +end + +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 time_sort(a, b) + if a.time == b.time then return string.match(a.title, 'segment end') end + return a.time < b.time +end + +function parse_update_interval() + local s = options.auto_update_interval + if s == '' then return 0 end -- Interval Disabled + + local num, mod = s:match '^(%d+)([hdm])$' + + if num == nil or mod == nil then + mp.osd_message('[sponsorblock] auto_update_interval ' .. s .. ' is invalid', + 5) + return nil + end + + local time_table = {m = 60, h = 60 * 60, d = 60 * 60 * 24} + + return num * time_table[mod] +end + +function clean_chapters() + local chapters = mp.get_property_native('chapter-list') + local new_chapters = {} + for _, chapter in pairs(chapters) do + if chapter.title ~= 'Preview segment start' and chapter.title ~= + 'Preview segment end' then table.insert(new_chapters, chapter) end + end + mp.set_property_native('chapter-list', new_chapters) +end + +function create_chapter(chapter_title, chapter_time) + local chapters = mp.get_property_native('chapter-list') + local duration = mp.get_property_native('duration') + table.insert(chapters, { + title = chapter_title, + time = (duration == nil or duration > chapter_time) and chapter_time or + duration - .001 + }) + table.sort(chapters, time_sort) + mp.set_property_native('chapter-list', chapters) +end + +function process(uuid, t, new_ranges) + start_time = tonumber(string.match(t, '[^,]+')) + end_time = tonumber(string.sub(string.match(t, ',[^,]+'), 2)) + for o_uuid, o_t in pairs(ranges) do + if (start_time >= o_t.start_time and start_time <= o_t.end_time) or + (o_t.start_time >= start_time and o_t.start_time <= end_time) then + new_ranges[o_uuid] = o_t + return + end + end + category = string.match(t, '[^,]+$') + if categories[category] and end_time - start_time >= options.min_duration then + new_ranges[uuid] = { + start_time = start_time, + end_time = end_time, + category = category, + skipped = false + } + end + if options.make_chapters and not chapter_cache[uuid] then + chapter_cache[uuid] = true + local category_title = (category:gsub('^%l', string.upper):gsub('_', ' ')) + create_chapter(category_title .. ' segment start (' .. + string.sub(uuid, 1, 6) .. ')', start_time) + create_chapter( + category_title .. ' segment end (' .. string.sub(uuid, 1, 6) .. ')', + end_time) + end +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, options.categories, tostring(options.sha256_length) + } + 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 + mp.msg.debug('Got: ' .. string.gsub(sponsors.stdout, '[\n\r]', '')) + 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 + process(uuid, t, new_ranges) + 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() + if options.fast_forward and options.fast_forward == true then + speed_timer = nil + mp.set_property('speed', 1) + end + 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] ' .. t.category .. ' skipped') + mp.set_property('time-pos', t.end_time) + else + mp.osd_message('[sponsorblock] skipping ' .. t.category) + 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 + if speed_timer ~= nil then speed_timer:kill() end + 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() + speed_timer = nil + 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, first = true} + last_skip = {uuid = '', dir = nil} + chapter_cache = {} + local video_path = mp.get_property('path', '') + mp.msg.debug('Path: ' .. video_path) + local video_referer = string.match(mp.get_property('http-header-fields', ''), + 'Referer:([^,]+)') or '' + mp.msg.debug('Referer: ' .. video_referer) + + local urls = { + 'https?://youtu%.be/([%w-_]+).*', + 'https?://w?w?w?%.?youtube%.com/v/([%w-_]+).*', '/watch.*[?&]v=([%w-_]+).*', + '/embed/([%w-_]+).*' + } + youtube_id = nil + for i, url in ipairs(urls) do + youtube_id = youtube_id or string.match(video_path, url) or + string.match(video_referer, url) + end + youtube_id = youtube_id or string.match(video_path, options.local_pattern) + + if not youtube_id or string.len(youtube_id) < 11 or + (local_pattern and string.len(youtube_id) ~= 11) then return end + youtube_id = string.sub(youtube_id, 1, 11) + mp.msg.debug('Found YouTube ID: ' .. youtube_id) + 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 + if options.skip then mp.observe_property('time-pos', 'native', skip_ads) end + 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 + + if file_exists(database_file) then + local db_info = utils.file_info(database_file) + local cur_time = os.time(os.date('*t')) + local upd_interval = parse_update_interval() + if upd_interval == nil or os.difftime(cur_time, db_info.mtime) < + upd_interval then return end + 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 + if options.make_chapters and not segment.first then + local start_time = math.min(segment.a, segment.b) + local end_time = math.max(segment.a, segment.b) + if end_time - start_time ~= 0 and end_time ~= 0 then + clean_chapters() + create_chapter('Preview segment start', start_time) + create_chapter('Preview segment end', end_time) + end + end + segment.first = false +end + +function select_category(selected) + for category in string.gmatch(options.categories, '([^,]+)') do + mp.remove_key_binding('select_category_' .. category) + mp.remove_key_binding('kp_select_category_' .. category) + end + submit_segment(selected) +end + +function submit_segment(category) + 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 + segment.progress = segment.progress + 2 + local category_list = '' + for category_id, category in pairs(all_categories) do + local category_title = (category:gsub('^%l', string.upper):gsub('_', ' ')) + category_list = category_list .. category_id .. ': ' .. category_title .. + '\n' + mp.add_forced_key_binding(tostring(category_id), + 'select_category_' .. category, + function() select_category(category) end) + mp.add_forced_key_binding('KP' .. tostring(category_id), + 'kp_select_category_' .. category, + function() select_category(category) end) + end + mp.osd_message(string.format( + '[sponsorblock] press a number to select category for segment: %.2d:%.2d:%.2d to %.2d:%.2d:%.2d\n\n' .. + category_list .. + '\nyou can press Shift+G again for default (Sponsor) or hide this message with g', + math.floor(start_time / (60 * 60)), + math.floor(start_time / 60 % 60), + math.floor(start_time % 60), + math.floor(end_time / (60 * 60)), + math.floor(end_time / 60 % 60), math.floor(end_time % 60)), + 30) + 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, category or 'sponsor' + } + 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, first = true} + mp.osd_message('[sponsorblock] segment submitted') + if options.make_chapters then + clean_chapters() + create_chapter('Submitted segment start', start_time) + create_chapter('Submitted segment end', end_time) + end + 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, first = true} + 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, first = true} + else + mp.osd_message('[sponsorblock] segment submission failed', 5) + end + end +end + +mp.register_event('file-loaded', file_loaded) +mp.add_key_binding('g', 'set_segment', set_segment) +mp.add_key_binding('G', 'submit_segment', submit_segment) +mp.add_key_binding('h', 'upvote_segment', function() return vote('1') end) +mp.add_key_binding('H', 'downvote_segment', function() return vote('0') end) +-- Bindings below are for backwards compatibility and could be removed at any time +mp.add_key_binding(nil, 'sponsorblock_set_segment', set_segment) +mp.add_key_binding(nil, 'sponsorblock_submit_segment', submit_segment) +mp.add_key_binding(nil, 'sponsorblock_upvote', function() return vote('1') end) +mp.add_key_binding(nil, 'sponsorblock_downvote', function() return vote('0') end) diff --git a/.config/mpv/scripts/sponsorblock_shared/main.lua b/.config/mpv/scripts/sponsorblock_shared/main.lua new file mode 100644 index 0000000..2bbe7a2 --- /dev/null +++ b/.config/mpv/scripts/sponsorblock_shared/main.lua @@ -0,0 +1,3 @@ +-- This is a dummy main.lua +-- required for mpv 0.33 +-- do not delete \ No newline at end of file diff --git a/.config/mpv/scripts/sponsorblock_shared/sponsorblock.py b/.config/mpv/scripts/sponsorblock_shared/sponsorblock.py new file mode 100644 index 0000000..2fdf3d6 --- /dev/null +++ b/.config/mpv/scripts/sponsorblock_shared/sponsorblock.py @@ -0,0 +1,124 @@ +import urllib.request +import urllib.parse +import hashlib +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] or not os.path.isfile(sys.argv[2])): + sha = None + if 3 <= int(sys.argv[6]) <= 32: + sha = hashlib.sha256(sys.argv[4].encode()).hexdigest()[:int(sys.argv[6])] + times = [] + try: + response = urllib.request.urlopen(sys.argv[3] + "/api/skipSegments" + ("/" + sha + "?" if sha else "?videoID=" + sys.argv[4] + "&") + urllib.parse.urlencode([("categories", json.dumps(sys.argv[5].split(",")))])) + segments = json.load(response) + for segment in segments: + if sha and sys.argv[4] != segment["videoID"]: + continue + if sha: + for s in segment["segments"]: + times.append(str(s["segment"][0]) + "," + str(s["segment"][1]) + "," + s["UUID"] + "," + s["category"]) + else: + times.append(str(segment["segment"][0]) + "," + str(segment["segment"][1]) + "," + segment["UUID"] + "," + segment["category"]) + 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() + times = [] + for category in sys.argv[5].split(","): + c.execute("SELECT startTime, endTime, votes, UUID, category FROM sponsorTimes WHERE videoID = ? AND shadowHidden = 0 AND votes > -1 AND category = ?", (sys.argv[4], category)) + 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"] + "," + time["category"]) + 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) + sys.exit(1) + except ConnectionResetError: + print("database update failed, connection reset", file=sys.stderr) + sys.exit(1) + except TimeoutError: + print("database update failed, timed out", file=sys.stderr) + sys.exit(1) + except urllib.error.URLError as e: + print("database update failed", file=sys.stderr) + print(e, file=sys.stderr) + + sys.exit(1) +elif sys.argv[1] == "submit": + try: + req = urllib.request.Request(sys.argv[3] + "/api/skipSegments", data=json.dumps({"videoID": sys.argv[4], "segments": [{"segment": [float(sys.argv[5]), float(sys.argv[6])], "category": sys.argv[9]}], "userID": uid}).encode(), headers={"Content-Type": "application/json"}) + response = urllib.request.urlopen(req) + 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 diff --git a/.config/mpv/scripts/sponsorblock_shared/sponsorblock.txt b/.config/mpv/scripts/sponsorblock_shared/sponsorblock.txt new file mode 100644 index 0000000..04debc8 --- /dev/null +++ b/.config/mpv/scripts/sponsorblock_shared/sponsorblock.txt @@ -0,0 +1 @@ +VvdShPVADjWuTg9L44tkj5B1IqOS8Jfak5XX \ No newline at end of file