mirror of
https://github.com/sahinakkaya/dotfiles.git
synced 2024-12-23 15:49:40 +01:00
598 lines
20 KiB
Lua
598 lines
20 KiB
Lua
-- 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)
|