mpv and xfce configs
This commit is contained in:
@ -0,0 +1,445 @@
|
||||
|
||||
-- https://github.com/stax76/mpv-scripts
|
||||
|
||||
----- options
|
||||
|
||||
local o = {
|
||||
lines_to_show = 10,
|
||||
pause_on_open = false, -- does not work on my system when enabled, menu won't show
|
||||
resume_on_exit = "only-if-was-paused",
|
||||
|
||||
-- styles
|
||||
font_size = 50,
|
||||
line_bottom_margin = 1,
|
||||
menu_x_padding = 5,
|
||||
menu_y_padding = 2,
|
||||
}
|
||||
|
||||
local opt = require "mp.options"
|
||||
opt.read_options(o)
|
||||
|
||||
----- string
|
||||
|
||||
function is_empty(input)
|
||||
if input == nil or input == "" then
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
function contains(input, find)
|
||||
if not is_empty(input) and not is_empty(find) then
|
||||
return input:find(find, 1, true)
|
||||
end
|
||||
end
|
||||
|
||||
function starts_with(str, start)
|
||||
return str:sub(1, #start) == start
|
||||
end
|
||||
|
||||
function split(input, sep)
|
||||
assert(#sep == 1) -- supports only single character separator
|
||||
local tbl = {}
|
||||
|
||||
if input ~= nil then
|
||||
for str in string.gmatch(input, "([^" .. sep .. "]+)") do
|
||||
table.insert(tbl, str)
|
||||
end
|
||||
end
|
||||
|
||||
return tbl
|
||||
end
|
||||
|
||||
function first_to_upper(str)
|
||||
return (str:gsub("^%l", string.upper))
|
||||
end
|
||||
|
||||
----- list
|
||||
|
||||
function list_contains(list, value)
|
||||
for _, v in pairs(list) do
|
||||
if v == value then
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
----- path
|
||||
|
||||
function get_temp_dir()
|
||||
local is_windows = package.config:sub(1,1) == "\\"
|
||||
|
||||
if is_windows then
|
||||
return os.getenv("TEMP") .. "\\"
|
||||
else
|
||||
return "/tmp/"
|
||||
end
|
||||
end
|
||||
|
||||
---- file
|
||||
|
||||
function file_exists(path)
|
||||
if is_empty(path) then return false end
|
||||
local file = io.open(path, "r")
|
||||
|
||||
if file ~= nil then
|
||||
io.close(file)
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
function file_write(path, content)
|
||||
local file = assert(io.open(path, "w"))
|
||||
file:write(content)
|
||||
file:close()
|
||||
end
|
||||
|
||||
----- mpv
|
||||
|
||||
local utils = require "mp.utils"
|
||||
local assdraw = require 'mp.assdraw'
|
||||
local msg = require "mp.msg"
|
||||
|
||||
----- path mpv
|
||||
|
||||
function file_name(value)
|
||||
local _, filename = utils.split_path(value)
|
||||
return filename
|
||||
end
|
||||
|
||||
----- main
|
||||
|
||||
package.path = mp.command_native({ "expand-path", "~~/script-modules/?.lua;" }) .. package.path
|
||||
|
||||
local em = require "extended-menu"
|
||||
local menu = em:new(o)
|
||||
local menu_content = { list = {}, current_i = nil }
|
||||
local osc_visibility = nil
|
||||
local media_info_cache = {}
|
||||
local original_set_active_func = em.set_active
|
||||
local original_get_line_func = em.get_line
|
||||
|
||||
function em:get_bindings()
|
||||
local bindings = {
|
||||
{ 'esc', function() self:set_active(false) end },
|
||||
{ 'enter', function() self:handle_enter() end },
|
||||
{ 'bs', function() self:handle_backspace() end },
|
||||
{ 'del', function() self:handle_del() end },
|
||||
{ 'ins', function() self:handle_ins() end },
|
||||
{ 'left', function() self:prev_char() end },
|
||||
{ 'right', function() self:next_char() end },
|
||||
{ 'ctrl+f', function() self:next_char() end },
|
||||
{ 'up', function() self:change_selected_index(-1) end },
|
||||
{ 'down', function() self:change_selected_index(1) end },
|
||||
{ 'ctrl+up', function() self:move_history(-1) end },
|
||||
{ 'ctrl+down', function() self:move_history(1) end },
|
||||
{ 'ctrl+left', function() self:prev_word() end },
|
||||
{ 'ctrl+right', function() self:next_word() end },
|
||||
{ 'home', function() self:go_home() end },
|
||||
{ 'end', function() self:go_end() end },
|
||||
{ 'pgup', function() self:change_selected_index(-o.lines_to_show) end },
|
||||
{ 'pgdwn', function() self:change_selected_index(o.lines_to_show) end },
|
||||
{ 'ctrl+u', function() self:del_to_start() end },
|
||||
{ 'ctrl+v', function() self:paste(true) end },
|
||||
{ 'ctrl+bs', function() self:del_word() end },
|
||||
{ 'ctrl+del', function() self:del_next_word() end },
|
||||
{ 'kp_dec', function() self:handle_char_input('.') end },
|
||||
}
|
||||
|
||||
for i = 0, 9 do
|
||||
bindings[#bindings + 1] =
|
||||
{'kp' .. i, function() self:handle_char_input('' .. i) end}
|
||||
end
|
||||
|
||||
return bindings
|
||||
end
|
||||
|
||||
function em:set_active(active)
|
||||
original_set_active_func(self, active)
|
||||
|
||||
if not active and osc_visibility then
|
||||
mp.command("script-message osc-visibility " .. osc_visibility .. " no_osd")
|
||||
osc_visibility = nil
|
||||
end
|
||||
end
|
||||
|
||||
menu.index_field = "index"
|
||||
|
||||
function get_media_info()
|
||||
local path = mp.get_property("path")
|
||||
|
||||
if media_info_cache[path] then
|
||||
return media_info_cache[path]
|
||||
end
|
||||
|
||||
local format_file = get_temp_dir() .. mp.get_script_name() .. " media-info-format-v1.txt"
|
||||
|
||||
if not file_exists(format_file) then
|
||||
media_info_format = [[General;N: %FileNameExtension%\\nG: %Format%, %FileSize/String%, %Duration/String%, %OverallBitRate/String%, %Recorded_Date%\\n
|
||||
Video;V: %Format%, %Format_Profile%, %Width%x%Height%, %BitRate/String%, %FrameRate% FPS\\n
|
||||
Audio;A: %Language/String%, %Format%, %Format_Profile%, %BitRate/String%, %Channel(s)% ch, %SamplingRate/String%, %Title%\\n
|
||||
Text;S: %Language/String%, %Format%, %Format_Profile%, %Title%\\n]]
|
||||
|
||||
file_write(format_file, media_info_format)
|
||||
end
|
||||
|
||||
if contains(path, "://") or not file_exists(path) then
|
||||
return
|
||||
end
|
||||
|
||||
local proc_result = mp.command_native({
|
||||
name = "subprocess",
|
||||
playback_only = false,
|
||||
capture_stdout = true,
|
||||
args = {"mediainfo", "--inform=file://" .. format_file, path},
|
||||
})
|
||||
|
||||
if proc_result.status == 0 then
|
||||
local output = proc_result.stdout
|
||||
|
||||
output = string.gsub(output, ", , ,", ",")
|
||||
output = string.gsub(output, ", ,", ",")
|
||||
output = string.gsub(output, ": , ", ": ")
|
||||
output = string.gsub(output, ", \\n\r*\n", "\\n")
|
||||
output = string.gsub(output, "\\n\r*\n", "\\n")
|
||||
output = string.gsub(output, ", \\n", "\\n")
|
||||
output = string.gsub(output, "\\n", "\n")
|
||||
output = string.gsub(output, "%.000 FPS", " FPS")
|
||||
output = string.gsub(output, "MPEG Audio, Layer 3", "MP3")
|
||||
|
||||
media_info_cache[path] = output
|
||||
|
||||
return output
|
||||
end
|
||||
end
|
||||
|
||||
function binding_get_line(self, _, v)
|
||||
local a = assdraw.ass_new()
|
||||
local cmd = self:ass_escape(v.cmd)
|
||||
local key = self:ass_escape(v.key)
|
||||
local comment = self:ass_escape(v.comment or '')
|
||||
|
||||
if v.priority == -1 or v.priority == -2 then
|
||||
local why_inactive = (v.priority == -1) and 'Inactive' or 'Shadowed'
|
||||
a:append(self:get_font_color('comment'))
|
||||
|
||||
if comment ~= "" then
|
||||
a:append(comment .. '\\h')
|
||||
end
|
||||
|
||||
a:append(key .. '\\h(' .. why_inactive .. ')' .. '\\h' .. cmd)
|
||||
return a.text
|
||||
end
|
||||
|
||||
if comment ~= "" then
|
||||
a:append(self:get_font_color('default'))
|
||||
a:append(comment .. '\\h')
|
||||
end
|
||||
|
||||
a:append(self:get_font_color('accent'))
|
||||
a:append(key)
|
||||
a:append(self:get_font_color('comment'))
|
||||
a:append(' ' .. cmd)
|
||||
return a.text
|
||||
end
|
||||
|
||||
mp.register_script_message("show-command-palette", function (name)
|
||||
menu_content.list = {}
|
||||
menu_content.current_i = 1
|
||||
menu.search_heading = first_to_upper(name)
|
||||
menu.filter_by_fields = { "content" }
|
||||
em.get_line = original_get_line_func
|
||||
|
||||
if name == "bindings" then
|
||||
local bindings = utils.parse_json(mp.get_property("input-bindings"))
|
||||
|
||||
for _, v in ipairs(bindings) do
|
||||
v.key = "(" .. v.key .. ")"
|
||||
|
||||
if not is_empty(v.comment) then
|
||||
v.comment = first_to_upper(v.comment)
|
||||
end
|
||||
end
|
||||
|
||||
for _, v in ipairs(bindings) do
|
||||
for _, v2 in ipairs(bindings) do
|
||||
if v.key == v2.key and v.priority < v2.priority then
|
||||
v.priority = -2
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
table.sort(bindings, function(i, j)
|
||||
return i.priority > j.priority
|
||||
end)
|
||||
|
||||
menu_content.list = bindings
|
||||
|
||||
function menu:submit(val)
|
||||
mp.command(val.cmd)
|
||||
end
|
||||
|
||||
menu.filter_by_fields = {'cmd', 'key', 'comment'}
|
||||
em.get_line = binding_get_line
|
||||
elseif name == "chapters" then
|
||||
local count = mp.get_property("chapter-list/count")
|
||||
if count == 0 then return end
|
||||
|
||||
for i = 0, count do
|
||||
local title = mp.get_property("chapter-list/" .. i .. "/title")
|
||||
|
||||
if title then
|
||||
table.insert(menu_content.list, { index = i + 1, content = title })
|
||||
end
|
||||
end
|
||||
|
||||
menu_content.current_i = mp.get_property_number("chapter") + 1
|
||||
|
||||
function menu:submit(val)
|
||||
mp.set_property_number("chapter", val.index - 1)
|
||||
end
|
||||
elseif name == "playlist" then
|
||||
local count = mp.get_property_number("playlist-count")
|
||||
if count == 0 then return end
|
||||
|
||||
for i = 0, (count - 1) do
|
||||
local text = mp.get_property("playlist/" .. i .. "/title")
|
||||
|
||||
if text == nil then
|
||||
text = file_name(mp.get_property("playlist/" .. i .. "/filename"))
|
||||
end
|
||||
|
||||
table.insert(menu_content.list, { index = i + 1, content = text })
|
||||
end
|
||||
|
||||
menu_content.current_i = mp.get_property_number("playlist-pos") + 1
|
||||
|
||||
function menu:submit(val)
|
||||
mp.set_property_number("playlist-pos", val.index - 1)
|
||||
end
|
||||
elseif name == "commands" then
|
||||
local commands = utils.parse_json(mp.get_property("command-list"))
|
||||
|
||||
for k, v in ipairs(commands) do
|
||||
local text = v.name
|
||||
|
||||
for _, arg in ipairs(v.args) do
|
||||
if arg.optional then
|
||||
text = text .. " [<" .. arg.name .. ">]"
|
||||
else
|
||||
text = text .. " <" .. arg.name .. ">"
|
||||
end
|
||||
end
|
||||
|
||||
table.insert(menu_content.list, { index = k, content = text })
|
||||
end
|
||||
|
||||
function menu:submit(val)
|
||||
print(val.content)
|
||||
local cmd = string.match(val.content, '%S+')
|
||||
mp.commandv("script-message-to", "console", "type", cmd .. " ")
|
||||
end
|
||||
elseif name == "properties" then
|
||||
local properties = split(mp.get_property("property-list"), ",")
|
||||
|
||||
for k, v in ipairs(properties) do
|
||||
table.insert(menu_content.list, { index = k, content = v })
|
||||
end
|
||||
|
||||
function menu:submit(val)
|
||||
mp.commandv('script-message-to', 'console', 'type', "print-text ${" .. val.content .. "}")
|
||||
end
|
||||
elseif name == "options" then
|
||||
local options = split(mp.get_property("options"), ",")
|
||||
|
||||
for k, v in ipairs(options) do
|
||||
local type = mp.get_property_osd("option-info/" .. v .. "/type", "")
|
||||
local default =mp.get_property_osd("option-info/" .. v .. "/default-value", "")
|
||||
v = v .. " (type: " .. type .. ", default: " .. default .. ")"
|
||||
table.insert(menu_content.list, { index = k, content = v })
|
||||
end
|
||||
|
||||
function menu:submit(val)
|
||||
print(val.content)
|
||||
local prop = string.match(val.content, '%S+')
|
||||
mp.commandv("script-message-to", "console", "type", "set " .. prop .. " ")
|
||||
end
|
||||
elseif name == "audio" then
|
||||
local tracks = split(get_media_info() .. "\nA: None", "\n")
|
||||
local id = 0
|
||||
|
||||
for _, v in ipairs(tracks) do
|
||||
if starts_with(v, "A: ") then
|
||||
id = id + 1
|
||||
table.insert(menu_content.list, { index = id, content = string.sub(v, 4) })
|
||||
end
|
||||
end
|
||||
|
||||
menu_content.current_i = mp.get_property_number("aid") or id
|
||||
|
||||
function menu:submit(val)
|
||||
mp.command("set aid " .. ((val.index == id) and 'no' or val.index))
|
||||
end
|
||||
elseif name == "subtitle" then
|
||||
local tracks = split(get_media_info() .. "\nS: None", "\n")
|
||||
local id = 0
|
||||
|
||||
for _, v in ipairs(tracks) do
|
||||
if starts_with(v, "S: ") then
|
||||
id = id + 1
|
||||
table.insert(menu_content.list, { index = id, content = string.sub(v, 4) })
|
||||
end
|
||||
end
|
||||
|
||||
menu_content.current_i = mp.get_property_number("sid") or id
|
||||
|
||||
function menu:submit(val)
|
||||
mp.command("set sid " .. ((val.index == id) and 'no' or val.index))
|
||||
end
|
||||
elseif name == "video" then
|
||||
local tracks = split(get_media_info() .. "\nV: None", "\n")
|
||||
local id = 0
|
||||
|
||||
for _, v in ipairs(tracks) do
|
||||
if starts_with(v, "V: ") then
|
||||
id = id + 1
|
||||
table.insert(menu_content.list, { index = id, content = string.sub(v, 4) })
|
||||
end
|
||||
end
|
||||
|
||||
menu_content.current_i = mp.get_property_number("vid") or id
|
||||
|
||||
function menu:submit(val)
|
||||
mp.command("set vid " .. ((val.index == id) and 'no' or val.index))
|
||||
end
|
||||
elseif name == "profiles" then
|
||||
local profiles = utils.parse_json(mp.get_property("profile-list"))
|
||||
local ignore_list = {"builtin-pseudo-gui", "encoding", "libmpv", "pseudo-gui", "default"}
|
||||
|
||||
for k, v in ipairs(profiles) do
|
||||
if not list_contains(ignore_list, v.name) then
|
||||
table.insert(menu_content.list, { index = k, content = v.name })
|
||||
end
|
||||
end
|
||||
|
||||
function menu:submit(val)
|
||||
mp.command("show-text " .. val.content);
|
||||
mp.command("apply-profile " .. val.content);
|
||||
end
|
||||
else
|
||||
msg.error("Unknown mode " .. name)
|
||||
return
|
||||
end
|
||||
|
||||
if is_empty(mp.get_property("path")) then
|
||||
osc_visibility = utils.shared_script_property_get("osc-visibility")
|
||||
|
||||
if osc_visibility then
|
||||
mp.command("script-message osc-visibility never no_osd")
|
||||
end
|
||||
else
|
||||
osc_visibility = nil
|
||||
end
|
||||
|
||||
menu:init(menu_content)
|
||||
end)
|
@ -0,0 +1,553 @@
|
||||
(function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){
|
||||
// referene: http://docs.aegisub.org/3.2/ASS_Tags/
|
||||
// based on mpv's assdraw.lua
|
||||
|
||||
var c = 0.551915024494 // circle approximation
|
||||
|
||||
function Assdraw() {
|
||||
this.scale = 4;
|
||||
this.text = ""
|
||||
}
|
||||
|
||||
Assdraw.SMART_WRAPPING = 0
|
||||
Assdraw.EOL_WRAPPING = 1
|
||||
Assdraw.NO_WORD_WRAPPING = 2
|
||||
Assdraw.SMART_WRAPPING_WIDER = 3
|
||||
Assdraw.BOTTOM_LEFT = 1
|
||||
Assdraw.BOTTOM_CENTER = 2
|
||||
Assdraw.BOTTOM_RIGHT = 3
|
||||
Assdraw.MIDDLE_LEFT = 4
|
||||
Assdraw.MIDDLE_CENTER = 5
|
||||
Assdraw.MIDDLE_RIGHT = 6
|
||||
Assdraw.TOP_LEFT = 7
|
||||
Assdraw.TOP_CENTER = 8
|
||||
Assdraw.TOP_RIGHT = 9
|
||||
|
||||
Assdraw.escape = function(s) {
|
||||
return s.replace(/([{}])/g, "\\$1")
|
||||
}
|
||||
|
||||
Assdraw.bolden = function(s) {
|
||||
return '{\\b1}' + s + '{\\b0}'
|
||||
}
|
||||
|
||||
Assdraw.prototype.newEvent = function() {
|
||||
// osd_libass.c adds an event per line
|
||||
if (this.text.length > 0) {
|
||||
this.text = this.text + "\n"
|
||||
}
|
||||
}
|
||||
|
||||
Assdraw.prototype.override = function(callback) {
|
||||
this.append('{')
|
||||
callback.call(this);
|
||||
this.append('}')
|
||||
}
|
||||
|
||||
Assdraw.prototype.primaryFillColor = function(hex) {
|
||||
this.append('\\1c&H' + hex + '&')
|
||||
return this;
|
||||
}
|
||||
|
||||
Assdraw.prototype.secondaryFillColor = function(hex) {
|
||||
this.append('\\2c&H' + hex + '&')
|
||||
return this;
|
||||
}
|
||||
|
||||
Assdraw.prototype.borderColor = function(hex) {
|
||||
this.append('\\3c&H' + hex + '&')
|
||||
return this;
|
||||
}
|
||||
|
||||
Assdraw.prototype.shadowColor = function(hex) {
|
||||
this.append('\\4c&H' + hex + '&')
|
||||
return this;
|
||||
}
|
||||
|
||||
Assdraw.prototype.primaryFillAlpha = function(hex) {
|
||||
this.append('\\1a&H' + hex + '&')
|
||||
return this;
|
||||
}
|
||||
|
||||
Assdraw.prototype.secondaryFillAlpha = function(hex) {
|
||||
this.append('\\2a&H' + hex + '&')
|
||||
return this;
|
||||
}
|
||||
|
||||
Assdraw.prototype.borderAlpha = function(hex) {
|
||||
this.append('\\3a&H' + hex + '&')
|
||||
return this;
|
||||
}
|
||||
|
||||
Assdraw.prototype.shadowAlpha = function(hex) {
|
||||
this.append('\\4a&H' + hex + '&')
|
||||
return this;
|
||||
}
|
||||
|
||||
Assdraw.prototype.fontName = function(s) {
|
||||
this.append('\\fn' + s)
|
||||
return this;
|
||||
}
|
||||
|
||||
Assdraw.prototype.fontSize = function(s) {
|
||||
this.append('\\fs' + s)
|
||||
return this;
|
||||
}
|
||||
|
||||
Assdraw.prototype.borderSize = function(n) {
|
||||
this.append('\\bord' + n)
|
||||
return this;
|
||||
}
|
||||
|
||||
Assdraw.prototype.xShadowDistance = function(n) {
|
||||
this.append('\\xshad' + n)
|
||||
return this;
|
||||
}
|
||||
|
||||
Assdraw.prototype.yShadowDistance = function(n) {
|
||||
this.append('\\yshad' + n)
|
||||
return this;
|
||||
}
|
||||
|
||||
Assdraw.prototype.letterSpacing = function(n) {
|
||||
this.append('\\fsp' + n)
|
||||
return this;
|
||||
}
|
||||
|
||||
Assdraw.prototype.wrapStyle = function(n) {
|
||||
this.append('\\q' + n)
|
||||
return this;
|
||||
}
|
||||
|
||||
Assdraw.prototype.drawStart = function() {
|
||||
this.text = this.text + "{\\p"+ this.scale + "}"
|
||||
}
|
||||
|
||||
Assdraw.prototype.drawStop = function() {
|
||||
this.text = this.text + "{\\p0}"
|
||||
}
|
||||
|
||||
Assdraw.prototype.coord = function(x, y) {
|
||||
var scale = Math.pow(2, (this.scale - 1))
|
||||
var ix = Math.ceil(x * scale)
|
||||
var iy = Math.ceil(y * scale)
|
||||
this.text = this.text + " " + ix + " " + iy
|
||||
}
|
||||
|
||||
Assdraw.prototype.append = function(s) {
|
||||
this.text = this.text + s
|
||||
}
|
||||
|
||||
Assdraw.prototype.appendLn = function(s) {
|
||||
this.append(s + '\\n')
|
||||
}
|
||||
|
||||
Assdraw.prototype.appendLN = function(s) {
|
||||
this.append(s + '\\N')
|
||||
}
|
||||
|
||||
Assdraw.prototype.merge = function(other) {
|
||||
this.text = this.text + other.text
|
||||
}
|
||||
|
||||
Assdraw.prototype.pos = function(x, y) {
|
||||
this.append("\\pos(" + x.toFixed(0) + "," + y.toFixed(0) + ")")
|
||||
}
|
||||
|
||||
Assdraw.prototype.lineAlignment = function(an) {
|
||||
this.append("\\an" + an)
|
||||
}
|
||||
|
||||
Assdraw.prototype.moveTo = function(x, y) {
|
||||
this.append(" m")
|
||||
this.coord(x, y)
|
||||
}
|
||||
|
||||
Assdraw.prototype.lineTo = function(x, y) {
|
||||
this.append(" l")
|
||||
this.coord(x, y)
|
||||
}
|
||||
|
||||
Assdraw.prototype.bezierCurve = function(x1, y1, x2, y2, x3, y3) {
|
||||
this.append(" b")
|
||||
this.coord(x1, y1)
|
||||
this.coord(x2, y2)
|
||||
this.coord(x3, y3)
|
||||
}
|
||||
|
||||
|
||||
Assdraw.prototype.rectCcw = function(x0, y0, x1, y1) {
|
||||
this.moveTo(x0, y0)
|
||||
this.lineTo(x0, y1)
|
||||
this.lineTo(x1, y1)
|
||||
this.lineTo(x1, y0)
|
||||
}
|
||||
|
||||
Assdraw.prototype.rectCw = function(x0, y0, x1, y1) {
|
||||
this.moveTo(x0, y0)
|
||||
this.lineTo(x1, y0)
|
||||
this.lineTo(x1, y1)
|
||||
this.lineTo(x0, y1)
|
||||
}
|
||||
|
||||
Assdraw.prototype.hexagonCw = function(x0, y0, x1, y1, r1, r2) {
|
||||
if (typeof r2 === 'undefined') {
|
||||
r2 = r1
|
||||
}
|
||||
this.moveTo(x0 + r1, y0)
|
||||
if (x0 != x1) {
|
||||
this.lineTo(x1 - r2, y0)
|
||||
}
|
||||
this.lineTo(x1, y0 + r2)
|
||||
if (x0 != x1) {
|
||||
this.lineTo(x1 - r2, y1)
|
||||
}
|
||||
this.lineTo(x0 + r1, y1)
|
||||
this.lineTo(x0, y0 + r1)
|
||||
}
|
||||
|
||||
Assdraw.prototype.hexagonCcw = function(x0, y0, x1, y1, r1, r2) {
|
||||
if (typeof r2 === 'undefined') {
|
||||
r2 = r1
|
||||
}
|
||||
this.moveTo(x0 + r1, y0)
|
||||
this.lineTo(x0, y0 + r1)
|
||||
this.lineTo(x0 + r1, y1)
|
||||
if (x0 != x1) {
|
||||
this.lineTo(x1 - r2, y1)
|
||||
}
|
||||
this.lineTo(x1, y0 + r2)
|
||||
if (x0 != x1) {
|
||||
this.lineTo(x1 - r2, y0)
|
||||
}
|
||||
}
|
||||
|
||||
Assdraw.prototype.roundRectCw = function(ass, x0, y0, x1, y1, r1, r2) {
|
||||
if (typeof r2 === 'undefined') {
|
||||
r2 = r1
|
||||
}
|
||||
var c1 = c * r1 // circle approximation
|
||||
var c2 = c * r2 // circle approximation
|
||||
this.moveTo(x0 + r1, y0)
|
||||
this.lineTo(x1 - r2, y0) // top line
|
||||
if (r2 > 0) {
|
||||
this.bezierCurve(x1 - r2 + c2, y0, x1, y0 + r2 - c2, x1, y0 + r2) // top right corner
|
||||
}
|
||||
this.lineTo(x1, y1 - r2) // right line
|
||||
if (r2 > 0) {
|
||||
this.bezierCurve(x1, y1 - r2 + c2, x1 - r2 + c2, y1, x1 - r2, y1) // bottom right corner
|
||||
}
|
||||
this.lineTo(x0 + r1, y1) // bottom line
|
||||
if (r1 > 0) {
|
||||
this.bezierCurve(x0 + r1 - c1, y1, x0, y1 - r1 + c1, x0, y1 - r1) // bottom left corner
|
||||
}
|
||||
this.lineTo(x0, y0 + r1) // left line
|
||||
if (r1 > 0) {
|
||||
this.bezierCurve(x0, y0 + r1 - c1, x0 + r1 - c1, y0, x0 + r1, y0) // top left corner
|
||||
}
|
||||
}
|
||||
|
||||
Assdraw.prototype.roundRectCcw = function(ass, x0, y0, x1, y1, r1, r2) {
|
||||
if (typeof r2 === 'undefined') {
|
||||
r2 = r1
|
||||
}
|
||||
var c1 = c * r1 // circle approximation
|
||||
var c2 = c * r2 // circle approximation
|
||||
this.moveTo(x0 + r1, y0)
|
||||
if (r1 > 0) {
|
||||
this.bezierCurve(x0 + r1 - c1, y0, x0, y0 + r1 - c1, x0, y0 + r1) // top left corner
|
||||
}
|
||||
this.lineTo(x0, y1 - r1) // left line
|
||||
if (r1 > 0) {
|
||||
this.bezierCurve(x0, y1 - r1 + c1, x0 + r1 - c1, y1, x0 + r1, y1) // bottom left corner
|
||||
}
|
||||
this.lineTo(x1 - r2, y1) // bottom line
|
||||
if (r2 > 0) {
|
||||
this.bezierCurve(x1 - r2 + c2, y1, x1, y1 - r2 + c2, x1, y1 - r2) // bottom right corner
|
||||
}
|
||||
this.lineTo(x1, y0 + r2) // right line
|
||||
if (r2 > 0) {
|
||||
this.bezierCurve(x1, y0 + r2 - c2, x1 - r2 + c2, y0, x1 - r2, y0) // top right corner
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Assdraw
|
||||
|
||||
},{}],2:[function(require,module,exports){
|
||||
var assdraw = require('./assdraw.js')
|
||||
var shortcuts = [
|
||||
{
|
||||
category: 'Navigation',
|
||||
shortcuts: [
|
||||
{keys: ', / .', effect: 'Seek by frame'},
|
||||
{keys: '← / →', effect: 'Seek by 5 seconds'},
|
||||
{keys: '↓ / ↑', effect: 'Seek by 1 minute'},
|
||||
{keys: '[Shift] PGDWN / PGUP', effect: 'Seek by 10 minutes'},
|
||||
{keys: '[Shift] ← / →', effect: 'Seek by 1 second (exact)'},
|
||||
{keys: '[Shift] ↓ / ↑', effect: 'Seek by 5 seconds (exact)'},
|
||||
{keys: '[Ctrl] ← / →', effect: 'Seek by subtitle'},
|
||||
{keys: '[Shift] BACKSPACE', effect: 'Undo last seek'},
|
||||
{keys: '[Ctrl+Shift] BACKSPACE', effect: 'Mark current position'},
|
||||
{keys: 'l', effect: 'Set/clear A-B loop points'},
|
||||
{keys: 'L', effect: 'Toggle infinite looping'},
|
||||
{keys: 'PGDWN / PGUP', effect: 'Previous/next chapter'},
|
||||
{keys: '< / >', effect: 'Go backward/forward in the playlist'},
|
||||
{keys: 'ENTER', effect: 'Go forward in the playlist'},
|
||||
{keys: 'F8', effect: 'Show playlist [UI]'},
|
||||
]
|
||||
},
|
||||
{
|
||||
category: 'Playback',
|
||||
shortcuts: [
|
||||
{keys: 'p / SPACE', effect: 'Pause/unpause'},
|
||||
{keys: '[ / ]', effect: 'Decrease/increase speed [10%]'},
|
||||
{keys: '{ / }', effect: 'Halve/double speed'},
|
||||
{keys: 'BACKSPACE', effect: 'Reset speed'},
|
||||
{keys: 'o / P', effect: 'Show progress'},
|
||||
{keys: 'O', effect: 'Toggle progress'},
|
||||
{keys: 'i / I', effect: 'Show/toggle stats'},
|
||||
]
|
||||
},
|
||||
{
|
||||
category: 'Subtitle',
|
||||
shortcuts: [
|
||||
{keys: '[Ctrl+Shift] ← / →', effect: 'Adjust subtitle delay [subtitle]'},
|
||||
{keys: 'z / Z', effect: 'Adjust subtitle delay [0.1sec]'},
|
||||
{keys: 'v', effect: 'Toggle subtitle visibility'},
|
||||
{keys: 'u', effect: 'Toggle subtitle style overrides'},
|
||||
{keys: 'V', effect: 'Toggle subtitle VSFilter aspect compatibility mode'},
|
||||
{keys: 'r / R', effect: 'Move subtitles up/down'},
|
||||
{keys: 'j / J', effect: 'Cycle subtitle'},
|
||||
{keys: 'F9', effect: 'Show audio/subtitle list [UI]'},
|
||||
]
|
||||
},
|
||||
{
|
||||
category: 'Audio',
|
||||
shortcuts: [
|
||||
{keys: 'm', effect: 'Mute sound'},
|
||||
{keys: '#', effect: 'Cycle audio track'},
|
||||
{keys: '/ / *', effect: 'Decrease/increase volume'},
|
||||
{keys: '9 / 0', effect: 'Decrease/increase volume'},
|
||||
{keys: '[Ctrl] - / +', effect: 'Decrease/increase audio delay [0.1sec]'},
|
||||
{keys: 'F9', effect: 'Show audio/subtitle list [UI]'},
|
||||
]
|
||||
},
|
||||
{
|
||||
category: 'Video',
|
||||
shortcuts: [
|
||||
{keys: '_', effect: 'Cycle video track'},
|
||||
{keys: 'A', effect: 'Cycle aspect ratio'},
|
||||
{keys: 'd', effect: 'Toggle deinterlacer'},
|
||||
{keys: '[Ctrl] h', effect: 'Toggle hardware video decoding'},
|
||||
{keys: 'w / W', effect: 'Decrease/increase pan-and-scan range'},
|
||||
{keys: '[Alt] - / +', effect: 'Zoom out/in'},
|
||||
{keys: '[Alt] ARROWS', effect: 'Move the video rectangle'},
|
||||
{keys: '[Alt] BACKSPACE', effect: 'Reset pan/zoom'},
|
||||
{keys: '1 / 2', effect: 'Decrease/increase contrast'},
|
||||
{keys: '3 / 4', effect: 'Decrease/increase brightness'},
|
||||
{keys: '5 / 6', effect: 'Decrease/increase gamma'},
|
||||
{keys: '7 / 8', effect: 'Decrease/increase saturation'},
|
||||
]
|
||||
},
|
||||
{
|
||||
category: 'Application',
|
||||
shortcuts: [
|
||||
{keys: 'q', effect: 'Quit'},
|
||||
{keys: 'Q', effect: 'Save position and quit'},
|
||||
{keys: 's', effect: 'Take a screenshot'},
|
||||
{keys: 'S', effect: 'Take a screenshot without subtitles'},
|
||||
{keys: '[Ctrl] s', effect: 'Take a screenshot as rendered'},
|
||||
]
|
||||
},
|
||||
{
|
||||
category: 'Window',
|
||||
shortcuts: [
|
||||
{keys: 'f', effect: 'Toggle fullscreen'},
|
||||
{keys: '[Command] f', effect: 'Toggle fullscreen [macOS]'},
|
||||
{keys: 'ESC', effect: 'Exit fullscreen'},
|
||||
{keys: 'T', effect: 'Toggle stay-on-top'},
|
||||
{keys: '[Alt] 0', effect: 'Resize window to 0.5x [macOS]'},
|
||||
{keys: '[Alt] 1', effect: 'Reset window size [macOS]'},
|
||||
{keys: '[Alt] 2', effect: 'Resize window to 2x [macOS]'},
|
||||
]
|
||||
},
|
||||
{
|
||||
category: 'Multimedia keys',
|
||||
shortcuts: [
|
||||
{keys: 'PAUSE', effect: 'Pause'}, // keyboard with multimedia keys
|
||||
{keys: 'STOP', effect: 'Quit'}, // keyboard with multimedia keys
|
||||
{keys: 'PREVIOUS / NEXT', effect: 'Seek 1 minute'}, // keyboard with multimedia keys
|
||||
]
|
||||
},
|
||||
]
|
||||
|
||||
var State = {
|
||||
active: false,
|
||||
startLine: 0,
|
||||
startCategory: 0
|
||||
}
|
||||
|
||||
var opts = {
|
||||
font: 'monospace',
|
||||
'font-size': 8,
|
||||
'usage-font-size': 6,
|
||||
}
|
||||
|
||||
function repeat(s, num) {
|
||||
var ret = '';
|
||||
for (var i = 0; i < num; i++) {
|
||||
ret = ret + s;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
function renderCategory(category) {
|
||||
var lines = []
|
||||
lines.push(assdraw.bolden(category.category))
|
||||
var maxKeysLength = 0;
|
||||
category.shortcuts.forEach(function(shortcut) {
|
||||
if (shortcut.keys.length > maxKeysLength) maxKeysLength = shortcut.keys.length
|
||||
})
|
||||
category.shortcuts.forEach(function(shortcut) {
|
||||
var padding = repeat(" ", maxKeysLength - shortcut.keys.length)
|
||||
lines.push(assdraw.escape(shortcut.keys + padding + " " + shortcut.effect))
|
||||
})
|
||||
return lines
|
||||
}
|
||||
|
||||
function render() {
|
||||
var screen = mp.get_osd_size()
|
||||
if (!State.active) {
|
||||
mp.set_osd_ass(0, 0, '{}')
|
||||
return
|
||||
}
|
||||
var ass = new assdraw()
|
||||
ass.newEvent()
|
||||
ass.override(function() {
|
||||
this.lineAlignment(assdraw.TOP_LEFT)
|
||||
this.primaryFillAlpha('00')
|
||||
this.borderAlpha('00')
|
||||
this.shadowAlpha('99')
|
||||
this.primaryFillColor('eeeeee')
|
||||
this.borderColor('111111')
|
||||
this.shadowColor('000000')
|
||||
this.fontName(opts.font)
|
||||
this.fontSize(opts['font-size'])
|
||||
this.borderSize(1)
|
||||
this.xShadowDistance(0)
|
||||
this.yShadowDistance(1)
|
||||
this.letterSpacing(0)
|
||||
this.wrapStyle(assdraw.EOL_WRAPPING)
|
||||
})
|
||||
var mainLines = [];
|
||||
var pushedCategory = false
|
||||
shortcuts.forEach(function(category, i) {
|
||||
if (i < State.startCategory) {
|
||||
return;
|
||||
}
|
||||
pushedCategory = true;
|
||||
if (pushedCategory) {
|
||||
mainLines.push("")
|
||||
}
|
||||
mainLines.push.apply(mainLines, renderCategory(category))
|
||||
})
|
||||
mainLines.slice(State.startLine).forEach(function(line) {
|
||||
ass.appendLN(line);
|
||||
})
|
||||
|
||||
ass.newEvent()
|
||||
var sideLines = renderCategory({
|
||||
category: 'usage',
|
||||
shortcuts: Keybindings
|
||||
})
|
||||
ass.override(function() {
|
||||
this.lineAlignment(assdraw.TOP_RIGHT)
|
||||
this.fontSize(opts['usage-font-size'])
|
||||
})
|
||||
sideLines.forEach(function(line) {
|
||||
ass.appendLN(line);
|
||||
})
|
||||
|
||||
mp.set_osd_ass(0, 0, ass.text)
|
||||
}
|
||||
|
||||
function setActive(active) {
|
||||
if (active == State.active) return
|
||||
if (active) {
|
||||
State.active = true
|
||||
updateBindings(Keybindings, true)
|
||||
} else {
|
||||
State.active = false
|
||||
updateBindings(Keybindings, false)
|
||||
}
|
||||
render()
|
||||
}
|
||||
|
||||
function updateBindings(bindings, enable) {
|
||||
bindings.forEach(function(binding, i) {
|
||||
var name = '__cheatsheet_binding_' + i
|
||||
if (enable) {
|
||||
mp.add_forced_key_binding(binding.keys, name, binding.callback, binding.options)
|
||||
} else {
|
||||
mp.remove_key_binding(name)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
var Keybindings = [
|
||||
{
|
||||
keys: 'esc',
|
||||
effect: 'close',
|
||||
callback: function() { setActive(false) }
|
||||
},
|
||||
{
|
||||
keys: '?',
|
||||
effect: 'close',
|
||||
callback: function() { setActive(false) }
|
||||
},
|
||||
{
|
||||
keys: 'j',
|
||||
effect: 'next line',
|
||||
callback: function() {
|
||||
State.startLine += 1
|
||||
render()
|
||||
},
|
||||
options: 'repeatable'
|
||||
},
|
||||
{
|
||||
keys: 'k',
|
||||
effect: 'prev line',
|
||||
callback: function() {
|
||||
State.startLine = Math.max(0, State.startine - 1)
|
||||
render()
|
||||
},
|
||||
options: 'repeatable'
|
||||
},
|
||||
{
|
||||
keys: 'n',
|
||||
effect: 'next category',
|
||||
callback: function() {
|
||||
State.startCategory += 1
|
||||
State.startLine = 0
|
||||
render()
|
||||
},
|
||||
options: 'repeatable'
|
||||
},
|
||||
{
|
||||
keys: 'p',
|
||||
effect: 'prev category',
|
||||
callback: function() {
|
||||
State.startCategory = Math.max(0, State.startCategory - 1)
|
||||
State.startLine = 0
|
||||
render()
|
||||
},
|
||||
options: 'repeatable'
|
||||
},
|
||||
]
|
||||
|
||||
mp.add_key_binding('?', 'cheatsheet-enable', function() { setActive(true) })
|
||||
|
||||
mp.observe_property('osd-width', 'native', render)
|
||||
mp.observe_property('osd-height', 'native', render)
|
||||
|
||||
},{"./assdraw.js":1}]},{},[2]);
|
422
xfce-custom/dots/home/neko/.config/mpv/scripts/reload.lua
Normal file
422
xfce-custom/dots/home/neko/.config/mpv/scripts/reload.lua
Normal file
@ -0,0 +1,422 @@
|
||||
-- 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
|
||||
mp.commandv("loadfile", path, "replace", "start=+" .. time_pos)
|
||||
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)
|
569
xfce-custom/dots/home/neko/.config/mpv/scripts/sponsorblock.lua
Normal file
569
xfce-custom/dots/home/neko/.config/mpv/scripts/sponsorblock.lua
Normal file
@ -0,0 +1,569 @@
|
||||
-- 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 = .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 - .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 = {
|
||||
"ytdl://([%w-_]+).*",
|
||||
"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("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)
|
@ -0,0 +1,3 @@
|
||||
-- This is a dummy main.lua
|
||||
-- required for mpv 0.33
|
||||
-- do not delete
|
@ -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
|
@ -0,0 +1 @@
|
||||
aMn4i07nXmgiLpjJPE235QcbVv3QxdXdH1xB
|
515
xfce-custom/dots/home/neko/.config/mpv/scripts/thumbfast.lua
Normal file
515
xfce-custom/dots/home/neko/.config/mpv/scripts/thumbfast.lua
Normal file
@ -0,0 +1,515 @@
|
||||
-- thumbfast.lua
|
||||
--
|
||||
-- High-performance on-the-fly thumbnailer
|
||||
--
|
||||
-- Built for easy integration in third-party UIs.
|
||||
|
||||
local options = {
|
||||
-- Socket path (leave empty for auto)
|
||||
socket = "",
|
||||
|
||||
-- Thumbnail path (leave empty for auto)
|
||||
thumbnail = "",
|
||||
|
||||
-- Maximum thumbnail size in pixels (scaled down to fit)
|
||||
-- Values are scaled when hidpi is enabled
|
||||
max_height = 200,
|
||||
max_width = 200,
|
||||
|
||||
-- Overlay id
|
||||
overlay_id = 42,
|
||||
|
||||
-- Thumbnail interval in seconds, set to 0 to disable (warning: high cpu usage)
|
||||
-- Clamped to min_thumbnails and max_thumbnails
|
||||
interval = 6,
|
||||
|
||||
-- Number of thumbnails
|
||||
min_thumbnails = 6,
|
||||
max_thumbnails = 120,
|
||||
|
||||
-- Spawn thumbnailer on file load for faster initial thumbnails
|
||||
spawn_first = false,
|
||||
|
||||
-- Enable on network playback
|
||||
network = false,
|
||||
|
||||
-- Enable on audio playback
|
||||
audio = false
|
||||
}
|
||||
|
||||
mp.utils = require "mp.utils"
|
||||
mp.options = require "mp.options"
|
||||
mp.options.read_options(options, "thumbfast")
|
||||
|
||||
if options.min_thumbnails < 1 then
|
||||
options.min_thumbnails = 1
|
||||
end
|
||||
|
||||
local os_name = ""
|
||||
|
||||
math.randomseed(os.time())
|
||||
local unique = math.random(10000000)
|
||||
local init = false
|
||||
|
||||
local spawned = false
|
||||
local can_generate = true
|
||||
local network = false
|
||||
local disabled = false
|
||||
local interval = 0
|
||||
|
||||
local x = nil
|
||||
local y = nil
|
||||
local last_x = x
|
||||
local last_y = y
|
||||
|
||||
local last_index = nil
|
||||
local last_request = nil
|
||||
local last_request_time = nil
|
||||
local last_display_time = 0
|
||||
|
||||
local effective_w = options.max_width
|
||||
local effective_h = options.max_height
|
||||
local thumb_size = effective_w * effective_h * 4
|
||||
|
||||
local filters_reset = {["lavfi-crop"]=true, crop=true}
|
||||
local filters_runtime = {hflip=true, vflip=true}
|
||||
local filters_all = filters_runtime
|
||||
for k,v in pairs(filters_reset) do filters_all[k] = v end
|
||||
|
||||
local last_vf_reset = ""
|
||||
local last_vf_runtime = ""
|
||||
|
||||
local last_rotate = 0
|
||||
|
||||
local par = ""
|
||||
local last_par = ""
|
||||
|
||||
local function get_os()
|
||||
local raw_os_name = ""
|
||||
|
||||
if jit and jit.os and jit.arch then
|
||||
raw_os_name = jit.os
|
||||
else
|
||||
if package.config:sub(1,1) == "\\" then
|
||||
-- Windows
|
||||
local env_OS = os.getenv("OS")
|
||||
if env_OS then
|
||||
raw_os_name = env_OS
|
||||
end
|
||||
else
|
||||
raw_os_name = mp.command_native({name = "subprocess", playback_only = false, capture_stdout = true, args = {"uname", "-s"}}).stdout
|
||||
end
|
||||
end
|
||||
|
||||
raw_os_name = (raw_os_name):lower()
|
||||
|
||||
local os_patterns = {
|
||||
["windows"] = "Windows",
|
||||
|
||||
-- Uses socat
|
||||
["linux"] = "Linux",
|
||||
|
||||
["osx"] = "Mac",
|
||||
["mac"] = "Mac",
|
||||
["darwin"] = "Mac",
|
||||
|
||||
["^mingw"] = "Windows",
|
||||
["^cygwin"] = "Windows",
|
||||
|
||||
-- Because they have the good netcat (with -U)
|
||||
["bsd$"] = "Mac",
|
||||
["sunos"] = "Mac"
|
||||
}
|
||||
|
||||
-- Default to linux
|
||||
local str_os_name = "Linux"
|
||||
|
||||
for pattern, name in pairs(os_patterns) do
|
||||
if raw_os_name:match(pattern) then
|
||||
str_os_name = name
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
return str_os_name
|
||||
end
|
||||
|
||||
local function vf_string(filters, full)
|
||||
local vf = ""
|
||||
local vf_table = mp.get_property_native("vf")
|
||||
|
||||
if #vf_table > 0 then
|
||||
for i = #vf_table, 1, -1 do
|
||||
if filters[vf_table[i].name] then
|
||||
local args = ""
|
||||
for key, value in pairs(vf_table[i].params) do
|
||||
if args ~= "" then
|
||||
args = args .. ":"
|
||||
end
|
||||
args = args .. key .. "=" .. value
|
||||
end
|
||||
vf = vf .. vf_table[i].name .. "=" .. args .. ","
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if full then
|
||||
vf = vf.."scale=w="..effective_w..":h="..effective_h..par..",pad=w="..effective_w..":h="..effective_h..":x=(ow-iw)/2:y=(oh-ih)/2,format=bgra"
|
||||
end
|
||||
|
||||
return vf
|
||||
end
|
||||
|
||||
local function calc_dimensions()
|
||||
local width = mp.get_property_number("video-out-params/dw")
|
||||
local height = mp.get_property_number("video-out-params/dh")
|
||||
if not width or not height then return end
|
||||
|
||||
local scale = mp.get_property_number("display-hidpi-scale", 1)
|
||||
|
||||
if width / height > options.max_width / options.max_height then
|
||||
effective_w = math.floor(options.max_width * scale + 0.5)
|
||||
effective_h = math.floor(height / width * effective_w + 0.5)
|
||||
else
|
||||
effective_h = math.floor(options.max_height * scale + 0.5)
|
||||
effective_w = math.floor(width / height * effective_h + 0.5)
|
||||
end
|
||||
|
||||
thumb_size = effective_w * effective_h * 4
|
||||
|
||||
local v_par = mp.get_property_number("video-out-params/par", 1)
|
||||
if v_par == 1 then
|
||||
par = ":force_original_aspect_ratio=decrease"
|
||||
else
|
||||
par = ""
|
||||
end
|
||||
end
|
||||
|
||||
local function info()
|
||||
local display_w, display_h = effective_w, effective_h
|
||||
if mp.get_property_number("video-params/rotate", 0) % 180 == 90 then
|
||||
display_w, display_h = effective_h, effective_w
|
||||
end
|
||||
|
||||
local json, err = mp.utils.format_json({width=display_w, height=display_h, disabled=disabled, socket=options.socket, thumbnail=options.thumbnail, overlay_id=options.overlay_id})
|
||||
mp.commandv("script-message", "thumbfast-info", json)
|
||||
end
|
||||
|
||||
local function remove_thumbnail_files()
|
||||
os.remove(options.thumbnail)
|
||||
os.remove(options.thumbnail..".bgra")
|
||||
end
|
||||
|
||||
local function spawn(time)
|
||||
if disabled then return end
|
||||
|
||||
local path = mp.get_property("path")
|
||||
if path == nil then return end
|
||||
|
||||
spawned = true
|
||||
|
||||
local open_filename = mp.get_property("stream-open-filename")
|
||||
local ytdl = open_filename and network and path ~= open_filename
|
||||
if ytdl then
|
||||
path = open_filename
|
||||
end
|
||||
|
||||
if os_name == "" then
|
||||
os_name = get_os()
|
||||
end
|
||||
|
||||
if options.socket == "" then
|
||||
if os_name == "Windows" then
|
||||
options.socket = "thumbfast"
|
||||
elseif os_name == "Mac" then
|
||||
options.socket = "/tmp/thumbfast"
|
||||
else
|
||||
options.socket = "/tmp/thumbfast"
|
||||
end
|
||||
end
|
||||
|
||||
if options.thumbnail == "" then
|
||||
if os_name == "Windows" then
|
||||
options.thumbnail = os.getenv("TEMP").."\\thumbfast.out"
|
||||
elseif os_name == "Mac" then
|
||||
options.thumbnail = "/tmp/thumbfast.out"
|
||||
else
|
||||
options.thumbnail = "/tmp/thumbfast.out"
|
||||
end
|
||||
end
|
||||
|
||||
if not init then
|
||||
-- ensure uniqueness
|
||||
options.socket = options.socket .. unique
|
||||
options.thumbnail = options.thumbnail .. unique
|
||||
init = true
|
||||
end
|
||||
|
||||
remove_thumbnail_files()
|
||||
|
||||
calc_dimensions()
|
||||
|
||||
info()
|
||||
|
||||
mp.command_native_async(
|
||||
{name = "subprocess", playback_only = true, args = {
|
||||
"mpv", path, "--no-config", "--msg-level=all=no", "--idle", "--pause", "--keep-open=always",
|
||||
"--edition="..(mp.get_property_number("edition") or "auto"), "--vid="..(mp.get_property_number("vid") or "auto"), "--no-sub", "--no-audio",
|
||||
"--input-ipc-server="..options.socket,
|
||||
"--start="..time, "--hr-seek=no",
|
||||
"--ytdl-format=worst", "--demuxer-readahead-secs=0", "--demuxer-max-bytes=128KiB",
|
||||
"--vd-lavc-skiploopfilter=all", "--vd-lavc-software-fallback=1", "--vd-lavc-fast",
|
||||
"--vf="..vf_string(filters_all, true),
|
||||
"--sws-allow-zimg=no", "--sws-fast=yes", "--sws-scaler=fast-bilinear",
|
||||
"--video-rotate="..last_rotate,
|
||||
"--ovc=rawvideo", "--of=image2", "--ofopts=update=1", "--o="..options.thumbnail
|
||||
}},
|
||||
function() end
|
||||
)
|
||||
end
|
||||
|
||||
local function run(command, callback)
|
||||
if not spawned then return end
|
||||
|
||||
callback = callback or function() end
|
||||
|
||||
local seek_command
|
||||
if os_name == "Windows" then
|
||||
seek_command = {"cmd", "/c", "echo "..command.." > \\\\.\\pipe\\" .. options.socket}
|
||||
elseif os_name == "Mac" then
|
||||
-- this doesn't work, on my system. not sure why.
|
||||
seek_command = {"/usr/bin/env", "sh", "-c", "echo '"..command.."' | nc -w0 -U " .. options.socket}
|
||||
else
|
||||
seek_command = {"/usr/bin/env", "sh", "-c", "echo '" .. command .. "' | socat - " .. options.socket}
|
||||
end
|
||||
|
||||
mp.command_native_async(
|
||||
{name = "subprocess", playback_only = true, capture_stdout = true, args = seek_command},
|
||||
callback
|
||||
)
|
||||
end
|
||||
|
||||
local function thumb_index(thumbtime)
|
||||
return math.floor(thumbtime / interval)
|
||||
end
|
||||
|
||||
local function index_time(index, thumbtime)
|
||||
if interval > 0 then
|
||||
local time = index * interval
|
||||
return time + interval / 3
|
||||
else
|
||||
return thumbtime
|
||||
end
|
||||
end
|
||||
|
||||
local function draw(w, h, thumbtime, display_time, script)
|
||||
local display_w, display_h = w, h
|
||||
if mp.get_property_number("video-params/rotate", 0) % 180 == 90 then
|
||||
display_w, display_h = h, w
|
||||
end
|
||||
|
||||
if x ~= nil then
|
||||
mp.command_native(
|
||||
{name = "overlay-add", id=options.overlay_id, x=x, y=y, file=options.thumbnail..".bgra", offset=0, fmt="bgra", w=display_w, h=display_h, stride=(4*display_w)}
|
||||
)
|
||||
elseif script then
|
||||
local json, err = mp.utils.format_json({width=display_w, height=display_h, x=x, y=y, socket=options.socket, thumbnail=options.thumbnail, overlay_id=options.overlay_id})
|
||||
mp.commandv("script-message-to", script, "thumbfast-render", json)
|
||||
end
|
||||
end
|
||||
|
||||
local function display_img(w, h, thumbtime, display_time, script, redraw)
|
||||
if last_display_time > display_time or disabled then return end
|
||||
|
||||
if not redraw then
|
||||
can_generate = false
|
||||
|
||||
local info = mp.utils.file_info(options.thumbnail)
|
||||
if not info or info.size ~= thumb_size then
|
||||
if thumbtime == -1 then
|
||||
can_generate = true
|
||||
return
|
||||
end
|
||||
|
||||
if thumbtime < 0 then
|
||||
thumbtime = thumbtime + 1
|
||||
end
|
||||
|
||||
-- display last successful thumbnail if one exists
|
||||
local info2 = mp.utils.file_info(options.thumbnail..".bgra")
|
||||
if info2 and info2.size == thumb_size then
|
||||
draw(w, h, thumbtime, display_time, script)
|
||||
end
|
||||
|
||||
-- retry up to 5 times
|
||||
return mp.add_timeout(0.05, function() display_img(w, h, thumbtime < 0 and thumbtime or -5, display_time, script) end)
|
||||
end
|
||||
|
||||
if last_display_time > display_time then return end
|
||||
|
||||
-- os.rename can't replace files on windows
|
||||
if os_name == "Windows" then
|
||||
os.remove(options.thumbnail..".bgra")
|
||||
end
|
||||
-- move the file because it can get overwritten while overlay-add is reading it, and crash the player
|
||||
os.rename(options.thumbnail, options.thumbnail..".bgra")
|
||||
|
||||
last_display_time = display_time
|
||||
else
|
||||
local info = mp.utils.file_info(options.thumbnail..".bgra")
|
||||
if not info or info.size ~= thumb_size then
|
||||
-- still waiting on intial thumbnail
|
||||
return mp.add_timeout(0.05, function() display_img(w, h, thumbtime, display_time, script) end)
|
||||
end
|
||||
if not can_generate then
|
||||
return draw(w, h, thumbtime, display_time, script)
|
||||
end
|
||||
end
|
||||
|
||||
draw(w, h, thumbtime, display_time, script)
|
||||
|
||||
can_generate = true
|
||||
|
||||
if not redraw then
|
||||
-- often, the file we read will be the last requested thumbnail
|
||||
-- retry after a small delay to ensure we got the latest image
|
||||
if thumbtime ~= -1 then
|
||||
mp.add_timeout(0.05, function() display_img(w, h, -1, display_time, script) end)
|
||||
mp.add_timeout(0.1, function() display_img(w, h, -1, display_time, script) end)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function thumb(time, r_x, r_y, script)
|
||||
if disabled then return end
|
||||
|
||||
time = tonumber(time)
|
||||
if time == nil then return end
|
||||
|
||||
if r_x == nil or r_y == nil then
|
||||
x, y = nil, nil
|
||||
else
|
||||
x, y = math.floor(r_x + 0.5), math.floor(r_y + 0.5)
|
||||
end
|
||||
|
||||
local index = thumb_index(time)
|
||||
local seek_time = index_time(index, time)
|
||||
|
||||
if last_request == seek_time or (interval > 0 and index == last_index) then
|
||||
last_index = index
|
||||
if x ~= last_x or y ~= last_y then
|
||||
last_x, last_y = x, y
|
||||
display_img(effective_w, effective_h, time, mp.get_time(), script, true)
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
local cur_request_time = mp.get_time()
|
||||
|
||||
last_index = index
|
||||
last_request_time = cur_request_time
|
||||
last_request = seek_time
|
||||
|
||||
if not spawned then
|
||||
spawn(seek_time)
|
||||
if can_generate then
|
||||
display_img(effective_w, effective_h, time, cur_request_time, script)
|
||||
mp.add_timeout(0.15, function() display_img(effective_w, effective_h, time, cur_request_time, script) end)
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
run("async seek "..seek_time.." absolute+keyframes", function() if can_generate then display_img(effective_w, effective_h, time, cur_request_time, script) end end)
|
||||
end
|
||||
|
||||
local function clear()
|
||||
last_display_time = mp.get_time()
|
||||
can_generate = true
|
||||
last_x = nil
|
||||
last_y = nil
|
||||
mp.command_native(
|
||||
{name = "overlay-remove", id=options.overlay_id}
|
||||
)
|
||||
end
|
||||
|
||||
local function watch_changes()
|
||||
local old_w = effective_w
|
||||
local old_h = effective_h
|
||||
|
||||
calc_dimensions()
|
||||
|
||||
local vf_reset = vf_string(filters_reset)
|
||||
local rotate = mp.get_property_number("video-rotate", 0)
|
||||
|
||||
if spawned then
|
||||
if old_w ~= effective_w or old_h ~= effective_h or last_vf_reset ~= vf_reset or (last_rotate % 180) ~= (rotate % 180) or par ~= last_par then
|
||||
last_rotate = rotate
|
||||
-- mpv doesn't allow us to change output size
|
||||
run("quit")
|
||||
clear()
|
||||
info()
|
||||
spawned = false
|
||||
spawn(last_request or mp.get_property_number("time-pos", 0))
|
||||
else
|
||||
if rotate ~= last_rotate then
|
||||
run("set video-rotate "..rotate)
|
||||
end
|
||||
local vf_runtime = vf_string(filters_runtime)
|
||||
if vf_runtime ~= last_vf_runtime then
|
||||
run("vf set "..vf_string(filters_all, true))
|
||||
last_vf_runtime = vf_runtime
|
||||
end
|
||||
end
|
||||
else
|
||||
if old_w ~= effective_w or old_h ~= effective_h or last_vf_reset ~= vf_reset or (last_rotate % 180) ~= (rotate % 180) or par ~= last_par then
|
||||
last_rotate = rotate
|
||||
info()
|
||||
end
|
||||
last_vf_runtime = vf_string(filters_runtime)
|
||||
end
|
||||
|
||||
last_vf_reset = vf_reset
|
||||
last_rotate = rotate
|
||||
last_par = par
|
||||
end
|
||||
|
||||
local function sync_changes(prop, val)
|
||||
if spawned and val then
|
||||
run("set "..prop.." "..val)
|
||||
end
|
||||
end
|
||||
|
||||
local function file_load()
|
||||
clear()
|
||||
|
||||
network = mp.get_property_bool("demuxer-via-network", false)
|
||||
local image = mp.get_property_native('current-tracks/video/image', true)
|
||||
local albumart = image and mp.get_property_native("current-tracks/video/albumart", false)
|
||||
|
||||
disabled = (network and not options.network) or (albumart and not options.audio) or (image and not albumart)
|
||||
info()
|
||||
if disabled then return end
|
||||
|
||||
interval = math.min(math.max(mp.get_property_number("duration", 1) / options.max_thumbnails, options.interval), mp.get_property_number("duration", options.interval * options.min_thumbnails) / options.min_thumbnails)
|
||||
|
||||
spawned = false
|
||||
if options.spawn_first then spawn(mp.get_property_number("time-pos", 0)) end
|
||||
end
|
||||
|
||||
local function shutdown()
|
||||
run("quit")
|
||||
remove_thumbnail_files()
|
||||
os.remove(options.socket)
|
||||
end
|
||||
|
||||
mp.observe_property("display-hidpi-scale", "native", watch_changes)
|
||||
mp.observe_property("video-out-params", "native", watch_changes)
|
||||
mp.observe_property("vf", "native", watch_changes)
|
||||
mp.observe_property("vid", "native", sync_changes)
|
||||
mp.observe_property("edition", "native", sync_changes)
|
||||
|
||||
mp.register_script_message("thumb", thumb)
|
||||
mp.register_script_message("clear", clear)
|
||||
|
||||
mp.register_event("file-loaded", file_load)
|
||||
mp.register_event("shutdown", shutdown)
|
4708
xfce-custom/dots/home/neko/.config/mpv/scripts/uosc.lua
Normal file
4708
xfce-custom/dots/home/neko/.config/mpv/scripts/uosc.lua
Normal file
File diff suppressed because it is too large
Load Diff
2913
xfce-custom/dots/home/neko/.config/mpv/scripts/webm.lua
Normal file
2913
xfce-custom/dots/home/neko/.config/mpv/scripts/webm.lua
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user