mirror of
https://github.com/ksyasuda/dotfiles.git
synced 2025-12-08 22:48:00 -08:00
add mpv
This commit is contained in:
1
.config/mpv/scripts/anilistUpdater/anilistUpdater.py
Symbolic link
1
.config/mpv/scripts/anilistUpdater/anilistUpdater.py
Symbolic link
@@ -0,0 +1 @@
|
||||
../../submodules/mpv-anilist-updater/anilistUpdater/anilistUpdater.py
|
||||
1
.config/mpv/scripts/anilistUpdater/main.lua
Symbolic link
1
.config/mpv/scripts/anilistUpdater/main.lua
Symbolic link
@@ -0,0 +1 @@
|
||||
../../submodules/mpv-anilist-updater/anilistUpdater/main.lua
|
||||
1
.config/mpv/scripts/animecards
Symbolic link
1
.config/mpv/scripts/animecards
Symbolic link
@@ -0,0 +1 @@
|
||||
../submodules/animecards/animecards
|
||||
1
.config/mpv/scripts/autosubsync-mpv
Symbolic link
1
.config/mpv/scripts/autosubsync-mpv
Symbolic link
@@ -0,0 +1 @@
|
||||
../submodules/autosubsync-mpv
|
||||
1
.config/mpv/scripts/immersion-tracker
Symbolic link
1
.config/mpv/scripts/immersion-tracker
Symbolic link
@@ -0,0 +1 @@
|
||||
../submodules/immersion-tracker
|
||||
388
.config/mpv/scripts/jimaku.js
Normal file
388
.config/mpv/scripts/jimaku.js
Normal file
@@ -0,0 +1,388 @@
|
||||
// Go to https://jimaku.cc/login and create a new account.
|
||||
// Then go to https://jimaku.cc/account and click the `Generate` button to create a new API key
|
||||
// Click the `Copy` button and paste it below
|
||||
var API_KEY = "";
|
||||
|
||||
// Configuration options
|
||||
var CONFIG = {
|
||||
// Filter the response to only have the specified episode
|
||||
prompt_episode: true,
|
||||
|
||||
// Subtitle suffix (e.g., ".JA" for Japanese subtitles)
|
||||
subtitle_suffix: ".JA",
|
||||
|
||||
// Preferred subtitle format (order matters, first is most preferred)
|
||||
preferred_formats: ["ass", "srt", "vtt"],
|
||||
|
||||
// Automatically load the subtitle after download
|
||||
auto_load: true,
|
||||
|
||||
// Default subtitle delay in seconds (can be positive or negative)
|
||||
default_delay: 0,
|
||||
|
||||
// Default subtitle font size
|
||||
default_font_size: 16,
|
||||
|
||||
// Automatically rename the subtitle file after download
|
||||
auto_rename: true,
|
||||
|
||||
// Automatically run autosubsync-mpv after downloading the subtitle
|
||||
run_auto_subsync: true
|
||||
};
|
||||
|
||||
// Keybindings
|
||||
// var MANUAL_SEARCH_KEY = "g";
|
||||
var FILENAME_AUTO_SEARCH_KEY = "ctrl+J";
|
||||
var PARENT_FOLDER_AUTO_SEARCH_KEY = "n";
|
||||
|
||||
function api(url, extraArgs) {
|
||||
var baseArgs = [
|
||||
"curl",
|
||||
"-s",
|
||||
"--url",
|
||||
url,
|
||||
"--header",
|
||||
"Authorization: " + API_KEY
|
||||
];
|
||||
|
||||
var args = Array.prototype.concat.apply(baseArgs, extraArgs);
|
||||
|
||||
var res = mp.command_native({
|
||||
name: "subprocess",
|
||||
playback_only: false,
|
||||
capture_stdout: true,
|
||||
capture_stderr: true,
|
||||
args: args
|
||||
});
|
||||
|
||||
if (res.stdout) return JSON.parse(res.stdout);
|
||||
}
|
||||
|
||||
function downloadSub(sub) {
|
||||
return api(sub.url, ["--output", sub.name]);
|
||||
}
|
||||
|
||||
function showMessage(message, persist) {
|
||||
var ass_start = mp.get_property_osd("osd-ass-cc/0");
|
||||
var ass_stop = mp.get_property_osd("osd-ass-cc/1");
|
||||
|
||||
mp.osd_message(
|
||||
ass_start + "{\\fs16}" + message + ass_stop,
|
||||
persist ? 999 : 2
|
||||
);
|
||||
}
|
||||
|
||||
// The timeout is neccessary due to a weird bug in mpv
|
||||
function inputGet(args) {
|
||||
mp.input.terminate();
|
||||
setTimeout(function () {
|
||||
mp.input.get(args);
|
||||
}, 1);
|
||||
}
|
||||
|
||||
// The timeout is neccessary due to a weird bug in mpv
|
||||
function inputSelect(args) {
|
||||
mp.input.terminate();
|
||||
setTimeout(function () {
|
||||
mp.input.select(args);
|
||||
}, 1);
|
||||
}
|
||||
|
||||
// Taken from mpv-subversive
|
||||
// https://github.com/nairyosangha/mpv-subversive/blob/master/backend/backend.lua#L146
|
||||
function sanitize(text) {
|
||||
var subPatterns = [
|
||||
/\.[a-zA-Z]+$/, // extension
|
||||
/\./g,
|
||||
/-/g,
|
||||
/_/g,
|
||||
/\[[^\]]+\]/g, // [] bracket
|
||||
/\([^\)]+\)/g, // () bracket
|
||||
/720[pP]/g,
|
||||
/480[pP]/g,
|
||||
/1080[pP]/g,
|
||||
/[xX]26[45]/g,
|
||||
/[bB]lu[-]?[rR]ay/g,
|
||||
/^[\s]*/,
|
||||
/[\s]*$/,
|
||||
/1920x1080/g,
|
||||
/1920X1080/g,
|
||||
/Hi10P/g,
|
||||
/FLAC/g,
|
||||
/AAC/g
|
||||
];
|
||||
|
||||
var result = text;
|
||||
|
||||
subPatterns.forEach(function (subPattern) {
|
||||
var newResult = result.replace(subPattern, " ");
|
||||
if (newResult.length > 0) {
|
||||
result = newResult;
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Adapted from mpv-subversive
|
||||
// https://github.com/nairyosangha/mpv-subversive/blob/master/backend/backend.lua#L164
|
||||
function extractTitle(text) {
|
||||
var matchers = [
|
||||
{ regex: /^([\w\s\d]+)[Ss]\d+[Ee]?\d+/, group: 1 },
|
||||
{ regex: /^([\w\s\d]+)-[\s]*\d+[\s]*[^\w]*$/, group: 1 },
|
||||
{ regex: /^([\w\s\d]+)[Ee]?[Pp]?[\s]+\d+$/, group: 1 },
|
||||
{ regex: /^([\w\s\d]+)[\s]\d+.*$/, group: 1 },
|
||||
{ regex: /^\d+[\s]*(.+)$/, group: 1 }
|
||||
];
|
||||
|
||||
for (var i = 0; i < matchers.length; i++) {
|
||||
var matcher = matchers[i];
|
||||
var match = text.match(matcher.regex);
|
||||
if (match) {
|
||||
return match[matcher.group].trim();
|
||||
}
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
function getNames(results) {
|
||||
return results.map(function (item) {
|
||||
return item.name;
|
||||
});
|
||||
}
|
||||
|
||||
function runAutoSubSyncMPV() {
|
||||
try {
|
||||
mp.command_native(["script-binding", "autosubsync-menu"]);
|
||||
} catch (e) {
|
||||
showMessage("autosubsync-mpv not installed");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function selectSub(selectedSub) {
|
||||
showMessage("Downloading: " + selectedSub.name);
|
||||
|
||||
try {
|
||||
downloadSub(selectedSub);
|
||||
|
||||
// Get current video filename without extension
|
||||
var videoPath = mp.get_property("path");
|
||||
if (!videoPath) {
|
||||
throw new Error("No video file is currently playing");
|
||||
}
|
||||
var videoName = videoPath.substring(0, videoPath.lastIndexOf("."));
|
||||
|
||||
// Get subtitle extension
|
||||
var subExt = selectedSub.name.substring(selectedSub.name.lastIndexOf("."));
|
||||
|
||||
var newSubName = selectedSub.name;
|
||||
if (CONFIG.auto_rename) {
|
||||
// Create new subtitle filename
|
||||
newSubName = videoName + CONFIG.subtitle_suffix + subExt;
|
||||
|
||||
// Rename the downloaded subtitle file
|
||||
var renameResult = mp.command_native({
|
||||
name: "subprocess",
|
||||
playback_only: false,
|
||||
args: ["mv", selectedSub.name, newSubName]
|
||||
});
|
||||
|
||||
if (renameResult.error) {
|
||||
throw new Error(
|
||||
"Failed to rename subtitle file: " + renameResult.error
|
||||
);
|
||||
}
|
||||
|
||||
showMessage(newSubName + " downloaded and renamed");
|
||||
} else {
|
||||
showMessage(newSubName + " downloaded");
|
||||
}
|
||||
|
||||
if (CONFIG.auto_load) {
|
||||
mp.commandv("sub_add", newSubName);
|
||||
showMessage(newSubName + " added");
|
||||
|
||||
// Apply subtitle settings if configured
|
||||
if (CONFIG.default_delay !== 0) {
|
||||
mp.commandv("sub_delay", CONFIG.default_delay);
|
||||
}
|
||||
if (CONFIG.default_font_size !== 16) {
|
||||
mp.commandv("sub_font_size", CONFIG.default_font_size);
|
||||
}
|
||||
}
|
||||
|
||||
if (CONFIG.run_auto_subsync) {
|
||||
runAutoSubSyncMPV();
|
||||
}
|
||||
|
||||
mp.set_property("pause", "no");
|
||||
} catch (error) {
|
||||
showMessage("Error: " + error.message, true);
|
||||
mp.set_property("pause", "no");
|
||||
}
|
||||
}
|
||||
|
||||
function sortByPreferredFormat(files) {
|
||||
return files.sort(function (a, b) {
|
||||
var extA = a.name.substring(a.name.lastIndexOf(".") + 1).toLowerCase();
|
||||
var extB = b.name.substring(b.name.lastIndexOf(".") + 1).toLowerCase();
|
||||
|
||||
var indexA = CONFIG.preferred_formats.indexOf(extA);
|
||||
var indexB = CONFIG.preferred_formats.indexOf(extB);
|
||||
|
||||
if (indexA === -1) return 1;
|
||||
if (indexB === -1) return -1;
|
||||
return indexA - indexB;
|
||||
});
|
||||
}
|
||||
|
||||
function selectEpisode(anime, episode) {
|
||||
mp.input.terminate();
|
||||
var episodeResults;
|
||||
|
||||
if (episode) {
|
||||
showMessage("Fetching subs for: " + anime.name + " episode " + episode);
|
||||
episodeResults = api(
|
||||
"https://jimaku.cc/api/entries/" + anime.id + "/files?episode=" + episode
|
||||
);
|
||||
} else {
|
||||
showMessage("Fetching all subs for: " + anime.name);
|
||||
episodeResults = api(
|
||||
"https://jimaku.cc/api/entries/" + anime.id + "/files"
|
||||
);
|
||||
}
|
||||
|
||||
if (episodeResults.error) {
|
||||
showMessage("Error: " + animeResults.error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (episodeResults.length === 0) {
|
||||
showMessage("No results found");
|
||||
return;
|
||||
}
|
||||
|
||||
// Sort results by preferred format
|
||||
episodeResults = sortByPreferredFormat(episodeResults);
|
||||
|
||||
if (episodeResults.length === 1) {
|
||||
var selectedEpisode = episodeResults[0];
|
||||
selectSub(selectedEpisode);
|
||||
return;
|
||||
}
|
||||
|
||||
var items = getNames(episodeResults);
|
||||
|
||||
inputSelect({
|
||||
prompt: "Select episode: ",
|
||||
items: items,
|
||||
submit: function (id) {
|
||||
var selectedEpisode = episodeResults[id - 1];
|
||||
selectSub(selectedEpisode);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function onAnimeSelected(anime) {
|
||||
if (CONFIG.prompt_episode) {
|
||||
inputGet({
|
||||
prompt: "Episode (leave blank for all): ",
|
||||
submit: function (episode) {
|
||||
selectEpisode(anime, episode);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
selectEpisode(anime);
|
||||
}
|
||||
}
|
||||
|
||||
function search(searchTerm, isAuto) {
|
||||
mp.input.terminate();
|
||||
showMessage('Searching for: "' + searchTerm + '"');
|
||||
|
||||
var animeResults = api(
|
||||
encodeURI(
|
||||
"https://jimaku.cc/api/entries/search?anime=true&query=" + searchTerm
|
||||
)
|
||||
);
|
||||
|
||||
if (animeResults.error) {
|
||||
showMessage("Error: " + animeResults.error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (animeResults.length === 0) {
|
||||
showMessage("No results found");
|
||||
if (isAuto) {
|
||||
manualSearch(searchTerm);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (animeResults.length === 1) {
|
||||
var selectedAnime = animeResults[0];
|
||||
onAnimeSelected(selectedAnime);
|
||||
return;
|
||||
}
|
||||
|
||||
var items = getNames(animeResults);
|
||||
|
||||
inputSelect({
|
||||
prompt: "Select anime: ",
|
||||
items: items,
|
||||
submit: function (id) {
|
||||
var selectedAnime = animeResults[id - 1];
|
||||
showMessage(selectedAnime.name, true);
|
||||
onAnimeSelected(selectedAnime);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function manualSearch(defaultText) {
|
||||
inputGet({
|
||||
prompt: "Search term: ",
|
||||
submit: search,
|
||||
default_text: defaultText
|
||||
});
|
||||
|
||||
mp.set_property("pause", "yes");
|
||||
showMessage("Manual Jimaku Search", true);
|
||||
}
|
||||
|
||||
function autoSearch() {
|
||||
var filename = mp.get_property("filename");
|
||||
var sanitizedFilename = sanitize(filename);
|
||||
var currentAnime = extractTitle(sanitizedFilename);
|
||||
|
||||
mp.set_property("pause", "yes");
|
||||
|
||||
search(currentAnime, true);
|
||||
}
|
||||
|
||||
function autoSearchParentFolder() {
|
||||
var path = mp.get_property("stream-open-filename");
|
||||
var pathSplit = path.split(path.indexOf("/") >= 0 ? "/" : "\\");
|
||||
var filename =
|
||||
pathSplit.length === 1 ? pathSplit[0] : pathSplit[pathSplit.length - 2];
|
||||
|
||||
var sanitizedFilename = sanitize(filename);
|
||||
var currentAnime = extractTitle(sanitizedFilename);
|
||||
|
||||
mp.set_property("pause", "yes");
|
||||
|
||||
search(currentAnime, true);
|
||||
}
|
||||
|
||||
// mp.add_key_binding(MANUAL_SEARCH_KEY, "jimaku-manual-search", manualSearch);
|
||||
mp.add_key_binding(
|
||||
FILENAME_AUTO_SEARCH_KEY,
|
||||
"jimaku-filename-auto-search",
|
||||
autoSearch
|
||||
);
|
||||
mp.add_key_binding(
|
||||
PARENT_FOLDER_AUTO_SEARCH_KEY,
|
||||
"jimaku-parent-folder-auto-search",
|
||||
autoSearchParentFolder
|
||||
);
|
||||
1
.config/mpv/scripts/modernz.lua
Symbolic link
1
.config/mpv/scripts/modernz.lua
Symbolic link
@@ -0,0 +1 @@
|
||||
../submodules/ModernZ/modernz.lua
|
||||
1
.config/mpv/scripts/mpv-youtube-queue.lua
Symbolic link
1
.config/mpv/scripts/mpv-youtube-queue.lua
Symbolic link
@@ -0,0 +1 @@
|
||||
../submodules/mpv-youtube-queue/mpv-youtube-queue.lua
|
||||
1025
.config/mpv/scripts/pause-indicator.lua
Normal file
1025
.config/mpv/scripts/pause-indicator.lua
Normal file
File diff suppressed because it is too large
Load Diff
416
.config/mpv/scripts/reload.lua
Normal file
416
.config/mpv/scripts/reload.lua
Normal file
@@ -0,0 +1,416 @@
|
||||
-- reload.lua
|
||||
--
|
||||
-- When an online video is stuck buffering or got very slow CDN
|
||||
-- source, restarting often helps. This script provides automatic
|
||||
-- reloading of videos that doesn't have buffering progress for some
|
||||
-- time while keeping the current time position. It also adds `Ctrl+r`
|
||||
-- keybinding to reload video manually.
|
||||
--
|
||||
-- SETTINGS
|
||||
--
|
||||
-- To override default setting put the `lua-settings/reload.conf` file in
|
||||
-- mpv user folder, on linux it is `~/.config/mpv`. NOTE: config file
|
||||
-- name should match the name of the script.
|
||||
--
|
||||
-- Default `reload.conf` settings:
|
||||
--
|
||||
-- ```
|
||||
-- # enable automatic reload on timeout
|
||||
-- # when paused-for-cache event fired, we will wait
|
||||
-- # paused_for_cache_timer_timeout sedonds and then reload the video
|
||||
-- paused_for_cache_timer_enabled=yes
|
||||
--
|
||||
-- # checking paused_for_cache property interval in seconds,
|
||||
-- # can not be less than 0.05 (50 ms)
|
||||
-- paused_for_cache_timer_interval=1
|
||||
--
|
||||
-- # time in seconds to wait until reload
|
||||
-- paused_for_cache_timer_timeout=10
|
||||
--
|
||||
-- # enable automatic reload based on demuxer cache
|
||||
-- # if demuxer-cache-time property didn't change in demuxer_cache_timer_timeout
|
||||
-- # time interval, the video will be reloaded as soon as demuxer cache depleated
|
||||
-- demuxer_cache_timer_enabled=yes
|
||||
--
|
||||
-- # checking demuxer-cache-time property interval in seconds,
|
||||
-- # can not be less than 0.05 (50 ms)
|
||||
-- demuxer_cache_timer_interval=2
|
||||
--
|
||||
-- # if demuxer cache didn't receive any data during demuxer_cache_timer_timeout
|
||||
-- # we decide that it has no progress and will reload the stream when
|
||||
-- # paused_for_cache event happens
|
||||
-- demuxer_cache_timer_timeout=20
|
||||
--
|
||||
-- # when the end-of-file is reached, reload the stream to check
|
||||
-- # if there is more content available.
|
||||
-- reload_eof_enabled=no
|
||||
--
|
||||
-- # keybinding to reload stream from current time position
|
||||
-- # you can disable keybinding by setting it to empty value
|
||||
-- # reload_key_binding=
|
||||
-- reload_key_binding=Ctrl+r
|
||||
-- ```
|
||||
--
|
||||
-- DEBUGGING
|
||||
--
|
||||
-- Debug messages will be printed to stdout with mpv command line option
|
||||
-- `--msg-level='reload=debug'`. You may also need to add the `--no-msg-color`
|
||||
-- option to make the debug logs visible if you are using a dark colorscheme
|
||||
-- in terminal.
|
||||
local msg = require 'mp.msg'
|
||||
local options = require 'mp.options'
|
||||
local utils = require 'mp.utils'
|
||||
|
||||
local settings = {
|
||||
paused_for_cache_timer_enabled = true,
|
||||
paused_for_cache_timer_interval = 1,
|
||||
paused_for_cache_timer_timeout = 10,
|
||||
demuxer_cache_timer_enabled = true,
|
||||
demuxer_cache_timer_interval = 2,
|
||||
demuxer_cache_timer_timeout = 20,
|
||||
reload_eof_enabled = false,
|
||||
reload_key_binding = "Ctrl+r"
|
||||
}
|
||||
|
||||
-- global state stores properties between reloads
|
||||
local property_path = nil
|
||||
local property_time_pos = 0
|
||||
local property_keep_open = nil
|
||||
|
||||
-- FSM managing the demuxer cache.
|
||||
--
|
||||
-- States:
|
||||
--
|
||||
-- * fetch - fetching new data
|
||||
-- * stale - unable to fetch new data for time < 'demuxer_cache_timer_timeout'
|
||||
-- * stuck - unable to fetch new data for time >= 'demuxer_cache_timer_timeout'
|
||||
--
|
||||
-- State transitions:
|
||||
--
|
||||
-- +---------------------------+
|
||||
-- v |
|
||||
-- +-------+ +-------+ +-------+
|
||||
-- + fetch +<--->+ stale +---->+ stuck |
|
||||
-- +-------+ +-------+ +-------+
|
||||
-- | ^ | ^ | ^
|
||||
-- +---+ +---+ +---+
|
||||
local demuxer_cache = {
|
||||
timer = nil,
|
||||
|
||||
state = { name = 'uninitialized', demuxer_cache_time = 0, in_state_time = 0 },
|
||||
|
||||
events = {
|
||||
continue_fetch = { name = 'continue_fetch', from = 'fetch', to = 'fetch' },
|
||||
continue_stale = { name = 'continue_stale', from = 'stale', to = 'stale' },
|
||||
continue_stuck = { name = 'continue_stuck', from = 'stuck', to = 'stuck' },
|
||||
fetch_to_stale = { name = 'fetch_to_stale', from = 'fetch', to = 'stale' },
|
||||
stale_to_fetch = { name = 'stale_to_fetch', from = 'stale', to = 'fetch' },
|
||||
stale_to_stuck = { name = 'stale_to_stuck', from = 'stale', to = 'stuck' },
|
||||
stuck_to_fetch = { name = 'stuck_to_fetch', from = 'stuck', to = 'fetch' }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
-- Always start with 'fetch' state
|
||||
function demuxer_cache.reset_state()
|
||||
demuxer_cache.state = {
|
||||
name = demuxer_cache.events.continue_fetch.to,
|
||||
demuxer_cache_time = 0,
|
||||
in_state_time = 0
|
||||
}
|
||||
end
|
||||
|
||||
-- Has 'demuxer_cache_time' changed
|
||||
function demuxer_cache.has_progress_since(t)
|
||||
return demuxer_cache.state.demuxer_cache_time ~= t
|
||||
end
|
||||
|
||||
function demuxer_cache.is_state_fetch()
|
||||
return demuxer_cache.state.name == demuxer_cache.events.continue_fetch.to
|
||||
end
|
||||
|
||||
function demuxer_cache.is_state_stale()
|
||||
return demuxer_cache.state.name == demuxer_cache.events.continue_stale.to
|
||||
end
|
||||
|
||||
function demuxer_cache.is_state_stuck()
|
||||
return demuxer_cache.state.name == demuxer_cache.events.continue_stuck.to
|
||||
end
|
||||
|
||||
function demuxer_cache.transition(event)
|
||||
if demuxer_cache.state.name == event.from then
|
||||
-- state setup
|
||||
demuxer_cache.state.demuxer_cache_time = event.demuxer_cache_time
|
||||
|
||||
if event.name == 'continue_fetch' then
|
||||
demuxer_cache.state.in_state_time = demuxer_cache.state
|
||||
.in_state_time +
|
||||
event.interval
|
||||
elseif event.name == 'continue_stale' then
|
||||
demuxer_cache.state.in_state_time = demuxer_cache.state
|
||||
.in_state_time +
|
||||
event.interval
|
||||
elseif event.name == 'continue_stuck' then
|
||||
demuxer_cache.state.in_state_time = demuxer_cache.state
|
||||
.in_state_time +
|
||||
event.interval
|
||||
elseif event.name == 'fetch_to_stale' then
|
||||
demuxer_cache.state.in_state_time = 0
|
||||
elseif event.name == 'stale_to_fetch' then
|
||||
demuxer_cache.state.in_state_time = 0
|
||||
elseif event.name == 'stale_to_stuck' then
|
||||
demuxer_cache.state.in_state_time = 0
|
||||
elseif event.name == 'stuck_to_fetch' then
|
||||
demuxer_cache.state.in_state_time = 0
|
||||
end
|
||||
|
||||
-- state transition
|
||||
demuxer_cache.state.name = event.to
|
||||
|
||||
msg.debug('demuxer_cache.transition', event.name,
|
||||
utils.to_string(demuxer_cache.state))
|
||||
else
|
||||
msg.error('demuxer_cache.transition', 'illegal transition', event.name,
|
||||
'from state', demuxer_cache.state.name)
|
||||
end
|
||||
end
|
||||
|
||||
function demuxer_cache.initialize(demuxer_cache_timer_interval)
|
||||
demuxer_cache.reset_state()
|
||||
demuxer_cache.timer = mp.add_periodic_timer(demuxer_cache_timer_interval,
|
||||
function()
|
||||
demuxer_cache.demuxer_cache_timer_tick(
|
||||
mp.get_property_native('demuxer-cache-time'),
|
||||
demuxer_cache_timer_interval)
|
||||
end)
|
||||
end
|
||||
|
||||
-- If there is no progress of demuxer_cache_time in
|
||||
-- settings.demuxer_cache_timer_timeout time interval switch state to
|
||||
-- 'stuck' and switch back to 'fetch' as soon as any progress is made
|
||||
function demuxer_cache.demuxer_cache_timer_tick(demuxer_cache_time,
|
||||
demuxer_cache_timer_interval)
|
||||
local event = nil
|
||||
local cache_has_progress = demuxer_cache.has_progress_since(
|
||||
demuxer_cache_time)
|
||||
|
||||
-- I miss pattern matching so much
|
||||
if demuxer_cache.is_state_fetch() then
|
||||
if cache_has_progress then
|
||||
event = demuxer_cache.events.continue_fetch
|
||||
else
|
||||
event = demuxer_cache.events.fetch_to_stale
|
||||
end
|
||||
elseif demuxer_cache.is_state_stale() then
|
||||
if cache_has_progress then
|
||||
event = demuxer_cache.events.stale_to_fetch
|
||||
elseif demuxer_cache.state.in_state_time <
|
||||
settings.demuxer_cache_timer_timeout then
|
||||
event = demuxer_cache.events.continue_stale
|
||||
else
|
||||
event = demuxer_cache.events.stale_to_stuck
|
||||
end
|
||||
elseif demuxer_cache.is_state_stuck() then
|
||||
if cache_has_progress then
|
||||
event = demuxer_cache.events.stuck_to_fetch
|
||||
else
|
||||
event = demuxer_cache.events.continue_stuck
|
||||
end
|
||||
end
|
||||
|
||||
event.demuxer_cache_time = demuxer_cache_time
|
||||
event.interval = demuxer_cache_timer_interval
|
||||
demuxer_cache.transition(event)
|
||||
end
|
||||
|
||||
local paused_for_cache = { timer = nil, time = 0 }
|
||||
|
||||
function paused_for_cache.reset_timer()
|
||||
msg.debug('paused_for_cache.reset_timer', paused_for_cache.time)
|
||||
if paused_for_cache.timer then
|
||||
paused_for_cache.timer:kill()
|
||||
paused_for_cache.timer = nil
|
||||
paused_for_cache.time = 0
|
||||
end
|
||||
end
|
||||
|
||||
function paused_for_cache.start_timer(interval_seconds, timeout_seconds)
|
||||
msg.debug('paused_for_cache.start_timer', paused_for_cache.time)
|
||||
if not paused_for_cache.timer then
|
||||
paused_for_cache.timer = mp.add_periodic_timer(interval_seconds,
|
||||
function()
|
||||
paused_for_cache.time = paused_for_cache.time + interval_seconds
|
||||
if paused_for_cache.time >= timeout_seconds then
|
||||
paused_for_cache.reset_timer()
|
||||
reload_resume()
|
||||
end
|
||||
msg.debug('paused_for_cache', 'tick', paused_for_cache.time)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
function paused_for_cache.handler(property, is_paused)
|
||||
if is_paused then
|
||||
if demuxer_cache.is_state_stuck() then
|
||||
msg.info("demuxer cache has no progress")
|
||||
-- reset demuxer state to avoid immediate reload if
|
||||
-- paused_for_cache event triggered right after reload
|
||||
demuxer_cache.reset_state()
|
||||
reload_resume()
|
||||
end
|
||||
|
||||
paused_for_cache.start_timer(settings.paused_for_cache_timer_interval,
|
||||
settings.paused_for_cache_timer_timeout)
|
||||
else
|
||||
paused_for_cache.reset_timer()
|
||||
end
|
||||
end
|
||||
|
||||
function read_settings()
|
||||
options.read_options(settings, mp.get_script_name())
|
||||
msg.debug(utils.to_string(settings))
|
||||
end
|
||||
|
||||
function reload(path, time_pos)
|
||||
msg.debug("reload", path, time_pos)
|
||||
if time_pos == nil then
|
||||
mp.commandv("loadfile", path, "replace")
|
||||
else
|
||||
local success = mp.commandv("loadfile", path, "replace", -1,
|
||||
"start=+" .. time_pos)
|
||||
-- fallback to old syntax of loadfile for compatibility
|
||||
if success == nil then
|
||||
msg.warn(
|
||||
"old loadfile syntax detected. falling back to using old syntax. update mpv to remove this warning")
|
||||
mp.commandv("loadfile", path, "replace", "start=+" .. time_pos)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function reload_resume()
|
||||
local path = mp.get_property("path", property_path)
|
||||
local time_pos = mp.get_property("time-pos")
|
||||
local reload_duration = mp.get_property_native("duration")
|
||||
|
||||
local playlist_count = mp.get_property_number("playlist/count")
|
||||
local playlist_pos = mp.get_property_number("playlist-pos")
|
||||
local playlist = {}
|
||||
for i = 0, playlist_count - 1 do
|
||||
playlist[i] = mp.get_property("playlist/" .. i .. "/filename")
|
||||
end
|
||||
-- Tries to determine live stream vs. pre-recordered VOD. VOD has non-zero
|
||||
-- duration property. When reloading VOD, to keep the current time position
|
||||
-- we should provide offset from the start. Stream doesn't have fixed start.
|
||||
-- Decent choice would be to reload stream from it's current 'live' positon.
|
||||
-- That's the reason we don't pass the offset when reloading streams.
|
||||
if reload_duration and reload_duration > 0 then
|
||||
msg.info("reloading video from", time_pos, "second")
|
||||
reload(path, time_pos)
|
||||
-- VODs get stuck when reload is called without a time_pos
|
||||
-- this is most noticeable in youtube videos whenever download gets stuck in the first frames
|
||||
-- video would stay paused without being actually paused
|
||||
-- issue surfaced in mpv 0.33, afaik
|
||||
elseif reload_duration and reload_duration == 0 then
|
||||
msg.info("reloading video from", time_pos, "second")
|
||||
reload(path, time_pos)
|
||||
else
|
||||
msg.info("reloading stream")
|
||||
reload(path, nil)
|
||||
end
|
||||
msg.info("file", playlist_pos + 1, "of", playlist_count, "in playlist")
|
||||
for i = 0, playlist_pos - 1 do
|
||||
mp.commandv("loadfile", playlist[i], "append")
|
||||
end
|
||||
mp.commandv("playlist-move", 0, playlist_pos + 1)
|
||||
for i = playlist_pos + 1, playlist_count - 1 do
|
||||
mp.commandv("loadfile", playlist[i], "append")
|
||||
end
|
||||
end
|
||||
|
||||
function reload_eof(property, eof_reached)
|
||||
msg.debug("reload_eof", property, eof_reached)
|
||||
local time_pos = mp.get_property_number("time-pos")
|
||||
local duration = mp.get_property_number("duration")
|
||||
|
||||
if eof_reached and round(time_pos) == round(duration) then
|
||||
msg.debug("property_time_pos", property_time_pos, "time_pos", time_pos)
|
||||
|
||||
-- Check that playback time_pos made progress after the last reload. When
|
||||
-- eof is reached we try to reload the video, in case there is more content
|
||||
-- available. If time_pos stayed the same after reload, it means that the
|
||||
-- video length stayed the same, and we can end the playback.
|
||||
if round(property_time_pos) == round(time_pos) then
|
||||
msg.info("eof reached, playback ended")
|
||||
mp.set_property("keep-open", property_keep_open)
|
||||
else
|
||||
msg.info("eof reached, checking if more content available")
|
||||
reload_resume()
|
||||
mp.set_property_bool("pause", false)
|
||||
property_time_pos = time_pos
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function on_file_loaded(event)
|
||||
local debug_info = {
|
||||
event = event,
|
||||
time_pos = mp.get_property("time-pos"),
|
||||
stream_pos = mp.get_property("stream-pos"),
|
||||
stream_end = mp.get_property("stream-end"),
|
||||
duration = mp.get_property("duration"),
|
||||
seekable = mp.get_property("seekable"),
|
||||
pause = mp.get_property("pause"),
|
||||
paused_for_cache = mp.get_property("paused-for-cache"),
|
||||
cache_buffering_state = mp.get_property("cache-buffering-state")
|
||||
}
|
||||
msg.debug("debug_info", utils.to_string(debug_info))
|
||||
|
||||
-- When the video is reloaded after being paused for cache, it won't start
|
||||
-- playing again while all properties looks fine:
|
||||
-- `pause=no`, `paused-for-cache=no` and `cache-buffering-state=100`.
|
||||
-- As a workaround, we cycle through the paused state by sending two SPACE
|
||||
-- keypresses.
|
||||
-- What didn't work:
|
||||
-- - Cycling through the `pause` property.
|
||||
-- - Run the `playlist-play-index current` command.
|
||||
mp.commandv("keypress", 'SPACE')
|
||||
mp.commandv("keypress", 'SPACE')
|
||||
end
|
||||
|
||||
-- Round positive numbers.
|
||||
function round(num) return math.floor(num + 0.5) end
|
||||
|
||||
-- main
|
||||
|
||||
read_settings()
|
||||
|
||||
if settings.reload_key_binding ~= "" then
|
||||
mp.add_key_binding(settings.reload_key_binding, "reload_resume",
|
||||
reload_resume)
|
||||
end
|
||||
|
||||
if settings.paused_for_cache_timer_enabled then
|
||||
mp.observe_property("paused-for-cache", "bool", paused_for_cache.handler)
|
||||
end
|
||||
|
||||
if settings.demuxer_cache_timer_enabled then
|
||||
demuxer_cache.initialize(settings.demuxer_cache_timer_interval)
|
||||
end
|
||||
|
||||
if settings.reload_eof_enabled then
|
||||
-- vo-configured == video output created && its configuration went ok
|
||||
mp.observe_property("vo-configured", "bool", function(name, vo_configured)
|
||||
msg.debug(name, vo_configured)
|
||||
if vo_configured then
|
||||
property_path = mp.get_property("path")
|
||||
property_keep_open = mp.get_property("keep-open")
|
||||
mp.set_property("keep-open", "yes")
|
||||
mp.set_property("keep-open-pause", "no")
|
||||
end
|
||||
end)
|
||||
|
||||
mp.observe_property("eof-reached", "bool", reload_eof)
|
||||
end
|
||||
|
||||
mp.register_event("file-loaded", on_file_loaded)
|
||||
|
||||
83
.config/mpv/scripts/run_websocket_server.lua
Normal file
83
.config/mpv/scripts/run_websocket_server.lua
Normal file
@@ -0,0 +1,83 @@
|
||||
-- mpv_websocket
|
||||
-- https://github.com/kuroahna/mpv_websocket
|
||||
|
||||
local utils = require("mp.utils")
|
||||
|
||||
local platform = mp.get_property_native("platform")
|
||||
|
||||
local config_file_path = mp.find_config_file("mpv.conf")
|
||||
local config_folder_path, config_file = utils.split_path(config_file_path)
|
||||
local mpv_websocket_path =
|
||||
utils.join_path(config_folder_path, platform == "windows" and "mpv_websocket.exe" or "mpv_websocket")
|
||||
local initialised_websocket
|
||||
|
||||
local _, err = utils.file_info(config_file_path)
|
||||
if err then
|
||||
error("failed to open mpv config file `" .. config_file_path .. "`")
|
||||
end
|
||||
|
||||
local _, err = utils.file_info(mpv_websocket_path)
|
||||
if err then
|
||||
error("failed to open mpv_websocket")
|
||||
end
|
||||
|
||||
local function find_mpv_socket(config_file_path)
|
||||
local file = io.open(config_file_path, "r")
|
||||
if file == nil then
|
||||
error("failed to read mpv config file `" .. config_file_path .. "`")
|
||||
end
|
||||
|
||||
local mpv_socket
|
||||
for line in file:lines() do
|
||||
mpv_socket = line:match("^input%-ipc%-server%s*=%s*(%g+)%s*")
|
||||
if mpv_socket then
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
file:close()
|
||||
|
||||
if not mpv_socket then
|
||||
error("input-ipc-server option does not exist in `" .. config_file_path .. "`")
|
||||
end
|
||||
|
||||
return mpv_socket
|
||||
end
|
||||
|
||||
local mpv_socket = find_mpv_socket(config_file_path)
|
||||
if platform == "windows" then
|
||||
mpv_socket = "\\\\.\\pipe" .. mpv_socket:gsub("/", "\\")
|
||||
end
|
||||
|
||||
local function start_websocket()
|
||||
initialised_websocket = mp.command_native_async({
|
||||
name = "subprocess",
|
||||
playback_only = false,
|
||||
capture_stdout = true,
|
||||
capture_stderr = true,
|
||||
args = {
|
||||
mpv_websocket_path,
|
||||
"-m",
|
||||
mpv_socket,
|
||||
"-w",
|
||||
"6677",
|
||||
},
|
||||
})
|
||||
end
|
||||
|
||||
local function end_websocket()
|
||||
mp.abort_async_command(initialised_websocket)
|
||||
initialised_websocket = nil
|
||||
end
|
||||
|
||||
local function toggle_websocket()
|
||||
local paused = mp.get_property_bool("pause")
|
||||
if initialised_websocket and paused then
|
||||
end_websocket()
|
||||
elseif not initialised_websocket and not paused then
|
||||
start_websocket()
|
||||
end
|
||||
end
|
||||
|
||||
mp.register_script_message("togglewebsocket", toggle_websocket)
|
||||
start_websocket()
|
||||
679
.config/mpv/scripts/sponsorblock.lua
Normal file
679
.config/mpv/scripts/sponsorblock.lua
Normal file
@@ -0,0 +1,679 @@
|
||||
-- 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,filler",
|
||||
|
||||
-- Categories to skip automatically
|
||||
skip_categories = "sponsor",
|
||||
|
||||
-- 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 = 0.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
|
||||
--]]
|
||||
options.local_database = false
|
||||
|
||||
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", "preview", "music_offtopic", "filler" }
|
||||
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 - 0.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(0.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(0.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)
|
||||
if youtube_id then
|
||||
break
|
||||
end
|
||||
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("ctrl+g", "set_segment", set_segment)
|
||||
mp.add_key_binding("ctrl+G", "submit_segment", submit_segment)
|
||||
mp.add_key_binding("ctrl+h", "upvote_segment", function()
|
||||
return vote("1")
|
||||
end)
|
||||
mp.add_key_binding("ctrl+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)
|
||||
3
.config/mpv/scripts/sponsorblock_shared/main.lua
Normal file
3
.config/mpv/scripts/sponsorblock_shared/main.lua
Normal file
@@ -0,0 +1,3 @@
|
||||
-- This is a dummy main.lua
|
||||
-- required for mpv 0.33
|
||||
-- do not delete
|
||||
122
.config/mpv/scripts/sponsorblock_shared/sponsorblock.py
Normal file
122
.config/mpv/scripts/sponsorblock_shared/sponsorblock.py
Normal file
@@ -0,0 +1,122 @@
|
||||
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:
|
||||
print("database update failed", 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
|
||||
1
.config/mpv/scripts/sponsorblock_shared/sponsorblock.txt
Normal file
1
.config/mpv/scripts/sponsorblock_shared/sponsorblock.txt
Normal file
@@ -0,0 +1 @@
|
||||
dFlkoCOSK3BQhXGGhUsXSNU0sPr7AV7avndk
|
||||
1
.config/mpv/scripts/subs2srs
Symbolic link
1
.config/mpv/scripts/subs2srs
Symbolic link
@@ -0,0 +1 @@
|
||||
../submodules/mpvacious
|
||||
1
.config/mpv/scripts/thumbfast.lua
Symbolic link
1
.config/mpv/scripts/thumbfast.lua
Symbolic link
@@ -0,0 +1 @@
|
||||
../submodules/thumbfast/thumbfast.lua
|
||||
1
.config/mpv/scripts/youtube-upnext.lua
Symbolic link
1
.config/mpv/scripts/youtube-upnext.lua
Symbolic link
@@ -0,0 +1 @@
|
||||
../submodules/mpv-youtube-upnext/youtube-upnext.lua
|
||||
1
.config/mpv/scripts/ytdl-preload.lua
Symbolic link
1
.config/mpv/scripts/ytdl-preload.lua
Symbolic link
@@ -0,0 +1 @@
|
||||
../submodules/ytdl-preload/ytdl-preload.lua
|
||||
Reference in New Issue
Block a user