dotfiles

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

sponsorblock.lua (15070B)


      1 -- sponsorblock.lua
      2 --
      3 -- This script skips sponsored segments of YouTube videos
      4 -- using data from https://github.com/ajayyy/SponsorBlock
      5 
      6 local ON_WINDOWS = package.config:sub(1,1) ~= '/'
      7 
      8 local options = {
      9     server_address = "https://api.sponsor.ajay.app",
     10 
     11     python_path = ON_WINDOWS and "python" or "python3",
     12 
     13     -- If true, sponsored segments will only be skipped once
     14     skip_once = true,
     15 
     16     -- Note that sponsored segments may ocasionally be inaccurate if this is turned off
     17     -- see https://ajay.app/blog.html#voting-and-pseudo-randomness-or-sponsorblock-or-youtube-sponsorship-segment-blocker
     18     local_database = true,
     19 
     20     -- Update database on first run, does nothing if local_database is false
     21     auto_update = true,
     22 
     23     -- User ID used to submit sponsored segments, leave blank for random
     24     user_id = "",
     25 
     26     -- Name to display on the stats page https://sponsor.ajay.app/stats/ leave blank to keep current name
     27     display_name = "",
     28 
     29     -- Tell the server when a skip happens
     30     report_views = true,
     31 
     32     -- Auto upvote skipped sponsors
     33     auto_upvote = true,
     34 
     35     -- Use sponsor times from server if they're more up to date than our local database
     36     server_fallback = true,
     37 
     38     -- Minimum duration for sponsors (in seconds), segments under that threshold will be ignored
     39     min_duration = 1,
     40 
     41     -- Fade audio for smoother transitions
     42     audio_fade = false,
     43 
     44     -- Audio fade step, applied once every 100ms until cap is reached
     45     audio_fade_step = 10,
     46 
     47     -- Audio fade cap
     48     audio_fade_cap = 0,
     49 
     50     -- Fast forward through sponsors instead of skipping
     51     fast_forward = false,
     52 
     53     -- Playback speed modifier when fast forwarding, applied once every second until cap is reached
     54     fast_forward_increase = .2,
     55 
     56     -- Playback speed cap
     57     fast_forward_cap = 2,
     58 
     59     -- Pattern for video id in local files, ignored if blank
     60     -- Recommended value for base youtube-dl is "-([%a%d%-_]+)%.[mw][kpe][v4b][m]?$"
     61     local_pattern = ""
     62 }
     63 
     64 mp.options = require "mp.options"
     65 mp.options.read_options(options, "sponsorblock")
     66 
     67 local legacy = mp.command_native_async == nil
     68 if legacy then
     69     options.local_database = false
     70 end
     71 
     72 local utils = require "mp.utils"
     73 local scripts_dir = mp.find_config_file("scripts")
     74 local sponsorblock = utils.join_path(scripts_dir, "shared/sponsorblock.py")
     75 local uid_path = utils.join_path(scripts_dir, "shared/sponsorblock.txt")
     76 local database_file = options.local_database and utils.join_path(scripts_dir, "shared/sponsorblock.db") or ""
     77 local youtube_id = nil
     78 local ranges = {}
     79 local init = false
     80 local segment = {a = 0, b = 0, progress = 0}
     81 local retrying = false
     82 local last_skip = {uuid = "", dir = nil}
     83 local speed_timer = nil
     84 local fade_timer = nil
     85 local fade_dir = nil
     86 local volume_before = mp.get_property_number("volume")
     87 
     88 function file_exists(name)
     89     local f = io.open(name,"r")
     90     if f ~= nil then io.close(f) return true else return false end
     91 end
     92 
     93 function t_count(t)
     94     local count = 0
     95     for _ in pairs(t) do count = count + 1 end
     96     return count
     97 end
     98 
     99 function getranges(_, exists, db, more)
    100     if type(exists) == "table" and exists["status"] == "1" then
    101         if options.server_fallback then
    102             mp.add_timeout(0, function() getranges(true, true, "") end)
    103         else
    104             return mp.osd_message("[sponsorblock] database update failed, gave up")
    105         end
    106     end
    107     if db ~= "" and db ~= database_file then db = database_file end
    108     if exists ~= true and not file_exists(db) then
    109         if not retrying then
    110             mp.osd_message("[sponsorblock] database update failed, retrying...")
    111             retrying = true
    112         end
    113         return update()
    114     end
    115     if retrying then
    116         mp.osd_message("[sponsorblock] database update succeeded")
    117         retrying = false
    118     end
    119     local sponsors
    120     local args = {
    121         options.python_path,
    122         sponsorblock,
    123         "ranges",
    124         db,
    125         options.server_address,
    126         youtube_id
    127     }
    128     if not legacy then
    129         sponsors = mp.command_native({name = "subprocess", capture_stdout = true, playback_only = false, args = args})
    130     else
    131         sponsors = utils.subprocess({args = args})
    132     end
    133     if not string.match(sponsors.stdout, "^%s*(.*%S)") then return end
    134     if string.match(sponsors.stdout, "error") then return getranges(true, true) end
    135     local new_ranges = {}
    136     local r_count = 0
    137     if more then r_count = -1 end
    138     for t in string.gmatch(sponsors.stdout, "[^:%s]+") do
    139         uuid = string.match(t, '[^,]+$')
    140         if ranges[uuid] then
    141             new_ranges[uuid] = ranges[uuid]
    142         else
    143             start_time = tonumber(string.match(t, '[^,]+'))
    144             end_time = tonumber(string.sub(string.match(t, ',[^,]+'), 2))
    145             if end_time - start_time >= options.min_duration then
    146                 new_ranges[uuid] = {
    147                     start_time = start_time,
    148                     end_time = end_time,
    149                     skipped = false
    150                 }
    151             end
    152         end
    153         r_count = r_count + 1
    154     end
    155     local c_count = t_count(ranges)
    156     if c_count == 0 or r_count >= c_count then
    157         ranges = new_ranges
    158     end
    159 end
    160 
    161 function fast_forward()
    162     local last_speed = mp.get_property_number("speed")
    163     local new_speed = math.min(last_speed + options.fast_forward_increase, options.fast_forward_cap)
    164     if new_speed <= last_speed then return end
    165     mp.set_property("speed", new_speed)
    166 end
    167 
    168 function fade_audio(step)
    169     local last_volume = mp.get_property_number("volume")
    170     local new_volume = math.max(options.audio_fade_cap, math.min(last_volume + step, volume_before))
    171     if new_volume == last_volume then
    172         if step >= 0 then fade_dir = nil end
    173         if fade_timer ~= nil then fade_timer:kill() end
    174         fade_timer = nil
    175         return
    176     end
    177     mp.set_property("volume", new_volume)
    178 end
    179 
    180 function skip_ads(name, pos)
    181     if pos == nil then return end
    182     local sponsor_ahead = false
    183     for uuid, t in pairs(ranges) do
    184         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
    185             if options.fast_forward == uuid then return end
    186             if options.fast_forward == false then
    187                 mp.osd_message("[sponsorblock] sponsor skipped")
    188                 mp.set_property("time-pos", t.end_time)
    189             else
    190                 mp.osd_message("[sponsorblock] skipping sponsor")
    191             end
    192             t.skipped = true
    193             last_skip = {uuid = uuid, dir = nil}
    194             if options.report_views or options.auto_upvote then
    195                 local args = {
    196                     options.python_path,
    197                     sponsorblock,
    198                     "stats",
    199                     database_file,
    200                     options.server_address,
    201                     youtube_id,
    202                     uuid,
    203                     options.report_views and "1" or "",
    204                     uid_path,
    205                     options.user_id,
    206                     options.auto_upvote and "1" or ""
    207                 }
    208                 if not legacy then
    209                     mp.command_native_async({name = "subprocess", playback_only = false, args = args}, function () end)
    210                 else
    211                     utils.subprocess_detached({args = args})
    212                 end
    213             end
    214             if options.fast_forward ~= false then
    215                 options.fast_forward = uuid
    216                 speed_timer = mp.add_periodic_timer(1, fast_forward)
    217             end
    218             return
    219         elseif (not options.skip_once or not t.skipped) and t.start_time <= pos + 1 and t.end_time > pos + 1 then
    220             sponsor_ahead = true
    221         end
    222     end
    223     if options.audio_fade then
    224         if sponsor_ahead then
    225             if fade_dir ~= false then
    226                 if fade_dir == nil then volume_before = mp.get_property_number("volume") end
    227                 if fade_timer ~= nil then fade_timer:kill() end
    228                 fade_dir = false
    229                 fade_timer = mp.add_periodic_timer(.1, function() fade_audio(-options.audio_fade_step) end)
    230             end
    231         elseif fade_dir == false then
    232             fade_dir = true
    233             if fade_timer ~= nil then fade_timer:kill() end
    234             fade_timer = mp.add_periodic_timer(.1, function() fade_audio(options.audio_fade_step) end)
    235         end
    236     end
    237     if options.fast_forward and options.fast_forward ~= true then
    238         options.fast_forward = true
    239         speed_timer:kill()
    240         mp.set_property("speed", 1)
    241     end
    242 end
    243 
    244 function vote(dir)
    245     if last_skip.uuid == "" then return mp.osd_message("[sponsorblock] no sponsors skipped, can't submit vote") end
    246     local updown = dir == "1" and "up" or "down"
    247     if last_skip.dir == dir then return mp.osd_message("[sponsorblock] " .. updown .. "vote already submitted") end
    248     last_skip.dir = dir
    249     local args = {
    250         options.python_path,
    251         sponsorblock,
    252         "stats",
    253         database_file,
    254         options.server_address,
    255         youtube_id,
    256         last_skip.uuid,
    257         "",
    258         uid_path,
    259         options.user_id,
    260         dir
    261     }
    262     if not legacy then
    263         mp.command_native_async({name = "subprocess", playback_only = false, args = args}, function () end)
    264     else
    265         utils.subprocess({args = args})
    266     end
    267     mp.osd_message("[sponsorblock] " .. updown .. "vote submitted")
    268 end
    269 
    270 function update()
    271     mp.command_native_async({name = "subprocess", playback_only = false, args = {
    272         options.python_path,
    273         sponsorblock,
    274         "update",
    275         database_file,
    276         options.server_address
    277     }}, getranges)
    278 end
    279 
    280 function file_loaded()
    281     local initialized = init
    282     ranges = {}
    283     segment = {a = 0, b = 0, progress = 0}
    284     last_skip = {uuid = "", dir = nil}
    285     local video_path = mp.get_property("path")
    286     local youtube_id1 = string.match(video_path, "https?://youtu%.be/([%a%d%-_]+).*")
    287     local youtube_id2 = string.match(video_path, "https?://w?w?w?%.?youtube%.com/v/([%a%d%-_]+).*")
    288     local youtube_id3 = string.match(video_path, "/watch%?v=([%a%d%-_]+).*")
    289     local youtube_id4 = string.match(video_path, "/embed/([%a%d%-_]+).*")
    290     local local_pattern = nil
    291     if options.local_pattern ~= "" then
    292         local_pattern = string.match(video_path, options.local_pattern)
    293     end
    294     youtube_id = youtube_id1 or youtube_id2 or youtube_id3 or youtube_id4 or local_pattern
    295     if not youtube_id then return end
    296     init = true
    297     if not options.local_database then
    298         getranges(true, true)
    299     else
    300         local exists = file_exists(database_file)
    301         if exists and options.server_fallback then
    302             getranges(true, true)
    303             mp.add_timeout(0, function() getranges(true, true, "", true) end)
    304         elseif exists then
    305             getranges(true, true)
    306         elseif options.server_fallback then
    307             mp.add_timeout(0, function() getranges(true, true, "") end)
    308         end
    309     end
    310     if initialized then return end
    311     mp.observe_property("time-pos", "native", skip_ads)
    312     if options.display_name ~= "" then
    313         local args = {
    314             options.python_path,
    315             sponsorblock,
    316             "username",
    317             database_file,
    318             options.server_address,
    319             youtube_id,
    320             "",
    321             "",
    322             uid_path,
    323             options.user_id,
    324             options.display_name
    325         }
    326         if not legacy then
    327             mp.command_native_async({name = "subprocess", playback_only = false, args = args}, function () end)
    328         else
    329             utils.subprocess_detached({args = args})
    330         end
    331     end
    332     if not options.local_database or (not options.auto_update and file_exists(database_file)) then return end
    333     update()
    334 end
    335 
    336 function set_segment()
    337     if not youtube_id then return end
    338     local pos = mp.get_property_number("time-pos")
    339     if pos == nil then return end
    340     if segment.progress > 1 then
    341         segment.progress = segment.progress - 2
    342     end
    343     if segment.progress == 1 then
    344         segment.progress = 0
    345         segment.b = pos
    346         mp.osd_message("[sponsorblock] segment boundary B set, press again for boundary A", 3)
    347     else
    348         segment.progress = 1
    349         segment.a = pos
    350         mp.osd_message("[sponsorblock] segment boundary A set, press again for boundary B", 3)
    351     end
    352 end
    353 
    354 function submit_segment()
    355     if not youtube_id then return end
    356     local start_time = math.min(segment.a, segment.b)
    357     local end_time = math.max(segment.a, segment.b)
    358     if end_time - start_time == 0 or end_time == 0 then
    359         mp.osd_message("[sponsorblock] empty segment, not submitting")
    360     elseif segment.progress <= 1 then
    361         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)
    362         segment.progress = segment.progress + 2
    363     else
    364         mp.osd_message("[sponsorblock] submitting segment...", 30)
    365         local submit
    366         local args = {
    367             options.python_path,
    368             sponsorblock,
    369             "submit",
    370             database_file,
    371             options.server_address,
    372             youtube_id,
    373             tostring(start_time),
    374             tostring(end_time),
    375             uid_path,
    376             options.user_id
    377         }
    378         if not legacy then
    379             submit = mp.command_native({name = "subprocess", capture_stdout = true, playback_only = false, args = args})
    380         else
    381             submit = utils.subprocess({args = args})
    382         end
    383         if string.match(submit.stdout, "success") then
    384             segment = {a = 0, b = 0, progress = 0}
    385             mp.osd_message("[sponsorblock] segment submitted")
    386         elseif string.match(submit.stdout, "error") then
    387             mp.osd_message("[sponsorblock] segment submission failed, server may be down. try again", 5)
    388         elseif string.match(submit.stdout, "502") then
    389             mp.osd_message("[sponsorblock] segment submission failed, server is down. try again", 5)
    390         elseif string.match(submit.stdout, "400") then
    391             mp.osd_message("[sponsorblock] segment submission failed, impossible inputs", 5)
    392             segment = {a = 0, b = 0, progress = 0}
    393         elseif string.match(submit.stdout, "429") then
    394             mp.osd_message("[sponsorblock] segment submission failed, rate limited. try again", 5)
    395         elseif string.match(submit.stdout, "409") then
    396             mp.osd_message("[sponsorblock] segment already submitted", 3)
    397             segment = {a = 0, b = 0, progress = 0}
    398         else
    399             mp.osd_message("[sponsorblock] segment submission failed", 5)
    400         end
    401     end
    402 end
    403 
    404 mp.register_event("file-loaded", file_loaded)
    405 mp.add_key_binding("g", "sponsorblock_set_segment", set_segment)
    406 mp.add_key_binding("G", "sponsorblock_submit_segment", submit_segment)
    407 mp.add_key_binding("h", "sponsorblock_upvote", function() return vote("1") end)
    408 mp.add_key_binding("H", "sponsorblock_downvote", function() return vote("0") end)