mpv and xfce configs

This commit is contained in:
starlight 2024-03-30 05:58:29 +00:00
parent fde3657808
commit 38f2cc85b6
28 changed files with 11664 additions and 0 deletions

Binary file not shown.

View File

@ -0,0 +1,14 @@
[Default Applications]
x-scheme-handler/http=xfce4-web-browser.desktop
x-scheme-handler/https=xfce4-web-browser.desktop
image/jpeg=sxiv.desktop
image/png=sxiv.desktop
image/gif=sxiv.desktop
[Added Associations]
x-scheme-handler/http=xfce4-web-browser.desktop;
x-scheme-handler/https=xfce4-web-browser.desktop;
image/jpeg=librewolf.desktop;sxiv.desktop;
image/png=sxiv.desktop;
image/gif=sxiv.desktop;
application/x-desktop=librewolf.desktop;

View File

@ -0,0 +1,10 @@
F1 script-message-to command_palette show-command-palette bindings # Show bindings
F2 script-message-to command_palette show-command-palette commands # Show commands
F3 script-message-to command_palette show-command-palette properties # Show properties
F4 script-message-to command_palette show-command-palette options # Show options
F8 script-message-to command_palette show-command-palette playlist # Show playlist
Alt+c script-message-to command_palette show-command-palette chapters # Show chapters
Alt+a script-message-to command_palette show-command-palette audio # Show audio tracks
Alt+s script-message-to command_palette show-command-palette subtitle # Show subtitle tracks
Alt+v script-message-to command_palette show-command-palette video # Show video tracks
Alt+p script-message-to command_palette show-command-palette profiles # Show profiles

View File

@ -0,0 +1,7 @@
# required so that the 2 UIs don't fight each other
osc=no
# uosc provides its own seeking/volume indicators, so you also don't need this
osd-bar=no
# uosc will draw its own window controls if you disable window border
border=yes
profile=pseudo-gui

View File

@ -0,0 +1,897 @@
local mp = require 'mp'
local utils = require 'mp.utils'
local assdraw = require 'mp.assdraw'
-- create namespace with default values
local em = {
-- customisable values ------------------------------------------------------
lines_to_show = 17, -- NOT including search line
pause_on_open = true,
resume_on_exit = "only-if-was-paused", -- another possible value is true
-- styles (earlyer it was a table, but required many more steps to pass def-s
-- here from .conf file)
font_size = 21,
-- cursor 'width', useful to change if you have hidpi monitor
cursor_x_border = 0.3,
line_bottom_margin = 1, -- basically space between lines
text_color = {
default = 'ffffff',
accent = 'd8a07b',
current = 'aaaaaa',
comment = '636363',
},
menu_x_padding = 5, -- this padding for now applies only to 'left', not x
menu_y_padding = 2, -- but this one applies to both - top & bottom
-- values that should be passed from main script ----------------------------
search_heading = 'Default search heading',
-- 'full' is required from main script, 'current_i' is optional
-- others are 'private'
list = {
full = {}, filtered = {}, current_i = nil, pointer_i = 1, show_from_to = {}
},
-- field to compare with when searching for 'current value' by 'current_i'
index_field = 'index',
-- fields to use when searching for string match / any other custom searching
-- if value has 0 length, then search list item itself
filter_by_fields = {},
-- 'private' values that are not supposed to be changed from the outside ----
is_active = false,
-- https://mpv.io/manual/master/#lua-scripting-mp-create-osd-overlay(format)
ass = mp.create_osd_overlay("ass-events"),
was_paused = false, -- flag that indicates that vid was paused by this script
line = '',
-- if there was no cursor it wouldn't have been needed, but for now we need
-- variable below only to compare it with 'line' and see if we need to filter
prev_line = '',
cursor = 1,
history = {},
history_pos = 1,
key_bindings = {},
insert_mode = false,
-- used only in 'update' func to get error text msgs
error_codes = {
no_match = 'Match required',
no_submit_provided = 'No submit function provided'
}
}
-- PRIVATE METHODS ------------------------------------------------------------
-- declare constructor function
function em:new(o)
o = o or {}
setmetatable(o, self)
self.__index = self
-- some options might be customised by user in .conf file and read as strings
-- in that case parse those
if type(o.filter_by_fields) == 'string' then
o.filter_by_fields = utils.parse_json(o.filter_by_fields)
end
if type(o.text_color) == 'string' then
o.text_color = utils.parse_json(o.text_color)
end
return o
end
-- this func is just a getter of a current list depending on search line
function em:current()
return self.line == '' and self.list.full or self.list.filtered
end
-- REVIEW: how to get rid of this wrapper and handle filter func sideeffects
-- in a more elegant way?
function em:filter_wrapper()
-- handles sideeffect that are needed to be run on filtering list
-- cuz the filter func may be redefined in main script and therefore needs
-- to be straight forward - only doing filtering and returning the table
-- passing current query just in case, so ppl can use it in their custom funcs
self.list.filtered = self:filter(self.line)
self.prev_line = self.line
self.list.pointer_i = 1
self:set_from_to(true)
end
function em:set_from_to(reset_flag)
-- additional variables just for shorter var name
local i = self.list.pointer_i
local to_show = self.lines_to_show
local total = #self:current()
if reset_flag or to_show >= total then
self.list.show_from_to = { 1, math.min(to_show, total) }
return
end
-- If menu is opened with something already selected we want this 'selected'
-- to be displayed close to the middle of the menu. That's why 'show_from_to'
-- is not initially set, so we can know - if show_from_to length is 0 - it is
-- first call of this func in cur. init
if #self.list.show_from_to == 0 then
-- set show_from_to so chosen item will be displayed close to middle
local half_list = math.ceil(to_show / 2)
if i < half_list then
self.list.show_from_to = { 1, to_show }
elseif total - i < half_list then
self.list.show_from_to = { total - to_show + 1, total }
else
self.list.show_from_to = { i - half_list + 1, i - half_list + to_show }
end
else
local first, last = table.unpack(self.list.show_from_to)
-- handle cursor moving towards start / end bondary
if first ~= 1 and i - first < 2 then
self.list.show_from_to = { first - 1, last - 1 }
end
if last ~= total and last - i < 2 then
self.list.show_from_to = { first + 1, last + 1 }
end
-- handle index jumps from beginning to end and backwards
if i > last then
self.list.show_from_to = { i - to_show + 1, i }
end
if i < first then self.list.show_from_to = { 1, to_show } end
end
end
function em:change_selected_index(num)
self.list.pointer_i = self.list.pointer_i + num
if self.list.pointer_i < 1 then
self.list.pointer_i = #self:current()
elseif self.list.pointer_i > #self:current() then
self.list.pointer_i = 1
end
self:set_from_to()
self:update()
end
-- Render the REPL and console as an ASS OSD
function em:update(err_code)
-- ASS tags documentation here - https://aegi.vmoe.info/docs/3.0/ASS_Tags/
-- do not bother if function was called to close the menu..
if not self.is_active then
em.ass:remove()
return
end
local line_height = self.font_size + self.line_bottom_margin
local ww, wh = mp.get_osd_size() -- window width & height
-- '+ 1' below is a search string
local menu_y_pos =
wh - (line_height * (self.lines_to_show + 1) + self.menu_y_padding * 2)
-- didn't find better place to handle filtered list update
if self.line ~= self.prev_line then self:filter_wrapper() end
local function get_background()
local a = self:ass_new_wrapper()
a:append('{\\1c&H1c1c1c\\1a&H19}') -- background color & opacity
a:pos(0, 0)
a:draw_start()
a:rect_cw(0, menu_y_pos, ww, wh)
a:draw_stop()
return a.text
end
local function get_search_header()
local a = self:ass_new_wrapper()
a:pos(self.menu_x_padding, menu_y_pos + self.menu_y_padding)
local search_prefix = table.concat({
self:get_font_color('accent'),
(#self:current() ~= 0 and self.list.pointer_i or '!'),
'/', #self:current(), '\\h\\h', self.search_heading, ':\\h'
});
a:append(search_prefix)
-- reset font color after search prefix
a:append(self:get_font_color 'default')
-- Create the cursor glyph as an ASS drawing. ASS will draw the cursor
-- inline with the surrounding text, but it sets the advance to the width
-- of the drawing. So the cursor doesn't affect layout too much, make it as
-- thin as possible and make it appear to be 1px wide by giving it 0.5px
-- horizontal borders.
local cheight = self.font_size * 8
-- TODO: maybe do it using draw_rect from ass?
local cglyph = '{\\r' .. -- styles reset
'\\1c&Hffffff&\\3c&Hffffff' .. -- font color and border color
'\\xbord' .. self.cursor_x_border .. '\\p4\\pbo24}' .. -- xborder, scale x8 and baseline offset
'm 0 0 l 0 ' .. cheight .. -- drawing just a line
'{\\p0\\r}' -- finish drawing and reset styles
local before_cur = self:ass_escape(self.line:sub(1, self.cursor - 1))
local after_cur = self:ass_escape(self.line:sub(self.cursor))
a:append(table.concat({
before_cur, cglyph, self:reset_styles(),
self:get_font_color('default'), after_cur,
(err_code and '\\h' .. self.error_codes[err_code] or "")
}))
return a.text
-- NOTE: perhaps this commented code will some day help me in coding cursor
-- like in M-x emacs menu:
-- Redraw the cursor with the REPL text invisible. This will make the
-- cursor appear in front of the text.
-- ass:new_event()
-- ass:an(1)
-- ass:append(style .. '{\\alpha&HFF&}> ' .. before_cur)
-- ass:append(cglyph)
-- ass:append(style .. '{\\alpha&HFF&}' .. after_cur)
end
local function get_list()
local a = assdraw.ass_new()
local function apply_highlighting(y)
a:new_event()
a:append(self:reset_styles())
a:append('{\\1c&Hffffff\\1a&HE6}') -- background color & opacity
a:pos(0, 0)
a:draw_start()
a:rect_cw(0, y, ww, y + self.font_size)
a:draw_stop()
end
-- REVIEW: maybe make another function 'get_line_str' and move there
-- everything from this for loop?
-- REVIEW: how to use something like table.unpack below?
for i = self.list.show_from_to[1], self.list.show_from_to[2] do
local value = assert(self:current()[i], 'no value with index ' .. i)
local y_offset = menu_y_pos + self.menu_y_padding +
(line_height * (i - self.list.show_from_to[1] + 1))
if i == self.list.pointer_i then apply_highlighting(y_offset) end
a:new_event()
a:append(self:reset_styles())
a:pos(self.menu_x_padding, y_offset)
a:append(self:get_line(i, value))
end
return a.text
end
em.ass.res_x = ww
em.ass.res_y = wh
em.ass.data = table.concat({
get_background(),
get_search_header(),
get_list()
}, "\n")
em.ass:update()
end
-- params:
-- - data : {list: {}, [current_i] : num}
function em:init(data)
self.list.full = data.list or {}
self.list.current_i = data.current_i or nil
self.list.pointer_i = data.current_i or 1
self:set_active(true)
end
function em:exit()
self:undefine_key_bindings()
collectgarbage()
end
-- TODO: write some idle func like this
-- function idle()
-- if pending_selection then
-- gallery:set_selection(pending_selection)
-- pending_selection = nil
-- end
-- if ass_changed or geometry_changed then
-- local ww, wh = mp.get_osd_size()
-- if geometry_changed then
-- geometry_changed = false
-- compute_geometry(ww, wh)
-- end
-- if ass_changed then
-- ass_changed = false
-- mp.set_osd_ass(ww, wh, ass)
-- end
-- end
-- end
-- ...
-- and handle it as follows
-- init():
-- mp.register_idle(idle)
-- idle()
-- exit():
-- mp.unregister_idle(idle)
-- idle()
-- And in these observers he is setting a flag, that's being checked in func above
-- mp.observe_property("osd-width", "native", mark_geometry_stale)
-- mp.observe_property("osd-height", "native", mark_geometry_stale)
-- PRIVATE METHODS END --------------------------------------------------------
-- PUBLIC METHODS -------------------------------------------------------------
function em:filter()
-- default filter func, might be redefined in main script
local result = {}
local function get_full_search_str(v)
local str = ''
for _, key in ipairs(self.filter_by_fields) do str = str .. (v[key] or '') end
return str
end
for _, v in ipairs(self.list.full) do
-- if filter_by_fields has 0 length, then search list item itself
if #self.filter_by_fields == 0 then
if self:search_method(v) then table.insert(result, v) end
else
-- NOTE: we might use search_method on fiels separately like this:
-- for _,key in ipairs(self.filter_by_fields) do
-- if self:search_method(v[key]) then table.insert(result, v) end
-- end
-- But since im planning to implement fuzzy search in future i need full
-- search string here
if self:search_method(get_full_search_str(v)) then
table.insert(result, v)
end
end
end
return result
end
-- TODO: implement fuzzy search and maybe match highlights
function em:search_method(str)
-- also might be redefined by main script
-- convert to string just to make sure..
return tostring(str):lower():find(self.line:lower(), 1, true)
end
-- this module requires submit function to be defined in main script
function em:submit() self:update('no_submit_provided') end
function em:update_list(list)
-- for now this func doesn't handle cases when we have 'current_i' to update
-- it
self.list.full = list
if self.line ~= self.prev_line then self:filter_wrapper() end
end
-- PUBLIC METHODS END ---------------------------------------------------------
-- HELPER METHODS -------------------------------------------------------------
function em:get_line(_, v) -- [i]ndex, [v]alue
-- this func might be redefined in main script to get a custom-formatted line
-- default implementation of this func supposes that value.content field is a
-- String
local a = assdraw.ass_new()
local style = (self.list.current_i == v[self.index_field])
and 'current' or 'default'
a:append(self:reset_styles())
a:append(self:get_font_color(style))
-- content as default field, which is holding string
-- no point in moving it to main object since content itself is being
-- composed in THIS function, that might (and most likely, should) be
-- redefined in main script
a:append(v.content or 'Something is off in `get_line` func')
return a.text
end
-- REVIEW: for now i don't see normal way of mergin this func with below one
-- but it's being used only once
function em:reset_styles()
local a = assdraw.ass_new()
-- alignment top left, no word wrapping, border 0, shadow 0
a:append('{\\an7\\q2\\bord0\\shad0}')
a:append('{\\fs' .. self.font_size .. '}')
return a.text
end
-- function to get rid of some copypaste
function em:ass_new_wrapper()
local a = assdraw.ass_new()
a:new_event()
a:append(self:reset_styles())
return a
end
function em:get_font_color(style)
return '{\\1c&H' .. self.text_color[style] .. '}'
end
-- HELPER METHODS END ---------------------------------------------------------
--[[
The below code is a modified implementation of text input from mpv's console.lua:
https://github.com/mpv-player/mpv/blob/87c9eefb2928252497f6141e847b74ad1158bc61/player/lua/console.lua
I was too lazy to list all modifications i've done to the script, but if u
rly need to see those - do diff with the original code
]] --
-------------------------------------------------------------------------------
-- START ORIGINAL MPV CODE --
-------------------------------------------------------------------------------
-- Copyright (C) 2019 the mpv developers
--
-- Permission to use, copy, modify, and/or distribute this software for any
-- purpose with or without fee is hereby granted, provided that the above
-- copyright notice and this permission notice appear in all copies.
--
-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
-- WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
-- MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
-- SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
-- WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
-- OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
-- CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
function em:detect_platform()
local o = {}
-- Kind of a dumb way of detecting the platform but whatever
if mp.get_property_native('options/vo-mmcss-profile', o) ~= o then
return 'windows'
elseif mp.get_property_native('options/macos-force-dedicated-gpu', o) ~= o then
return 'macos'
elseif os.getenv('WAYLAND_DISPLAY') then
return 'wayland'
end
return 'x11'
end
-- Escape a string for verbatim display on the OSD
function em:ass_escape(str)
-- There is no escape for '\' in ASS (I think?) but '\' is used verbatim if
-- it isn't followed by a recognised character, so add a zero-width
-- non-breaking space
str = str:gsub('\\', '\\\239\187\191')
str = str:gsub('{', '\\{')
str = str:gsub('}', '\\}')
-- Precede newlines with a ZWNBSP to prevent ASS's weird collapsing of
-- consecutive newlines
str = str:gsub('\n', '\239\187\191\\N')
-- Turn leading spaces into hard spaces to prevent ASS from stripping them
str = str:gsub('\\N ', '\\N\\h')
str = str:gsub('^ ', '\\h')
return str
end
-- Set the REPL visibility ("enable", Esc)
function em:set_active(active)
if active == self.is_active then return end
if active then
self.is_active = true
self.insert_mode = false
mp.enable_messages('terminal-default')
self:define_key_bindings()
-- set flag 'was_paused' only if vid wasn't paused before EM init
if self.pause_on_open and not mp.get_property_bool("pause", false) then
mp.set_property_bool("pause", true)
self.was_paused = true
end
self:set_from_to()
self:update()
else
-- no need to call 'update' in this block cuz 'clear' method is calling it
self.is_active = false
self:undefine_key_bindings()
if self.resume_on_exit == true or
(self.resume_on_exit == "only-if-was-paused" and self.was_paused) then
mp.set_property_bool("pause", false)
end
self:clear()
collectgarbage()
end
end
-- Naive helper function to find the next UTF-8 character in 'str' after 'pos'
-- by skipping continuation bytes. Assumes 'str' contains valid UTF-8.
function em:next_utf8(str, pos)
if pos > str:len() then return pos end
repeat
pos = pos + 1
until pos > str:len() or str:byte(pos) < 0x80 or str:byte(pos) > 0xbf
return pos
end
-- As above, but finds the previous UTF-8 charcter in 'str' before 'pos'
function em:prev_utf8(str, pos)
if pos <= 1 then return pos end
repeat
pos = pos - 1
until pos <= 1 or str:byte(pos) < 0x80 or str:byte(pos) > 0xbf
return pos
end
-- Insert a character at the current cursor position (any_unicode)
function em:handle_char_input(c)
if self.insert_mode then
self.line = self.line:sub(1, self.cursor - 1) .. c .. self.line:sub(self:next_utf8(self.line, self.cursor))
else
self.line = self.line:sub(1, self.cursor - 1) .. c .. self.line:sub(self.cursor)
end
self.cursor = self.cursor + #c
self:update()
end
-- Remove the character behind the cursor (Backspace)
function em:handle_backspace()
if self.cursor <= 1 then return end
local prev = self:prev_utf8(self.line, self.cursor)
self.line = self.line:sub(1, prev - 1) .. self.line:sub(self.cursor)
self.cursor = prev
self:update()
end
-- Remove the character in front of the cursor (Del)
function em:handle_del()
if self.cursor > self.line:len() then return end
self.line = self.line:sub(1, self.cursor - 1) .. self.line:sub(self:next_utf8(self.line, self.cursor))
self:update()
end
-- Toggle insert mode (Ins)
function em:handle_ins()
self.insert_mode = not self.insert_mode
end
-- Move the cursor to the next character (Right)
function em:next_char()
self.cursor = self:next_utf8(self.line, self.cursor)
self:update()
end
-- Move the cursor to the previous character (Left)
function em:prev_char()
self.cursor = self:prev_utf8(self.line, self.cursor)
self:update()
end
-- Clear the current line (Ctrl+C)
function em:clear()
self.line = ''
self.prev_line = ''
self.list.current_i = nil
self.list.pointer_i = 1
self.list.filtered = {}
self.list.show_from_to = {}
self.was_paused = false
self.cursor = 1
self.insert_mode = false
self.history_pos = #self.history + 1
self:update()
end
-- Run the current command and clear the line (Enter)
function em:handle_enter()
if #self:current() == 0 then
self:update('no_match')
return
end
if self.history[#self.history] ~= self.line then
self.history[#self.history + 1] = self.line
end
self:submit(self:current()[self.list.pointer_i])
self:set_active(false)
end
-- Go to the specified position in the command history
function em:go_history(new_pos)
local old_pos = self.history_pos
self.history_pos = new_pos
-- Restrict the position to a legal value
if self.history_pos > #self.history + 1 then
self.history_pos = #self.history + 1
elseif self.history_pos < 1 then
self.history_pos = 1
end
-- Do nothing if the history position didn't actually change
if self.history_pos == old_pos then
return
end
-- If the user was editing a non-history line, save it as the last history
-- entry. This makes it much less frustrating to accidentally hit Up/Down
-- while editing a line.
if old_pos == #self.history + 1 and self.line ~= '' and self.history[#self.history] ~= self.line then
self.history[#self.history + 1] = self.line
end
-- Now show the history line (or a blank line for #history + 1)
if self.history_pos <= #self.history then
self.line = self.history[self.history_pos]
else
self.line = ''
end
self.cursor = self.line:len() + 1
self.insert_mode = false
self:update()
end
-- Go to the specified relative position in the command history (Up, Down)
function em:move_history(amount)
self:go_history(self.history_pos + amount)
end
-- Go to the first command in the command history (PgUp)
function em:handle_pgup()
self:go_history(1)
end
-- Stop browsing history and start editing a blank line (PgDown)
function em:handle_pgdown()
self:go_history(#self.history + 1)
end
-- Move to the start of the current word, or if already at the start, the start
-- of the previous word. (Ctrl+Left)
function em:prev_word()
-- This is basically the same as next_word() but backwards, so reverse the
-- string in order to do a "backwards" find. This wouldn't be as annoying
-- to do if Lua didn't insist on 1-based indexing.
self.cursor = self.line:len() - select(2, self.line:reverse():find('%s*[^%s]*', self.line:len() - self.cursor + 2)) + 1
self:update()
end
-- Move to the end of the current word, or if already at the end, the end of
-- the next word. (Ctrl+Right)
function em:next_word()
self.cursor = select(2, self.line:find('%s*[^%s]*', self.cursor)) + 1
self:update()
end
-- Move the cursor to the beginning of the line (HOME)
function em:go_home()
self.cursor = 1
self:update()
end
-- Move the cursor to the end of the line (END)
function em:go_end()
self.cursor = self.line:len() + 1
self:update()
end
-- Delete from the cursor to the beginning of the word (Ctrl+Backspace)
function em:del_word()
local before_cur = self.line:sub(1, self.cursor - 1)
local after_cur = self.line:sub(self.cursor)
before_cur = before_cur:gsub('[^%s]+%s*$', '', 1)
self.line = before_cur .. after_cur
self.cursor = before_cur:len() + 1
self:update()
end
-- Delete from the cursor to the end of the word (Ctrl+Del)
function em:del_next_word()
if self.cursor > self.line:len() then return end
local before_cur = self.line:sub(1, self.cursor - 1)
local after_cur = self.line:sub(self.cursor)
after_cur = after_cur:gsub('^%s*[^%s]+', '', 1)
self.line = before_cur .. after_cur
self:update()
end
-- Delete from the cursor to the end of the line (Ctrl+K)
function em:del_to_eol()
self.line = self.line:sub(1, self.cursor - 1)
self:update()
end
-- Delete from the cursor back to the start of the line (Ctrl+U)
function em:del_to_start()
self.line = self.line:sub(self.cursor)
self.cursor = 1
self:update()
end
-- Returns a string of UTF-8 text from the clipboard (or the primary selection)
function em:get_clipboard(clip)
-- Pick a better default font for Windows and macOS
local platform = self:detect_platform()
if platform == 'x11' then
local res = utils.subprocess({
args = { 'xclip', '-selection', clip and 'clipboard' or 'primary', '-out' },
playback_only = false,
})
if not res.error then
return res.stdout
end
elseif platform == 'wayland' then
local res = utils.subprocess({
args = { 'wl-paste', clip and '-n' or '-np' },
playback_only = false,
})
if not res.error then
return res.stdout
end
elseif platform == 'windows' then
local res = utils.subprocess({
args = { 'powershell', '-NoProfile', '-Command', [[& {
Trap {
Write-Error -ErrorRecord $_
Exit 1
}
$clip = ""
if (Get-Command "Get-Clipboard" -errorAction SilentlyContinue) {
$clip = Get-Clipboard -Raw -Format Text -TextFormatType UnicodeText
} else {
Add-Type -AssemblyName PresentationCore
$clip = [Windows.Clipboard]::GetText()
}
$clip = $clip -Replace "`r",""
$u8clip = [System.Text.Encoding]::UTF8.GetBytes($clip)
[Console]::OpenStandardOutput().Write($u8clip, 0, $u8clip.Length)
}]] },
playback_only = false,
})
if not res.error then
return res.stdout
end
elseif platform == 'macos' then
local res = utils.subprocess({
args = { 'pbpaste' },
playback_only = false,
})
if not res.error then
return res.stdout
end
end
return ''
end
-- Paste text from the window-system's clipboard. 'clip' determines whether the
-- clipboard or the primary selection buffer is used (on X11 and Wayland only.)
function em:paste(clip)
local text = self:get_clipboard(clip)
local before_cur = self.line:sub(1, self.cursor - 1)
local after_cur = self.line:sub(self.cursor)
self.line = before_cur .. text .. after_cur
self.cursor = self.cursor + text:len()
self:update()
end
-- List of input bindings. This is a weird mashup between common GUI text-input
-- bindings and readline bindings.
function em:get_bindings()
local bindings = {
{ 'ctrl+[', function() self:set_active(false) end },
{ 'ctrl+g', function() self:set_active(false) end },
{ 'esc', function() self:set_active(false) end },
{ 'enter', function() self:handle_enter() end },
{ 'kp_enter', function() self:handle_enter() end },
{ 'ctrl+m', function() self:handle_enter() end },
{ 'bs', function() self:handle_backspace() end },
{ 'shift+bs', function() self:handle_backspace() end },
{ 'ctrl+h', function() self:handle_backspace() end },
{ 'del', function() self:handle_del() end },
{ 'shift+del', function() self:handle_del() end },
{ 'ins', function() self:handle_ins() end },
{ 'shift+ins', function() self:paste(false) end },
{ 'mbtn_mid', function() self:paste(false) end },
{ 'left', function() self:prev_char() end },
{ 'ctrl+b', function() self:prev_char() end },
{ 'right', function() self:next_char() end },
{ 'ctrl+f', function() self:next_char() end },
{ 'ctrl+k', function() self:change_selected_index(-1) end },
{ 'ctrl+p', function() self:change_selected_index(-1) end },
{ 'ctrl+j', function() self:change_selected_index(1) end },
{ 'ctrl+n', function() self:change_selected_index(1) end },
{ 'up', function() self:move_history(-1) end },
{ 'alt+p', function() self:move_history(-1) end },
{ 'wheel_up', function() self:move_history(-1) end },
{ 'down', function() self:move_history(1) end },
{ 'alt+n', function() self:move_history(1) end },
{ 'wheel_down', function() self:move_history(1) end },
{ 'wheel_left', function() end },
{ 'wheel_right', function() end },
{ 'ctrl+left', function() self:prev_word() end },
{ 'alt+b', function() self:prev_word() end },
{ 'ctrl+right', function() self:next_word() end },
{ 'alt+f', function() self:next_word() end },
{ 'ctrl+a', function() self:go_home() end },
{ 'home', function() self:go_home() end },
{ 'ctrl+e', function() self:go_end() end },
{ 'end', function() self:go_end() end },
{ 'pgup', function() self:handle_pgup() end },
{ 'pgdwn', function() self:handle_pgdown() end },
{ 'ctrl+c', function() self:clear() end },
{ 'ctrl+d', function() self:handle_del() end },
{ 'ctrl+u', function() self:del_to_start() end },
{ 'ctrl+v', function() self:paste(true) end },
{ 'meta+v', function() self:paste(true) end },
{ 'ctrl+bs', function() self:del_word() end },
{ 'ctrl+w', function() self:del_word() end },
{ 'ctrl+del', function() self:del_next_word() end },
{ 'alt+d', 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:text_input(info)
if info.key_text and (info.event == "press" or info.event == "down"
or info.event == "repeat")
then
self:handle_char_input(info.key_text)
end
end
function em:define_key_bindings()
if #self.key_bindings > 0 then
return
end
for _, bind in ipairs(self:get_bindings()) do
-- Generate arbitrary name for removing the bindings later.
local name = "search_" .. (#self.key_bindings + 1)
self.key_bindings[#self.key_bindings + 1] = name
mp.add_forced_key_binding(bind[1], name, bind[2], { repeatable = true })
end
mp.add_forced_key_binding("any_unicode", "search_input", function(...)
self:text_input(...)
end, { repeatable = true, complex = true })
self.key_bindings[#self.key_bindings + 1] = "search_input"
end
function em:undefine_key_bindings()
for _, name in ipairs(self.key_bindings) do
mp.remove_key_binding(name)
end
self.key_bindings = {}
end
-------------------------------------------------------------------------------
-- END ORIGINAL MPV CODE --
-------------------------------------------------------------------------------
return em

View File

@ -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)

View File

@ -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]);

View 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)

View 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)

View File

@ -0,0 +1,3 @@
-- This is a dummy main.lua
-- required for mpv 0.33
-- do not delete

View File

@ -0,0 +1,122 @@
import urllib.request
import urllib.parse
import hashlib
import sqlite3
import random
import string
import json
import sys
import os
if sys.argv[1] in ["submit", "stats", "username"]:
if not sys.argv[8]:
if os.path.isfile(sys.argv[7]):
with open(sys.argv[7]) as f:
uid = f.read()
else:
uid = "".join(random.choices(string.ascii_letters + string.digits, k=36))
with open(sys.argv[7], "w") as f:
f.write(uid)
else:
uid = sys.argv[8]
opener = urllib.request.build_opener()
opener.addheaders = [("User-Agent", "mpv_sponsorblock/1.0 (https://github.com/po5/mpv_sponsorblock)")]
urllib.request.install_opener(opener)
if sys.argv[1] == "ranges" and (not sys.argv[2] or not os.path.isfile(sys.argv[2])):
sha = None
if 3 <= int(sys.argv[6]) <= 32:
sha = hashlib.sha256(sys.argv[4].encode()).hexdigest()[:int(sys.argv[6])]
times = []
try:
response = urllib.request.urlopen(sys.argv[3] + "/api/skipSegments" + ("/" + sha + "?" if sha else "?videoID=" + sys.argv[4] + "&") + urllib.parse.urlencode([("categories", json.dumps(sys.argv[5].split(",")))]))
segments = json.load(response)
for segment in segments:
if sha and sys.argv[4] != segment["videoID"]:
continue
if sha:
for s in segment["segments"]:
times.append(str(s["segment"][0]) + "," + str(s["segment"][1]) + "," + s["UUID"] + "," + s["category"])
else:
times.append(str(segment["segment"][0]) + "," + str(segment["segment"][1]) + "," + segment["UUID"] + "," + segment["category"])
print(":".join(times))
except (TimeoutError, urllib.error.URLError) as e:
print("error")
except urllib.error.HTTPError as e:
if e.code == 404:
print("")
else:
print("error")
elif sys.argv[1] == "ranges":
conn = sqlite3.connect(sys.argv[2])
conn.row_factory = sqlite3.Row
c = conn.cursor()
times = []
for category in sys.argv[5].split(","):
c.execute("SELECT startTime, endTime, votes, UUID, category FROM sponsorTimes WHERE videoID = ? AND shadowHidden = 0 AND votes > -1 AND category = ?", (sys.argv[4], category))
sponsors = c.fetchall()
best = list(sponsors)
dealtwith = []
similar = []
for sponsor_a in sponsors:
for sponsor_b in sponsors:
if sponsor_a is not sponsor_b and sponsor_a["startTime"] >= sponsor_b["startTime"] and sponsor_a["startTime"] <= sponsor_b["endTime"]:
similar.append([sponsor_a, sponsor_b])
if sponsor_a in best:
best.remove(sponsor_a)
if sponsor_b in best:
best.remove(sponsor_b)
for sponsors_a in similar:
if sponsors_a in dealtwith:
continue
group = set(sponsors_a)
for sponsors_b in similar:
if sponsors_b[0] in group or sponsors_b[1] in group:
group.add(sponsors_b[0])
group.add(sponsors_b[1])
dealtwith.append(sponsors_b)
best.append(max(group, key=lambda x:x["votes"]))
for time in best:
times.append(str(time["startTime"]) + "," + str(time["endTime"]) + "," + time["UUID"] + "," + time["category"])
print(":".join(times))
elif sys.argv[1] == "update":
try:
urllib.request.urlretrieve(sys.argv[3] + "/database.db", sys.argv[2] + ".tmp")
os.replace(sys.argv[2] + ".tmp", sys.argv[2])
except PermissionError:
print("database update failed, file currently in use", file=sys.stderr)
sys.exit(1)
except ConnectionResetError:
print("database update failed, connection reset", file=sys.stderr)
sys.exit(1)
except TimeoutError:
print("database update failed, timed out", file=sys.stderr)
sys.exit(1)
except urllib.error.URLError:
print("database update failed", file=sys.stderr)
sys.exit(1)
elif sys.argv[1] == "submit":
try:
req = urllib.request.Request(sys.argv[3] + "/api/skipSegments", data=json.dumps({"videoID": sys.argv[4], "segments": [{"segment": [float(sys.argv[5]), float(sys.argv[6])], "category": sys.argv[9]}], "userID": uid}).encode(), headers={"Content-Type": "application/json"})
response = urllib.request.urlopen(req)
print("success")
except urllib.error.HTTPError as e:
print(e.code)
except:
print("error")
elif sys.argv[1] == "stats":
try:
if sys.argv[6]:
urllib.request.urlopen(sys.argv[3] + "/api/viewedVideoSponsorTime?UUID=" + sys.argv[5])
if sys.argv[9]:
urllib.request.urlopen(sys.argv[3] + "/api/voteOnSponsorTime?UUID=" + sys.argv[5] + "&userID=" + uid + "&type=" + sys.argv[9])
except:
pass
elif sys.argv[1] == "username":
try:
data = urllib.parse.urlencode({"userID": uid, "userName": sys.argv[9]}).encode()
req = urllib.request.Request(sys.argv[3] + "/api/setUsername", data=data)
urllib.request.urlopen(req)
except:
pass

View File

@ -0,0 +1 @@
aMn4i07nXmgiLpjJPE235QcbVv3QxdXdH1xB

View 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)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,3 @@
WebBrowser=custom-WebBrowser
TerminalEmulator=xfce4-terminal

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<channel name="displays" version="1.0">
<property name="ActiveProfile" type="string" value="Default"/>
</channel>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<channel name="thunar" version="1.0">
<property name="last-view" type="string" value="ThunarIconView"/>
<property name="last-icon-view-zoom-level" type="string" value="THUNAR_ZOOM_LEVEL_100_PERCENT"/>
<property name="last-separator-position" type="int" value="170"/>
<property name="last-show-hidden" type="bool" value="true"/>
</channel>

View File

@ -0,0 +1,194 @@
<?xml version="1.0" encoding="UTF-8"?>
<channel name="xfce4-keyboard-shortcuts" version="1.0">
<property name="commands" type="empty">
<property name="default" type="empty">
<property name="&lt;Alt&gt;F1" type="empty"/>
<property name="&lt;Alt&gt;F2" type="empty">
<property name="startup-notify" type="empty"/>
</property>
<property name="&lt;Alt&gt;F3" type="empty">
<property name="startup-notify" type="empty"/>
</property>
<property name="&lt;Primary&gt;&lt;Alt&gt;Delete" type="empty"/>
<property name="&lt;Primary&gt;&lt;Alt&gt;l" type="empty"/>
<property name="&lt;Primary&gt;&lt;Alt&gt;t" type="empty"/>
<property name="XF86Display" type="empty"/>
<property name="&lt;Super&gt;p" type="empty"/>
<property name="&lt;Primary&gt;Escape" type="empty"/>
<property name="XF86WWW" type="empty"/>
<property name="HomePage" type="empty"/>
<property name="XF86Mail" type="empty"/>
<property name="Print" type="empty"/>
<property name="&lt;Alt&gt;Print" type="empty"/>
<property name="&lt;Shift&gt;Print" type="empty"/>
<property name="&lt;Super&gt;e" type="empty"/>
<property name="&lt;Primary&gt;&lt;Alt&gt;f" type="empty"/>
<property name="&lt;Primary&gt;&lt;Alt&gt;Escape" type="empty"/>
<property name="&lt;Primary&gt;&lt;Shift&gt;Escape" type="empty"/>
<property name="&lt;Super&gt;r" type="empty">
<property name="startup-notify" type="empty"/>
</property>
</property>
<property name="custom" type="empty">
<property name="&lt;Alt&gt;F2" type="string" value="xfce4-appfinder --collapsed">
<property name="startup-notify" type="bool" value="true"/>
</property>
<property name="&lt;Alt&gt;Print" type="string" value="xfce4-screenshooter -w"/>
<property name="&lt;Super&gt;r" type="string" value="xfce4-appfinder -c">
<property name="startup-notify" type="bool" value="true"/>
</property>
<property name="XF86WWW" type="string" value="exo-open --launch WebBrowser"/>
<property name="XF86Mail" type="string" value="exo-open --launch MailReader"/>
<property name="&lt;Alt&gt;F3" type="string" value="xfce4-appfinder">
<property name="startup-notify" type="bool" value="true"/>
</property>
<property name="Print" type="string" value="xfce4-screenshooter"/>
<property name="&lt;Primary&gt;Escape" type="string" value="xfdesktop --menu"/>
<property name="&lt;Shift&gt;Print" type="string" value="xfce4-screenshooter -r"/>
<property name="&lt;Primary&gt;&lt;Alt&gt;Delete" type="string" value="xfce4-session-logout"/>
<property name="&lt;Primary&gt;&lt;Alt&gt;t" type="string" value="exo-open --launch TerminalEmulator"/>
<property name="&lt;Primary&gt;&lt;Alt&gt;f" type="string" value="thunar"/>
<property name="&lt;Primary&gt;&lt;Alt&gt;l" type="string" value="xflock4"/>
<property name="&lt;Alt&gt;F1" type="string" value="xfce4-popup-applicationsmenu"/>
<property name="&lt;Super&gt;p" type="string" value="xfce4-display-settings --minimal"/>
<property name="&lt;Primary&gt;&lt;Shift&gt;Escape" type="string" value="xfce4-taskmanager"/>
<property name="&lt;Super&gt;e" type="string" value="thunar"/>
<property name="&lt;Primary&gt;&lt;Alt&gt;Escape" type="string" value="xkill"/>
<property name="HomePage" type="string" value="exo-open --launch WebBrowser"/>
<property name="XF86Display" type="string" value="xfce4-display-settings --minimal"/>
<property name="override" type="bool" value="true"/>
</property>
</property>
<property name="xfwm4" type="empty">
<property name="default" type="empty">
<property name="&lt;Alt&gt;Insert" type="empty"/>
<property name="Escape" type="empty"/>
<property name="Left" type="empty"/>
<property name="Right" type="empty"/>
<property name="Up" type="empty"/>
<property name="Down" type="empty"/>
<property name="&lt;Alt&gt;Tab" type="empty"/>
<property name="&lt;Alt&gt;&lt;Shift&gt;Tab" type="empty"/>
<property name="&lt;Alt&gt;Delete" type="empty"/>
<property name="&lt;Primary&gt;&lt;Alt&gt;Down" type="empty"/>
<property name="&lt;Primary&gt;&lt;Alt&gt;Left" type="empty"/>
<property name="&lt;Shift&gt;&lt;Alt&gt;Page_Down" type="empty"/>
<property name="&lt;Alt&gt;F4" type="empty"/>
<property name="&lt;Alt&gt;F6" type="empty"/>
<property name="&lt;Alt&gt;F7" type="empty"/>
<property name="&lt;Alt&gt;F8" type="empty"/>
<property name="&lt;Alt&gt;F9" type="empty"/>
<property name="&lt;Alt&gt;F10" type="empty"/>
<property name="&lt;Alt&gt;F11" type="empty"/>
<property name="&lt;Alt&gt;F12" type="empty"/>
<property name="&lt;Primary&gt;&lt;Shift&gt;&lt;Alt&gt;Left" type="empty"/>
<property name="&lt;Primary&gt;&lt;Alt&gt;End" type="empty"/>
<property name="&lt;Primary&gt;&lt;Alt&gt;Home" type="empty"/>
<property name="&lt;Primary&gt;&lt;Shift&gt;&lt;Alt&gt;Right" type="empty"/>
<property name="&lt;Primary&gt;&lt;Shift&gt;&lt;Alt&gt;Up" type="empty"/>
<property name="&lt;Primary&gt;&lt;Alt&gt;KP_1" type="empty"/>
<property name="&lt;Primary&gt;&lt;Alt&gt;KP_2" type="empty"/>
<property name="&lt;Primary&gt;&lt;Alt&gt;KP_3" type="empty"/>
<property name="&lt;Primary&gt;&lt;Alt&gt;KP_4" type="empty"/>
<property name="&lt;Primary&gt;&lt;Alt&gt;KP_5" type="empty"/>
<property name="&lt;Primary&gt;&lt;Alt&gt;KP_6" type="empty"/>
<property name="&lt;Primary&gt;&lt;Alt&gt;KP_7" type="empty"/>
<property name="&lt;Primary&gt;&lt;Alt&gt;KP_8" type="empty"/>
<property name="&lt;Primary&gt;&lt;Alt&gt;KP_9" type="empty"/>
<property name="&lt;Alt&gt;space" type="empty"/>
<property name="&lt;Shift&gt;&lt;Alt&gt;Page_Up" type="empty"/>
<property name="&lt;Primary&gt;&lt;Alt&gt;Right" type="empty"/>
<property name="&lt;Primary&gt;&lt;Alt&gt;d" type="empty"/>
<property name="&lt;Primary&gt;&lt;Alt&gt;Up" type="empty"/>
<property name="&lt;Super&gt;Tab" type="empty"/>
<property name="&lt;Primary&gt;F1" type="empty"/>
<property name="&lt;Primary&gt;F2" type="empty"/>
<property name="&lt;Primary&gt;F3" type="empty"/>
<property name="&lt;Primary&gt;F4" type="empty"/>
<property name="&lt;Primary&gt;F5" type="empty"/>
<property name="&lt;Primary&gt;F6" type="empty"/>
<property name="&lt;Primary&gt;F7" type="empty"/>
<property name="&lt;Primary&gt;F8" type="empty"/>
<property name="&lt;Primary&gt;F9" type="empty"/>
<property name="&lt;Primary&gt;F10" type="empty"/>
<property name="&lt;Primary&gt;F11" type="empty"/>
<property name="&lt;Primary&gt;F12" type="empty"/>
<property name="&lt;Super&gt;KP_Left" type="empty"/>
<property name="&lt;Super&gt;KP_Right" type="empty"/>
<property name="&lt;Super&gt;KP_Up" type="empty"/>
<property name="&lt;Super&gt;KP_Down" type="empty"/>
<property name="&lt;Super&gt;KP_Page_Up" type="empty"/>
<property name="&lt;Super&gt;KP_Home" type="empty"/>
<property name="&lt;Super&gt;KP_End" type="empty"/>
<property name="&lt;Super&gt;KP_Next" type="empty"/>
</property>
<property name="custom" type="empty">
<property name="&lt;Primary&gt;F12" type="string" value="workspace_12_key"/>
<property name="&lt;Super&gt;KP_Down" type="string" value="tile_up_key"/>
<property name="&lt;Alt&gt;F4" type="string" value="close_window_key"/>
<property name="&lt;Primary&gt;&lt;Alt&gt;KP_3" type="string" value="move_window_workspace_3_key"/>
<property name="&lt;Primary&gt;F2" type="string" value="workspace_2_key"/>
<property name="&lt;Primary&gt;F6" type="string" value="workspace_6_key"/>
<property name="&lt;Primary&gt;&lt;Alt&gt;Down" type="string" value="down_workspace_key"/>
<property name="&lt;Primary&gt;&lt;Alt&gt;KP_9" type="string" value="move_window_workspace_9_key"/>
<property name="&lt;Super&gt;KP_Up" type="string" value="tile_down_key"/>
<property name="&lt;Primary&gt;&lt;Alt&gt;End" type="string" value="move_window_next_workspace_key"/>
<property name="&lt;Primary&gt;F8" type="string" value="workspace_8_key"/>
<property name="&lt;Primary&gt;&lt;Shift&gt;&lt;Alt&gt;Left" type="string" value="move_window_left_key"/>
<property name="&lt;Super&gt;KP_Right" type="string" value="tile_right_key"/>
<property name="&lt;Primary&gt;&lt;Alt&gt;KP_4" type="string" value="move_window_workspace_4_key"/>
<property name="Right" type="string" value="right_key"/>
<property name="Down" type="string" value="down_key"/>
<property name="&lt;Primary&gt;F3" type="string" value="workspace_3_key"/>
<property name="&lt;Shift&gt;&lt;Alt&gt;Page_Down" type="string" value="lower_window_key"/>
<property name="&lt;Primary&gt;F9" type="string" value="workspace_9_key"/>
<property name="&lt;Alt&gt;Tab" type="string" value="cycle_windows_key"/>
<property name="&lt;Primary&gt;&lt;Shift&gt;&lt;Alt&gt;Right" type="string" value="move_window_right_key"/>
<property name="&lt;Primary&gt;&lt;Alt&gt;Right" type="string" value="right_workspace_key"/>
<property name="&lt;Alt&gt;F6" type="string" value="stick_window_key"/>
<property name="&lt;Primary&gt;&lt;Alt&gt;KP_5" type="string" value="move_window_workspace_5_key"/>
<property name="&lt;Primary&gt;F11" type="string" value="workspace_11_key"/>
<property name="&lt;Alt&gt;F10" type="string" value="maximize_window_key"/>
<property name="&lt;Alt&gt;Delete" type="string" value="del_workspace_key"/>
<property name="&lt;Super&gt;Tab" type="string" value="switch_window_key"/>
<property name="&lt;Primary&gt;&lt;Alt&gt;d" type="string" value="show_desktop_key"/>
<property name="&lt;Primary&gt;F4" type="string" value="workspace_4_key"/>
<property name="&lt;Super&gt;KP_Page_Up" type="string" value="tile_up_right_key"/>
<property name="&lt;Alt&gt;F7" type="string" value="move_window_key"/>
<property name="Up" type="string" value="up_key"/>
<property name="&lt;Primary&gt;&lt;Alt&gt;KP_6" type="string" value="move_window_workspace_6_key"/>
<property name="&lt;Alt&gt;F11" type="string" value="fullscreen_key"/>
<property name="&lt;Alt&gt;space" type="string" value="popup_menu_key"/>
<property name="&lt;Super&gt;KP_Home" type="string" value="tile_up_left_key"/>
<property name="Escape" type="string" value="cancel_key"/>
<property name="&lt;Primary&gt;&lt;Alt&gt;KP_1" type="string" value="move_window_workspace_1_key"/>
<property name="&lt;Super&gt;KP_Next" type="string" value="tile_down_right_key"/>
<property name="&lt;Super&gt;KP_Left" type="string" value="tile_left_key"/>
<property name="&lt;Shift&gt;&lt;Alt&gt;Page_Up" type="string" value="raise_window_key"/>
<property name="&lt;Primary&gt;&lt;Alt&gt;Home" type="string" value="move_window_prev_workspace_key"/>
<property name="&lt;Alt&gt;&lt;Shift&gt;Tab" type="string" value="cycle_reverse_windows_key"/>
<property name="&lt;Primary&gt;&lt;Alt&gt;Left" type="string" value="left_workspace_key"/>
<property name="&lt;Alt&gt;F12" type="string" value="above_key"/>
<property name="&lt;Primary&gt;&lt;Shift&gt;&lt;Alt&gt;Up" type="string" value="move_window_up_key"/>
<property name="&lt;Primary&gt;F5" type="string" value="workspace_5_key"/>
<property name="&lt;Alt&gt;F8" type="string" value="resize_window_key"/>
<property name="&lt;Primary&gt;&lt;Alt&gt;KP_7" type="string" value="move_window_workspace_7_key"/>
<property name="&lt;Primary&gt;&lt;Alt&gt;KP_2" type="string" value="move_window_workspace_2_key"/>
<property name="&lt;Super&gt;KP_End" type="string" value="tile_down_left_key"/>
<property name="&lt;Primary&gt;&lt;Alt&gt;Up" type="string" value="up_workspace_key"/>
<property name="&lt;Alt&gt;F9" type="string" value="hide_window_key"/>
<property name="&lt;Primary&gt;F7" type="string" value="workspace_7_key"/>
<property name="&lt;Primary&gt;F10" type="string" value="workspace_10_key"/>
<property name="Left" type="string" value="left_key"/>
<property name="&lt;Primary&gt;&lt;Alt&gt;KP_8" type="string" value="move_window_workspace_8_key"/>
<property name="&lt;Alt&gt;Insert" type="string" value="add_workspace_key"/>
<property name="&lt;Primary&gt;F1" type="string" value="workspace_1_key"/>
<property name="override" type="bool" value="true"/>
</property>
</property>
<property name="providers" type="array">
<value type="string" value="xfwm4"/>
<value type="string" value="commands"/>
</property>
</channel>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<channel name="xfce4-mime-settings" version="1.0">
<property name="last" type="empty">
<property name="window-width" type="int" value="550"/>
<property name="window-height" type="int" value="400"/>
<property name="mime-width" type="int" value="300"/>
<property name="status-width" type="int" value="75"/>
<property name="default-width" type="int" value="133"/>
</property>
</channel>

View File

@ -0,0 +1,108 @@
<?xml version="1.0" encoding="UTF-8"?>
<channel name="xfce4-panel" version="1.0">
<property name="configver" type="int" value="2"/>
<property name="panels" type="array">
<value type="int" value="1"/>
<value type="int" value="2"/>
<property name="dark-mode" type="bool" value="true"/>
<property name="panel-1" type="empty">
<property name="position" type="string" value="p=6;x=0;y=0"/>
<property name="length" type="uint" value="100"/>
<property name="position-locked" type="bool" value="true"/>
<property name="icon-size" type="uint" value="16"/>
<property name="size" type="uint" value="26"/>
<property name="plugin-ids" type="array">
<value type="int" value="1"/>
<value type="int" value="2"/>
<value type="int" value="3"/>
<value type="int" value="4"/>
<value type="int" value="5"/>
<value type="int" value="6"/>
<value type="int" value="8"/>
<value type="int" value="11"/>
<value type="int" value="12"/>
<value type="int" value="13"/>
<value type="int" value="14"/>
</property>
</property>
<property name="panel-2" type="empty">
<property name="autohide-behavior" type="uint" value="1"/>
<property name="position" type="string" value="p=10;x=0;y=0"/>
<property name="position-locked" type="bool" value="true"/>
<property name="size" type="uint" value="48"/>
<property name="plugin-ids" type="array">
<value type="int" value="15"/>
<value type="int" value="16"/>
<value type="int" value="17"/>
<value type="int" value="18"/>
<value type="int" value="19"/>
<value type="int" value="20"/>
<value type="int" value="21"/>
<value type="int" value="22"/>
</property>
</property>
</property>
<property name="plugins" type="empty">
<property name="plugin-1" type="string" value="applicationsmenu"/>
<property name="plugin-2" type="string" value="tasklist">
<property name="grouping" type="uint" value="1"/>
</property>
<property name="plugin-3" type="string" value="separator">
<property name="expand" type="bool" value="true"/>
<property name="style" type="uint" value="0"/>
</property>
<property name="plugin-4" type="string" value="pager"/>
<property name="plugin-5" type="string" value="separator">
<property name="style" type="uint" value="0"/>
</property>
<property name="plugin-6" type="string" value="systray">
<property name="square-icons" type="bool" value="true"/>
</property>
<property name="plugin-8" type="string" value="pulseaudio">
<property name="enable-keyboard-shortcuts" type="bool" value="true"/>
<property name="show-notifications" type="bool" value="true"/>
<property name="mpris-players" type="string" value="firefox.instance_1_35"/>
</property>
<property name="plugin-9" type="string" value="power-manager-plugin"/>
<property name="plugin-10" type="string" value="notification-plugin"/>
<property name="plugin-11" type="string" value="separator">
<property name="style" type="uint" value="0"/>
</property>
<property name="plugin-12" type="string" value="clock"/>
<property name="plugin-13" type="string" value="separator">
<property name="style" type="uint" value="0"/>
</property>
<property name="plugin-14" type="string" value="actions"/>
<property name="plugin-15" type="string" value="showdesktop"/>
<property name="plugin-16" type="string" value="separator"/>
<property name="plugin-17" type="string" value="launcher">
<property name="items" type="array">
<value type="string" value="17117663751.desktop"/>
<value type="string" value="17089428881.desktop"/>
</property>
</property>
<property name="plugin-18" type="string" value="launcher">
<property name="items" type="array">
<value type="string" value="17117663752.desktop"/>
<value type="string" value="17089428882.desktop"/>
</property>
</property>
<property name="plugin-19" type="string" value="launcher">
<property name="items" type="array">
<value type="string" value="17117663753.desktop"/>
<value type="string" value="17089428883.desktop"/>
</property>
</property>
<property name="plugin-20" type="string" value="launcher">
<property name="items" type="array">
<value type="string" value="17117663754.desktop"/>
<value type="string" value="17089428884.desktop"/>
</property>
</property>
<property name="plugin-21" type="string" value="separator"/>
<property name="plugin-22" type="string" value="directorymenu">
<property name="base-directory" type="string" value="/home/neko"/>
</property>
</property>
</channel>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<channel name="xfce4-settings-manager" version="1.0">
<property name="last" type="empty">
<property name="window-width" type="int" value="640"/>
<property name="window-height" type="int" value="500"/>
</property>
</channel>

View File

@ -0,0 +1,91 @@
<?xml version="1.0" encoding="UTF-8"?>
<channel name="xfwm4" version="1.0">
<property name="general" type="empty">
<property name="activate_action" type="string" value="bring"/>
<property name="borderless_maximize" type="bool" value="true"/>
<property name="box_move" type="bool" value="false"/>
<property name="box_resize" type="bool" value="false"/>
<property name="button_layout" type="string" value="O|SHMC"/>
<property name="button_offset" type="int" value="0"/>
<property name="button_spacing" type="int" value="0"/>
<property name="click_to_focus" type="bool" value="true"/>
<property name="cycle_apps_only" type="bool" value="false"/>
<property name="cycle_draw_frame" type="bool" value="true"/>
<property name="cycle_raise" type="bool" value="false"/>
<property name="cycle_hidden" type="bool" value="true"/>
<property name="cycle_minimum" type="bool" value="true"/>
<property name="cycle_minimized" type="bool" value="false"/>
<property name="cycle_preview" type="bool" value="true"/>
<property name="cycle_tabwin_mode" type="int" value="0"/>
<property name="cycle_workspaces" type="bool" value="false"/>
<property name="double_click_action" type="string" value="maximize"/>
<property name="double_click_distance" type="int" value="5"/>
<property name="double_click_time" type="int" value="250"/>
<property name="easy_click" type="string" value="Alt"/>
<property name="focus_delay" type="int" value="250"/>
<property name="focus_hint" type="bool" value="true"/>
<property name="focus_new" type="bool" value="true"/>
<property name="frame_opacity" type="int" value="100"/>
<property name="frame_border_top" type="int" value="0"/>
<property name="full_width_title" type="bool" value="true"/>
<property name="horiz_scroll_opacity" type="bool" value="false"/>
<property name="inactive_opacity" type="int" value="100"/>
<property name="maximized_offset" type="int" value="0"/>
<property name="mousewheel_rollup" type="bool" value="true"/>
<property name="move_opacity" type="int" value="100"/>
<property name="placement_mode" type="string" value="center"/>
<property name="placement_ratio" type="int" value="20"/>
<property name="popup_opacity" type="int" value="100"/>
<property name="prevent_focus_stealing" type="bool" value="false"/>
<property name="raise_delay" type="int" value="250"/>
<property name="raise_on_click" type="bool" value="true"/>
<property name="raise_on_focus" type="bool" value="false"/>
<property name="raise_with_any_button" type="bool" value="true"/>
<property name="repeat_urgent_blink" type="bool" value="false"/>
<property name="resize_opacity" type="int" value="100"/>
<property name="scroll_workspaces" type="bool" value="true"/>
<property name="shadow_delta_height" type="int" value="0"/>
<property name="shadow_delta_width" type="int" value="0"/>
<property name="shadow_delta_x" type="int" value="0"/>
<property name="shadow_delta_y" type="int" value="-3"/>
<property name="shadow_opacity" type="int" value="50"/>
<property name="show_app_icon" type="bool" value="false"/>
<property name="show_dock_shadow" type="bool" value="true"/>
<property name="show_frame_shadow" type="bool" value="true"/>
<property name="show_popup_shadow" type="bool" value="false"/>
<property name="snap_resist" type="bool" value="false"/>
<property name="snap_to_border" type="bool" value="true"/>
<property name="snap_to_windows" type="bool" value="false"/>
<property name="snap_width" type="int" value="10"/>
<property name="vblank_mode" type="string" value="auto"/>
<property name="theme" type="string" value="Default"/>
<property name="tile_on_move" type="bool" value="true"/>
<property name="title_alignment" type="string" value="center"/>
<property name="title_font" type="string" value="Sans Bold 9"/>
<property name="title_horizontal_offset" type="int" value="0"/>
<property name="titleless_maximize" type="bool" value="false"/>
<property name="title_shadow_active" type="string" value="false"/>
<property name="title_shadow_inactive" type="string" value="false"/>
<property name="title_vertical_offset_active" type="int" value="0"/>
<property name="title_vertical_offset_inactive" type="int" value="0"/>
<property name="toggle_workspaces" type="bool" value="false"/>
<property name="unredirect_overlays" type="bool" value="true"/>
<property name="urgent_blink" type="bool" value="false"/>
<property name="use_compositing" type="bool" value="true"/>
<property name="workspace_count" type="int" value="4"/>
<property name="wrap_cycle" type="bool" value="true"/>
<property name="wrap_layout" type="bool" value="true"/>
<property name="wrap_resistance" type="int" value="10"/>
<property name="wrap_windows" type="bool" value="true"/>
<property name="wrap_workspaces" type="bool" value="false"/>
<property name="zoom_desktop" type="bool" value="true"/>
<property name="zoom_pointer" type="bool" value="true"/>
<property name="workspace_names" type="array">
<value type="string" value="Workspace 1"/>
<value type="string" value="Workspace 2"/>
<value type="string" value="Workspace 3"/>
<value type="string" value="Workspace 4"/>
</property>
</property>
</channel>

View File

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8"?>
<channel name="xsettings" version="1.0">
<property name="Net" type="empty">
<property name="ThemeName" type="string" value="Catppuccin-Mocha-Standard-Blue-Dark"/>
<property name="IconThemeName" type="string" value="Adwaita"/>
<property name="DoubleClickTime" type="empty"/>
<property name="DoubleClickDistance" type="empty"/>
<property name="DndDragThreshold" type="empty"/>
<property name="CursorBlink" type="empty"/>
<property name="CursorBlinkTime" type="empty"/>
<property name="SoundThemeName" type="empty"/>
<property name="EnableEventSounds" type="empty"/>
<property name="EnableInputFeedbackSounds" type="empty"/>
</property>
<property name="Xft" type="empty">
<property name="DPI" type="empty"/>
<property name="Antialias" type="empty"/>
<property name="Hinting" type="empty"/>
<property name="HintStyle" type="empty"/>
<property name="RGBA" type="empty"/>
</property>
<property name="Gtk" type="empty">
<property name="CanChangeAccels" type="empty"/>
<property name="ColorPalette" type="empty"/>
<property name="FontName" type="empty"/>
<property name="MonospaceFontName" type="empty"/>
<property name="IconSizes" type="empty"/>
<property name="KeyThemeName" type="empty"/>
<property name="ToolbarStyle" type="empty"/>
<property name="ToolbarIconSize" type="empty"/>
<property name="MenuImages" type="empty"/>
<property name="ButtonImages" type="empty"/>
<property name="MenuBarAccel" type="empty"/>
<property name="CursorThemeName" type="empty"/>
<property name="CursorThemeSize" type="empty"/>
<property name="DecorationLayout" type="empty"/>
<property name="DialogsUseHeader" type="empty"/>
<property name="TitlebarMiddleClick" type="empty"/>
</property>
<property name="Gdk" type="empty">
<property name="WindowScalingFactor" type="empty"/>
</property>
</channel>

View File

@ -0,0 +1,11 @@
[Desktop Entry]
NoDisplay=true
Version=1.0
Encoding=UTF-8
Type=X-XFCE-Helper
X-XFCE-Category=WebBrowser
X-XFCE-CommandsWithParameter=librewolf "%s"
Icon=librewolf
Name=librewolf
X-XFCE-Commands=librewolf

View File

@ -1 +1,3 @@
hello from the other side :3 <3
meow - starlight