├── .gitignore ├── fonts ├── Netflix Sans Bold.otf ├── Netflix Sans Light.otf ├── Netflix Sans Medium.otf └── fluent-system-icons.ttf ├── .gitattributes ├── script-opts ├── gif.conf ├── thumbfast.conf └── modernz.conf ├── LICENSE ├── scripts ├── copy-time.lua ├── cycle-commands.lua ├── cycle-profile.lua ├── sponsorblock-minimal.lua ├── seek-to.lua ├── mpv-gif.lua ├── autoload.lua ├── audio_visualizer.lua ├── thumbfast.lua └── playlistmanager.lua ├── input.conf ├── mpv.conf └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Shaders cache directory 2 | shaders/cache 3 | -------------------------------------------------------------------------------- /fonts/Netflix Sans Bold.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noelsimbolon/mpv-config/HEAD/fonts/Netflix Sans Bold.otf -------------------------------------------------------------------------------- /fonts/Netflix Sans Light.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noelsimbolon/mpv-config/HEAD/fonts/Netflix Sans Light.otf -------------------------------------------------------------------------------- /fonts/Netflix Sans Medium.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noelsimbolon/mpv-config/HEAD/fonts/Netflix Sans Medium.otf -------------------------------------------------------------------------------- /fonts/fluent-system-icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noelsimbolon/mpv-config/HEAD/fonts/fluent-system-icons.ttf -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Treats all of the paths/files below as vendored 2 | # and therefore are not included in the language 3 | # statistics for the repository 4 | # shaders/*.glsl linguist-vendored 5 | # shaders/*.hook linguist-vendored 6 | # scripts/* linguist-vendored -------------------------------------------------------------------------------- /script-opts/gif.conf: -------------------------------------------------------------------------------- 1 | # ========== CONFIGURATION FILE FOR MPV-GIF.LUA ========== 2 | 3 | # Sets the output directory 4 | dir= 5 | 6 | # Sets the resolution of the output GIFs 7 | rez=1920 8 | 9 | # Sets the framerate of the output gifs. Default is 15. Don't go too overboard or the filesize will balloon 10 | fps=23.976 11 | -------------------------------------------------------------------------------- /script-opts/thumbfast.conf: -------------------------------------------------------------------------------- 1 | # This is configuration file for thumbfast.lua 2 | # The default options are listed in thumbfast.lua 3 | 4 | # Maximum thumbnail size in pixels (scaled down to fit) 5 | # Values are scaled when hidpi is enabled 6 | max_height=250 7 | max_width=250 8 | 9 | # Spawn thumbnailer on file load for faster initial thumbnails 10 | spawn_first=yes 11 | 12 | # Enable on network playback 13 | # This allows thumbnailing in videos played over network, e.g. YouTube 14 | network=yes 15 | 16 | # Enable hardware decoding 17 | hwdec=yes 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 noelsimbolon 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /scripts/copy-time.lua: -------------------------------------------------------------------------------- 1 | -- copy-time (Windows version) 2 | 3 | -- Copies current timecode in HH:MM:SS.MS format to clipboard 4 | 5 | ------------------------------------------------------------------------------- 6 | -- Script adapted by Alex Rogers (https://github.com/linguisticmind) 7 | -- Modified from https://github.com/Arieleg/mpv-copyTime 8 | -- Released under GNU GPL 3.0 9 | 10 | require "mp" 11 | 12 | function set_clipboard (text) 13 | local echo 14 | if text ~= "" then 15 | for i = 1, 2 do text = text:gsub("[%^&\\<>|]", "^%0") end 16 | echo = "(echo " .. text:gsub("\n", " & echo ") .. ")" 17 | else 18 | echo = "echo:" 19 | end 20 | mp.commandv("run", "cmd.exe", "/d", "/c", echo .. " | clip") 21 | end 22 | 23 | function copy_time() 24 | local time_pos = mp.get_property_number("time-pos") 25 | local time_in_seconds = time_pos 26 | local time_seg = time_pos % 60 27 | time_pos = time_pos - time_seg 28 | local time_hours = math.floor(time_pos / 3600) 29 | time_pos = time_pos - (time_hours * 3600) 30 | local time_minutes = time_pos/60 31 | time_seg,time_ms=string.format("%.03f", time_seg):match"([^.]*).(.*)" 32 | time = string.format("%02d:%02d:%02d.%s", time_hours, time_minutes, time_seg, time_ms) 33 | set_clipboard(time) 34 | mp.osd_message(string.format("Copied to clipboard: %s", time)) 35 | end 36 | 37 | -- the keybinding here is set to nil on purpose 'cause I modified the keybinding (in input.conf) 38 | mp.add_key_binding(nil, "copy-time", copy_time) 39 | -------------------------------------------------------------------------------- /scripts/cycle-commands.lua: -------------------------------------------------------------------------------- 1 | --[=====[ 2 | script to cycle commands with a keybind, accomplished through script messages 3 | available at: https://github.com/CogentRedTester/mpv-scripts 4 | 5 | syntax: 6 | script-message cycle-commands "command1" "command2" "command3" 7 | 8 | The syntax of each command is identical to the standard input.conf syntax, but each command must be within 9 | a pair of double quotes. 10 | 11 | Commands with mutiword arguments require you to send double quotes just like normal command syntax, however, 12 | you will need to escape the quotes with a backslash so that they are sent as part of the string. 13 | Semicolons also work exactly like they do normally, so you can easily send multiple commands each cycle. 14 | 15 | Here is an example of a standard input.conf entry: 16 | 17 | script-message cycle-commands "show-text one 1000 ; print-text two" "show-text \"three four\"" 18 | 19 | This would, on keypress one, print 'one' to the OSD for 1 second and 'two' to the console, 20 | and on keypress two 'three four' would be printed to the OSD. 21 | Notice how the quotation marks around 'three four' are escaped using backslashes. 22 | All other syntax details should be exactly the same as usual input commands. 23 | 24 | There are no limits to the number of commands, and the script message can be used as often as one wants, 25 | the script stores the current iteration for each unique cycle command, so there should be no overlap 26 | unless one binds the exact same command string (including spacing) 27 | ]=====]-- 28 | 29 | local mp = require 'mp' 30 | local msg = require 'mp.msg' 31 | 32 | --keeps track of the current position for a specific cycle 33 | local iterators = {} 34 | 35 | --main function to identify and run the cycles 36 | local function main(...) 37 | local commands = {...} 38 | 39 | --to identify the specific cycle we'll concatenate all the strings together to use as our table key 40 | local str = table.concat(commands, " | ") 41 | msg.trace('recieved:', str) 42 | 43 | if iterators[str] == nil then 44 | msg.debug('unknown cycle, creating iterator') 45 | iterators[str] = 1 46 | else 47 | iterators[str] = iterators[str] + 1 48 | if iterators[str] > #commands then iterators[str] = 1 end 49 | end 50 | 51 | --mp.command should run the commands exactly as if they were entered in input.conf. 52 | --This should provide universal support for all input.conf command syntax 53 | local cmd = commands[ iterators[str] ] 54 | msg.verbose('sending command:', cmd) 55 | mp.command(cmd) 56 | end 57 | 58 | mp.register_script_message('cycle-commands', main) 59 | -------------------------------------------------------------------------------- /input.conf: -------------------------------------------------------------------------------- 1 | # Check the following link for mpv's default keybindings: 2 | # https://github.com/mpv-player/mpv/blob/master/etc/input.conf 3 | 4 | # ========== CUSTOM KEYBINDINGS ========== 5 | 6 | # GENERAL 7 | k cycle ontop 8 | 9 | # VIDEO 10 | d cycle deband 11 | D cycle deinterlace 12 | n cycle video-unscaled 13 | C cycle-values video-aspect-override "16:9" "4:3" "2.35:1" "-1" # cycle the video aspect ratio ("-1" is the container aspect) 14 | 15 | # AUDIO 16 | a cycle audio 17 | A cycle audio down 18 | WHEEL_UP add volume 2 19 | WHEEL_DOWN add volume -2 20 | UP add volume 2 21 | DOWN add volume -2 22 | x add audio-delay -0.05 23 | X add audio-delay +0.05 24 | 25 | # SUBTITLES 26 | Shift+g add sub-scale +0.05 # increase the subtitle font size 27 | Shift+f add sub-scale -0.05 # decrease the subtitle font size 28 | E add sub-gauss +0.1 # https://mpv.io/manual/stable/#options-sub-gauss 29 | R add sub-gauss -0.1 30 | z add sub-delay -0.05 # shift subtitles 50 ms earlier 31 | Z add sub-delay +0.05 # shift subtitles 50 ms later 32 | u cycle sub-gray # https://mpv.io/manual/stable/#options-sub-gray 33 | U cycle blend-subtitles # https://mpv.io/manual/stable/#options-blend-subtitles 34 | p cycle sub-fix-timing # https://mpv.io/manual/stable/#options-sub-fix-timing 35 | g sub-reload # https://mpv.io/manual/stable/#command-interface-sub-reload 36 | l cycle-values sub-ass-override "yes" "force" "no" # https://mpv.io/manual/stable/#options-sub-ass-override 37 | 38 | # SCRIPT KEYBINDINGS 39 | ~ script-message cycle-commands "apply-profile HDR ; show-text 'HDR profile applied'" "apply-profile HDR restore ; show-text 'HDR profile restored'" 40 | # ~ script-message cycle-commands "apply-profile Clip ; show-text 'Clip profile applied'" "apply-profile Mobius ; show-text 'Mobius profile applied'" "apply-profile Reinhard ; show-text 'Reinhard profile applied'" "apply-profile Hable ; show-text 'Hable profile applied'" "apply-profile bt.2390 ; show-text 'bt.2390 profile applied'" "apply-profile Gamma ; show-text 'Gamma profile applied'" "apply-profile Linear ; show-text 'Linear profile applied'" 41 | c script-binding cycle-visualizer # cycle audio visualizer (audio-visualizer.lua) 42 | b script-binding set_gif_start # set the start timestamp for to make GIF (mpv-gif.lua) 43 | B script-binding set_gif_end # set the stop timestamp for to make GIF (mpv-gif.lua) 44 | ctrl+b script-binding make_gif # make the GIF using start and stop timestamps (mpv-gif.lua) 45 | ctrl+B script-binding make_gif_with_subtitles # make the GIF using start and stop timestamps with subtitles (doesn't seem to work) (mpv-gif.lua) 46 | ctrl+c script-binding copy-time # copy current timestamp to clipboard in HH:MM:SS.MS format (copy-time.lua) 47 | ctrl+S script-binding toggle-seeker # toggle keyboard input to seek to inputted timestamp (seek-to.lua) 48 | ctrl+v script-binding paste-timestamp # automatically seek to pasted timestamp from clipboard (seek-to.lua) 49 | alt+b script-binding sponsorblock # toggle sponsorblock on/off (sponsorblock-minimal.lua) 50 | -------------------------------------------------------------------------------- /scripts/cycle-profile.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | script to cycle profiles with a keybind, accomplished through script messages 3 | available at: https://github.com/CogentRedTester/mpv-scripts 4 | 5 | syntax: 6 | script-message cycle-profiles "profile1;profile2;profile3" 7 | 8 | You must use semicolons to separate the profiles, do not include any spaces that are not part of the profile name. 9 | The script will print the profile description to the screen when switching, if there is no profile description, then it just prints the name 10 | ]]-- 11 | 12 | --change this to change what character separates the profile names 13 | seperator = ";" 14 | 15 | msg = require 'mp.msg' 16 | 17 | --splits the profiles string into an array of profile names 18 | --function taken from: https://stackoverflow.com/questions/1426954/split-string-in-lua/7615129#7615129 19 | function mysplit (inputstr, sep) 20 | if sep == nil then 21 | sep = "%s" 22 | end 23 | local t={} 24 | for str in string.gmatch(inputstr, "([^"..sep.."]+)") do 25 | table.insert(t, str) 26 | end 27 | return t 28 | end 29 | 30 | --table of all available profiles and options 31 | profileList = mp.get_property_native('profile-list') 32 | 33 | --keeps track of current profile for every unique cycle 34 | iterator = {} 35 | 36 | --stores descriptions for profiles 37 | --once requested a description is stored here so it does not need to be found again 38 | profilesDescs = {} 39 | 40 | --if trying to cycle to an unknown profile this function is run to find a description to print 41 | function findDesc(profile) 42 | msg.verbose('unknown profile ' .. profile .. ', searching for description') 43 | 44 | for i = 1, #profileList, 1 do 45 | if profileList[i]['name'] == profile then 46 | msg.verbose('profile found') 47 | local desc = profileList[i]['profile-desc'] 48 | 49 | if desc ~= nil then 50 | msg.verbose('description found') 51 | profilesDescs[profile] = desc 52 | else 53 | msg.verbose('no description, will use name') 54 | profilesDescs[profile] = profile 55 | end 56 | return 57 | end 58 | end 59 | 60 | msg.verbose('profile not found') 61 | profilesDescs[profile] = "no profile '" .. profile .. "'" 62 | end 63 | 64 | --prints the profile description to the OSD 65 | --if the profile has not been requested before during the session then it runs findDesc() 66 | function printProfileDesc(profile) 67 | local desc = profilesDescs[profile] 68 | if desc == nil then 69 | findDesc(profile) 70 | desc = profilesDescs[profile] 71 | end 72 | 73 | msg.verbose('profile description: ' .. desc) 74 | mp.osd_message(desc) 75 | end 76 | 77 | function main(profileStr) 78 | --if there is not already an iterator for this cycle then it creates one 79 | if iterator[profileStr] == nil then 80 | msg.verbose('unknown cycle, creating new iterator') 81 | iterator[profileStr] = 1 82 | end 83 | local i = iterator[profileStr] 84 | 85 | --converts the string into an array of profile names 86 | local profiles = mysplit(profileStr, seperator) 87 | msg.verbose('cycling ' .. tostring(profiles)) 88 | msg.verbose("number of profiles: " .. tostring(#profiles)) 89 | 90 | --sends the command to apply the profile 91 | msg.info("applying profile " .. profiles[i]) 92 | mp.commandv('apply-profile', profiles[i]) 93 | 94 | --prints the profile description to the OSD 95 | printProfileDesc(profiles[i]) 96 | 97 | --moves the iterator 98 | iterator[profileStr] = iterator[profileStr] + 1 99 | if iterator[profileStr] > #profiles then 100 | msg.verbose('reached end of profiles, wrapping back to start') 101 | iterator[profileStr] = 1 102 | end 103 | end 104 | 105 | mp.register_script_message('cycle-profiles', main) 106 | -------------------------------------------------------------------------------- /scripts/sponsorblock-minimal.lua: -------------------------------------------------------------------------------- 1 | -- sponsorblock-minimal.lua 2 | -- source: https://codeberg.org/jouni/mpv_sponsorblock_minimal 3 | -- 4 | -- This script skips sponsored segments of YouTube videos 5 | -- using data from https://github.com/ajayyy/SponsorBlock 6 | 7 | local opt = require 'mp.options' 8 | local utils = require 'mp.utils' 9 | 10 | local ON = false 11 | local ranges = nil 12 | 13 | local options = { 14 | server = "https://sponsor.ajay.app/api/skipSegments", 15 | 16 | -- Categories to fetch and skip 17 | categories = '"sponsor"', 18 | 19 | -- Set this to "true" to use sha256HashPrefix instead of videoID 20 | hash = "" 21 | } 22 | 23 | opt.read_options(options) 24 | 25 | function get_ranges(youtube_id, url) 26 | local luacurl_available, cURL = pcall(require,'cURL') 27 | 28 | local res = nil 29 | if not(luacurl_available) then -- if Lua-cURL is not available on this system 30 | local sponsors = mp.command_native{ 31 | name = "subprocess", 32 | capture_stdout = true, 33 | playback_only = false, 34 | args = {"curl", "-L", "-s", "-g", url} 35 | } 36 | res = sponsors.stdout 37 | else -- otherwise use Lua-cURL (binding to libcurl) 38 | local buf={} 39 | local c = cURL.easy_init() 40 | c:setopt_followlocation(1) 41 | c:setopt_url(url) 42 | c:setopt_writefunction(function(chunk) table.insert(buf,chunk); return true; end) 43 | c:perform() 44 | res = table.concat(buf) 45 | end 46 | 47 | if res then 48 | local json = utils.parse_json(res) 49 | if type(json) == "table" then 50 | if options.hash == "true" then 51 | for _, i in pairs(json) do 52 | if i.videoID == youtube_id then 53 | return i.segments 54 | end 55 | end 56 | else 57 | return json 58 | end 59 | end 60 | end 61 | 62 | return nil 63 | end 64 | 65 | function skip_ads(name,pos) 66 | if pos then 67 | for _, i in pairs(ranges) do 68 | v = i.segment[2] 69 | if i.segment[1] <= pos and v > pos then 70 | --this message may sometimes be wrong 71 | --it only seems to be a visual thing though 72 | mp.osd_message(("[sponsorblock] skipping forward %ds"):format(math.floor(v-mp.get_property("time-pos")))) 73 | --need to do the +0.01 otherwise mpv will start spamming skip sometimes 74 | --example: https://www.youtube.com/watch?v=4ypMJzeNooo 75 | mp.set_property("time-pos",v+0.01) 76 | return 77 | end 78 | end 79 | end 80 | end 81 | 82 | function file_loaded() 83 | local video_path = mp.get_property("path", "") 84 | local video_referer = string.match(mp.get_property("http-header-fields", ""), "Referer:([^,]+)") or "" 85 | 86 | local urls = { 87 | "ytdl://youtu%.be/([%w-_]+).*", 88 | "ytdl://w?w?w?%.?youtube%.com/v/([%w-_]+).*", 89 | "https?://youtu%.be/([%w-_]+).*", 90 | "https?://w?w?w?%.?youtube%.com/v/([%w-_]+).*", 91 | "/watch.*[?&]v=([%w-_]+).*", 92 | "/embed/([%w-_]+).*", 93 | "^ytdl://([%w-_]+)$", 94 | "-([%w-_]+)%." 95 | } 96 | local youtube_id = nil 97 | local purl = mp.get_property("metadata/by-key/PURL", "") 98 | for i,url in ipairs(urls) do 99 | youtube_id = youtube_id or string.match(video_path, url) or string.match(video_referer, url) or string.match(purl, url) 100 | if youtube_id then break end 101 | end 102 | 103 | if not youtube_id or string.len(youtube_id) < 11 then return end 104 | youtube_id = string.sub(youtube_id, 1, 11) 105 | 106 | local url = "" 107 | if options.hash == "true" then 108 | local sha = mp.command_native{ 109 | name = "subprocess", 110 | capture_stdout = true, 111 | args = {"sha256sum"}, 112 | stdin_data = youtube_id 113 | } 114 | url = ("%s/%s?categories=[%s]"):format(options.server, string.sub(sha.stdout, 0, 4), options.categories) 115 | else 116 | url = ("%s?videoID=%s&categories=[%s]"):format(options.server, youtube_id, options.categories) 117 | end 118 | 119 | ranges = get_ranges(youtube_id, url) 120 | if ranges then 121 | ON = true 122 | mp.add_key_binding("b","sponsorblock",toggle) 123 | mp.observe_property("time-pos", "native", skip_ads) 124 | end 125 | end 126 | 127 | function end_file() 128 | if not ON then return end 129 | mp.unobserve_property(skip_ads) 130 | ranges = nil 131 | ON = false 132 | end 133 | 134 | function toggle() 135 | if ON then 136 | mp.unobserve_property(skip_ads) 137 | mp.osd_message("[sponsorblock] off") 138 | ON = false 139 | else 140 | mp.observe_property("time-pos", "native", skip_ads) 141 | mp.osd_message("[sponsorblock] on") 142 | ON = true 143 | end 144 | end 145 | 146 | mp.register_event("file-loaded", file_loaded) 147 | mp.register_event("end-file", end_file) 148 | -------------------------------------------------------------------------------- /scripts/seek-to.lua: -------------------------------------------------------------------------------- 1 | -- Source: https://github.com/dexeonify/mpv-config/blob/main/scripts/seek-to.lua 2 | -- with modifications 3 | 4 | local assdraw = require 'mp.assdraw' 5 | local utils = require 'mp.utils' 6 | local msg = require 'mp.msg' 7 | local active = false 8 | local cursor_position = 1 9 | local time_scale = {60*60*10, 60*60, 60*10, 60, 10, 1, 0.1, 0.01, 0.001} 10 | 11 | local ass_begin = mp.get_property("osd-ass-cc/0") 12 | local ass_end = mp.get_property("osd-ass-cc/1") 13 | 14 | local history = { {} } 15 | for i = 1, 9 do 16 | history[1][i] = 0 17 | end 18 | local history_position = 1 19 | 20 | -- timer to redraw periodically the message 21 | -- to avoid leaving bindings when the seeker disappears for whatever reason 22 | -- pretty hacky tbh 23 | local timer = nil 24 | local timer_duration = 3 25 | 26 | function show_seeker() 27 | local prepend_char = {'','',':','',':','','.','',''} 28 | local str = '' 29 | for i = 1, 9 do 30 | str = str .. prepend_char[i] 31 | if i == cursor_position then 32 | str = str .. '{\\b1}' .. history[history_position][i] .. '{\\r}' 33 | else 34 | str = str .. history[history_position][i] 35 | end 36 | end 37 | mp.osd_message("Seek to: " .. ass_begin .. str .. ass_end, timer_duration) 38 | end 39 | 40 | function copy_history_to_last() 41 | if history_position ~= #history then 42 | for i = 1, 9 do 43 | history[#history][i] = history[history_position][i] 44 | end 45 | history_position = #history 46 | end 47 | end 48 | 49 | function change_number(i) 50 | -- can't set above 60 minutes or seconds 51 | if (cursor_position == 3 or cursor_position == 5) and i >= 6 then 52 | return 53 | end 54 | if history[history_position][cursor_position] ~= i then 55 | copy_history_to_last() 56 | history[#history][cursor_position] = i 57 | end 58 | shift_cursor(false) 59 | end 60 | 61 | function shift_cursor(left) 62 | if left then 63 | cursor_position = math.max(1, cursor_position - 1) 64 | else 65 | cursor_position = math.min(cursor_position + 1, 9) 66 | end 67 | end 68 | 69 | function current_time_as_sec(time) 70 | local sec = 0 71 | for i = 1, 9 do 72 | sec = sec + time_scale[i] * time[i] 73 | end 74 | return sec 75 | end 76 | 77 | function time_equal(lhs, rhs) 78 | for i = 1, 9 do 79 | if lhs[i] ~= rhs[i] then 80 | return false 81 | end 82 | end 83 | return true 84 | end 85 | 86 | function seek_to() 87 | copy_history_to_last() 88 | mp.commandv("osd-bar", "seek", current_time_as_sec(history[history_position]), "absolute") 89 | --deduplicate consecutive timestamps 90 | if #history == 1 or not time_equal(history[history_position], history[#history - 1]) then 91 | history[#history + 1] = {} 92 | history_position = #history 93 | end 94 | for i = 1, 9 do 95 | history[#history][i] = 0 96 | end 97 | end 98 | 99 | function backspace() 100 | if history[history_position][cursor_position] ~= 0 then 101 | copy_history_to_last() 102 | history[#history][cursor_position] = 0 103 | end 104 | shift_cursor(true) 105 | end 106 | 107 | function history_move(up) 108 | if up then 109 | history_position = math.max(1, history_position - 1) 110 | else 111 | history_position = math.min(history_position + 1, #history) 112 | end 113 | end 114 | 115 | local key_mappings = { 116 | LEFT = function() shift_cursor(true) show_seeker() end, 117 | RIGHT = function() shift_cursor(false) show_seeker() end, 118 | UP = function() history_move(true) show_seeker() end, 119 | DOWN = function() history_move(false) show_seeker() end, 120 | BS = function() backspace() show_seeker() end, 121 | ESC = function() set_inactive() end, 122 | ENTER = function() seek_to() set_inactive() end 123 | } 124 | for i = 0, 9 do 125 | local func = function() change_number(i) show_seeker() end 126 | key_mappings[string.format("KP%d", i)] = func 127 | key_mappings[string.format("%d", i)] = func 128 | end 129 | 130 | function set_active() 131 | if not mp.get_property("seekable") then return end 132 | -- find duration of the video and set cursor position accordingly 133 | local duration = mp.get_property_number("duration") 134 | if duration ~= nil then 135 | for i = 1, 9 do 136 | if duration > time_scale[i] then 137 | cursor_position = i 138 | break 139 | end 140 | end 141 | end 142 | for key, func in pairs(key_mappings) do 143 | mp.add_forced_key_binding(key, "seek-to-"..key, func) 144 | end 145 | show_seeker() 146 | timer = mp.add_periodic_timer(timer_duration, show_seeker) 147 | active = true 148 | end 149 | 150 | function set_inactive() 151 | mp.osd_message("") 152 | for key, _ in pairs(key_mappings) do 153 | mp.remove_key_binding("seek-to-"..key) 154 | end 155 | timer:kill() 156 | active = false 157 | end 158 | 159 | function paste_timestamp() 160 | -- get clipboard data 161 | local clipboard = utils.subprocess({ 162 | args = { "powershell", "-Command", "Get-Clipboard", "-Raw" }, 163 | playback_only = false, 164 | capture_stdout = true, 165 | capture_stderr = true 166 | }) 167 | 168 | -- error handling 169 | if not clipboard.error then 170 | timestamp = clipboard.stdout 171 | else 172 | msg.error("Error getting data from clipboard:") 173 | msg.error(" stderr: " .. clipboard.stderr) 174 | msg.error(" stdout: " .. clipboard.stdout) 175 | return 176 | end 177 | 178 | -- find timestamp from clipboard 179 | match = timestamp:match("%d?%d?:?%d%d:%d%d%.?%d*") 180 | 181 | -- paste and seek to timestamp 182 | if match ~= nil then 183 | mp.osd_message("Timestamp pasted: " .. match) 184 | mp.commandv("osd-bar", "seek", match, "absolute") 185 | else 186 | msg.warn("No pastable timestamp found!") 187 | end 188 | end 189 | 190 | -- keybindings are set in input.conf 191 | mp.add_key_binding(nil, "toggle-seeker", function() if active then set_inactive() else set_active() end end) 192 | mp.add_key_binding(nil, "paste-timestamp", paste_timestamp) 193 | -------------------------------------------------------------------------------- /scripts/mpv-gif.lua: -------------------------------------------------------------------------------- 1 | -- Original by Ruin0x11 2 | -- Ported to Windows by Scheliux, Dragoner7 3 | 4 | -- Create animated GIFs with mpv 5 | -- Requires ffmpeg. 6 | -- Adapted from http://blog.pkh.me/p/21-high-quality-gif-with-ffmpeg.html 7 | -- Usage: "b" to set start frame, "B" to set end frame, "Ctrl+b" to create. 8 | 9 | require 'mp.options' 10 | local msg = require 'mp.msg' 11 | 12 | local options = { 13 | dir = "C:/Program Files/mpv/gifs", 14 | rez = 600, 15 | fps = 15, 16 | } 17 | 18 | read_options(options, "gif") 19 | 20 | 21 | local fps 22 | 23 | -- Check for invalid fps values 24 | -- Can you believe Lua doesn't have a proper ternary operator in the year of our lord 2020? 25 | if options.fps ~= nil and options.fps >= 1 and options.fps < 30 then 26 | fps = options.fps 27 | else 28 | fps = 15 29 | end 30 | 31 | -- Set this to the filters to pass into ffmpeg's -vf option. 32 | -- filters="fps=24,scale=320:-1:flags=spline" 33 | filters=string.format("fps=%s,scale='trunc(ih*dar/2)*2:trunc(ih/2)*2',setsar=1/1,scale=%s:-1:flags=spline", fps, options.rez) --change spline to lanczos depending on preference 34 | 35 | -- Setup output directory 36 | output_directory=string.gsub(options.dir, '\"', '') 37 | 38 | start_time = -1 39 | end_time = -1 40 | palette="%TEMP%palette.png" 41 | 42 | -- The roundabout way has to be used due to a some weird 43 | -- behavior with %TEMP% on the subtitles= parameter in ffmpeg 44 | -- on Windows–it needs to be quadruple backslashed 45 | subs = "C:/Users/%USERNAME%/AppData/Local/Temp/subs.srt" 46 | 47 | function make_gif_with_subtitles() 48 | make_gif_internal(true) 49 | end 50 | 51 | function make_gif() 52 | make_gif_internal(false) 53 | end 54 | 55 | function table_length(t) 56 | local count = 0 57 | for _ in pairs(t) do count = count + 1 end 58 | return count 59 | end 60 | 61 | 62 | function make_gif_internal(burn_subtitles) 63 | local start_time_l = start_time 64 | local end_time_l = end_time 65 | if start_time_l == -1 or end_time_l == -1 or start_time_l >= end_time_l then 66 | mp.osd_message("Invalid start/end time.") 67 | return 68 | end 69 | 70 | mp.osd_message("Creating GIF.") 71 | 72 | -- shell escape 73 | function esc(s) 74 | return string.gsub(s, '"', '"\\""') 75 | end 76 | 77 | function esc_for_sub(s) 78 | s = string.gsub(s, [[\]], [[/]]) 79 | s = string.gsub(s, '"', '"\\""') 80 | s = string.gsub(s, ":", [[\\:]]) 81 | s = string.gsub(s, "'", [[\\']]) 82 | return s 83 | end 84 | 85 | local pathname = mp.get_property("path", "") 86 | local trim_filters = esc(filters) 87 | 88 | local position = start_time_l 89 | local duration = end_time_l - start_time_l 90 | 91 | if burn_subtitles then 92 | -- Determine currently active sub track 93 | 94 | local i = 0 95 | local tracks_count = mp.get_property_number("track-list/count") 96 | local subs_array = {} 97 | 98 | -- check for subtitle tracks 99 | 100 | while i < tracks_count do 101 | local type = mp.get_property(string.format("track-list/%d/type", i)) 102 | local selected = mp.get_property(string.format("track-list/%d/selected", i)) 103 | 104 | -- if it's a sub track, save it 105 | 106 | if type == "sub" then 107 | local length = table_length(subs_array) 108 | subs_array[length] = selected == "yes" 109 | end 110 | i = i + 1 111 | end 112 | 113 | if table_length(subs_array) > 0 then 114 | 115 | local correct_track = 0 116 | 117 | -- iterate through saved subtitle tracks until the correct one is found 118 | 119 | for index, is_selected in pairs(subs_array) do 120 | if (is_selected) then 121 | correct_track = index 122 | end 123 | end 124 | 125 | trim_filters = trim_filters .. string.format(",subtitles=%s:si=%s", esc_for_sub(pathname), correct_track) 126 | 127 | end 128 | 129 | end 130 | 131 | 132 | -- first, create the palette 133 | args = string.format('ffmpeg -v warning -ss %s -t %s -i "%s" -vf "%s,palettegen" -y "%s"', position, duration, esc(pathname), esc(trim_filters), esc(palette)) 134 | msg.debug(args) 135 | os.execute(args) 136 | 137 | -- then, make the gif 138 | local filename = mp.get_property("filename/no-ext") 139 | local file_path = output_directory .. "/" .. filename 140 | 141 | -- increment filename 142 | for i=0,999 do 143 | local fn = string.format('%s_%03d.gif',file_path,i) 144 | if not file_exists(fn) then 145 | gifname = fn 146 | break 147 | end 148 | end 149 | if not gifname then 150 | mp.osd_message('No available filenames!') 151 | return 152 | end 153 | 154 | local copyts = "" 155 | 156 | if burn_subtitles then 157 | copyts = "-copyts" 158 | end 159 | 160 | args = string.format('ffmpeg -v warning -ss %s %s -t %s -i "%s" -i "%s" -lavfi "%s [x]; [x][1:v] paletteuse" -y "%s"', position, copyts, duration, esc(pathname), esc(palette), esc(trim_filters), esc(gifname)) 161 | os.execute(args) 162 | 163 | local ok, err, code = os.rename(gifname, gifname) 164 | if ok then 165 | msg.info("GIF created: " .. gifname) 166 | mp.osd_message("GIF created: " .. gifname) 167 | else 168 | mp.osd_message("Error creating file, check CLI for more info.") 169 | end 170 | end 171 | 172 | function set_gif_start() 173 | start_time = mp.get_property_number("time-pos", -1) 174 | mp.osd_message("GIF Start: " .. start_time) 175 | end 176 | 177 | function set_gif_end() 178 | end_time = mp.get_property_number("time-pos", -1) 179 | mp.osd_message("GIF End: " .. end_time) 180 | end 181 | 182 | function file_exists(name) 183 | local f=io.open(name,"r") 184 | if f~=nil then io.close(f) return true else return false end 185 | end 186 | 187 | -- all keybindings here are set to nil on purpose 'cause I modified the keybindings (in input.conf) 188 | mp.add_key_binding(nil, "set_gif_start", set_gif_start) 189 | mp.add_key_binding(nil, "set_gif_end", set_gif_end) 190 | mp.add_key_binding(nil, "make_gif", make_gif) 191 | mp.add_key_binding(nil, "make_gif_with_subtitles", make_gif_with_subtitles) -- making GIFs with subtitles doesn't seem to work 192 | -------------------------------------------------------------------------------- /scripts/autoload.lua: -------------------------------------------------------------------------------- 1 | -- This script automatically loads playlist entries before and after the 2 | -- the currently played file. It does so by scanning the directory a file is 3 | -- located in when starting playback. It sorts the directory entries 4 | -- alphabetically, and adds entries before and after the current file to 5 | -- the internal playlist. (It stops if it would add an already existing 6 | -- playlist entry at the same position - this makes it "stable".) 7 | -- Add at most 5000 * 2 files when starting a file (before + after). 8 | 9 | --[[ 10 | To configure this script use file autoload.conf in directory script-opts (the "script-opts" 11 | directory must be in the mpv configuration directory, typically ~/.config/mpv/). 12 | 13 | Example configuration would be: 14 | 15 | disabled=no 16 | images=no 17 | videos=yes 18 | audio=yes 19 | ignore_hidden=yes 20 | 21 | --]] 22 | 23 | MAXENTRIES = 5000 24 | 25 | local msg = require 'mp.msg' 26 | local options = require 'mp.options' 27 | local utils = require 'mp.utils' 28 | 29 | o = { 30 | disabled = false, 31 | images = true, 32 | videos = true, 33 | audio = true, 34 | ignore_hidden = true 35 | } 36 | options.read_options(o) 37 | 38 | function Set (t) 39 | local set = {} 40 | for _, v in pairs(t) do set[v] = true end 41 | return set 42 | end 43 | 44 | function SetUnion (a,b) 45 | local res = {} 46 | for k in pairs(a) do res[k] = true end 47 | for k in pairs(b) do res[k] = true end 48 | return res 49 | end 50 | 51 | EXTENSIONS_VIDEO = Set { 52 | '3g2', '3gp', 'avi', 'flv', 'm2ts', 'm4v', 'mj2', 'mkv', 'mov', 53 | 'mp4', 'mpeg', 'mpg', 'ogv', 'rmvb', 'webm', 'wmv', 'y4m' 54 | } 55 | 56 | EXTENSIONS_AUDIO = Set { 57 | 'aiff', 'ape', 'au', 'flac', 'm4a', 'mka', 'mp3', 'oga', 'ogg', 58 | 'ogm', 'opus', 'wav', 'wma' 59 | } 60 | 61 | EXTENSIONS_IMAGES = Set { 62 | 'avif', 'bmp', 'gif', 'j2k', 'jp2', 'jpeg', 'jpg', 'jxl', 'png', 63 | 'svg', 'tga', 'tif', 'tiff', 'webp' 64 | } 65 | 66 | EXTENSIONS = Set {} 67 | if o.videos then EXTENSIONS = SetUnion(EXTENSIONS, EXTENSIONS_VIDEO) end 68 | if o.audio then EXTENSIONS = SetUnion(EXTENSIONS, EXTENSIONS_AUDIO) end 69 | if o.images then EXTENSIONS = SetUnion(EXTENSIONS, EXTENSIONS_IMAGES) end 70 | 71 | function add_files_at(index, files) 72 | index = index - 1 73 | local oldcount = mp.get_property_number("playlist-count", 1) 74 | for i = 1, #files do 75 | mp.commandv("loadfile", files[i], "append") 76 | mp.commandv("playlist-move", oldcount + i - 1, index + i - 1) 77 | end 78 | end 79 | 80 | function get_extension(path) 81 | match = string.match(path, "%.([^%.]+)$" ) 82 | if match == nil then 83 | return "nomatch" 84 | else 85 | return match 86 | end 87 | end 88 | 89 | table.filter = function(t, iter) 90 | for i = #t, 1, -1 do 91 | if not iter(t[i]) then 92 | table.remove(t, i) 93 | end 94 | end 95 | end 96 | 97 | -- alphanum sorting for humans in Lua 98 | -- http://notebook.kulchenko.com/algorithms/alphanumeric-natural-sorting-for-humans-in-lua 99 | 100 | function alphanumsort(filenames) 101 | local function padnum(n, d) 102 | return #d > 0 and ("%03d%s%.12f"):format(#n, n, tonumber(d) / (10 ^ #d)) 103 | or ("%03d%s"):format(#n, n) 104 | end 105 | 106 | local tuples = {} 107 | for i, f in ipairs(filenames) do 108 | tuples[i] = {f:lower():gsub("0*(%d+)%.?(%d*)", padnum), f} 109 | end 110 | table.sort(tuples, function(a, b) 111 | return a[1] == b[1] and #b[2] < #a[2] or a[1] < b[1] 112 | end) 113 | for i, tuple in ipairs(tuples) do filenames[i] = tuple[2] end 114 | return filenames 115 | end 116 | 117 | local autoloaded = nil 118 | 119 | function get_playlist_filenames() 120 | local filenames = {} 121 | for n = 0, pl_count - 1, 1 do 122 | local filename = mp.get_property('playlist/'..n..'/filename') 123 | local _, file = utils.split_path(filename) 124 | filenames[file] = true 125 | end 126 | return filenames 127 | end 128 | 129 | function find_and_add_entries() 130 | local path = mp.get_property("path", "") 131 | local dir, filename = utils.split_path(path) 132 | msg.trace(("dir: %s, filename: %s"):format(dir, filename)) 133 | if o.disabled then 134 | msg.verbose("stopping: autoload disabled") 135 | return 136 | elseif #dir == 0 then 137 | msg.verbose("stopping: not a local path") 138 | return 139 | end 140 | 141 | pl_count = mp.get_property_number("playlist-count", 1) 142 | -- check if this is a manually made playlist 143 | if (pl_count > 1 and autoloaded == nil) or 144 | (pl_count == 1 and EXTENSIONS[string.lower(get_extension(filename))] == nil) then 145 | msg.verbose("stopping: manually made playlist") 146 | return 147 | else 148 | autoloaded = true 149 | end 150 | 151 | local pl = mp.get_property_native("playlist", {}) 152 | local pl_current = mp.get_property_number("playlist-pos-1", 1) 153 | msg.trace(("playlist-pos-1: %s, playlist: %s"):format(pl_current, 154 | utils.to_string(pl))) 155 | 156 | local files = utils.readdir(dir, "files") 157 | if files == nil then 158 | msg.verbose("no other files in directory") 159 | return 160 | end 161 | table.filter(files, function (v, k) 162 | -- The current file could be a hidden file, ignoring it doesn't load other 163 | -- files from the current directory. 164 | if (o.ignore_hidden and not (v == filename) and string.match(v, "^%.")) then 165 | return false 166 | end 167 | local ext = get_extension(v) 168 | if ext == nil then 169 | return false 170 | end 171 | return EXTENSIONS[string.lower(ext)] 172 | end) 173 | alphanumsort(files) 174 | 175 | if dir == "." then 176 | dir = "" 177 | end 178 | 179 | -- Find the current pl entry (dir+"/"+filename) in the sorted dir list 180 | local current 181 | for i = 1, #files do 182 | if files[i] == filename then 183 | current = i 184 | break 185 | end 186 | end 187 | if current == nil then 188 | return 189 | end 190 | msg.trace("current file position in files: "..current) 191 | 192 | local append = {[-1] = {}, [1] = {}} 193 | local filenames = get_playlist_filenames() 194 | for direction = -1, 1, 2 do -- 2 iterations, with direction = -1 and +1 195 | for i = 1, MAXENTRIES do 196 | local file = files[current + i * direction] 197 | if file == nil or file[1] == "." then 198 | break 199 | end 200 | 201 | local filepath = dir .. file 202 | -- skip files already in playlist 203 | if filenames[file] then break end 204 | 205 | if direction == -1 then 206 | if pl_current == 1 then -- never add additional entries in the middle 207 | msg.info("Prepending " .. file) 208 | table.insert(append[-1], 1, filepath) 209 | end 210 | else 211 | msg.info("Adding " .. file) 212 | table.insert(append[1], filepath) 213 | end 214 | end 215 | end 216 | 217 | add_files_at(pl_current + 1, append[1]) 218 | add_files_at(pl_current, append[-1]) 219 | end 220 | 221 | mp.register_event("start-file", find_and_add_entries) 222 | -------------------------------------------------------------------------------- /mpv.conf: -------------------------------------------------------------------------------- 1 | # ========== GENERAL ========== 2 | 3 | profile=high-quality # Allows for higher quality playback on mpv 4 | vo=gpu-next # https://mpv.io/manual/stable/#video-output-drivers-gpu 5 | priority=high # Makes PC prioritize MPV for allocating resources (Windows only) 6 | 7 | # gpu-api=vulkan offers better overall performance, but it sometimes breaks playback 8 | # For displaying HDR content (not HDR -> SDR) "d3d11" is recommended (Windows only) 9 | gpu-api=vulkan # https://mpv.io/manual/stable/#options-gpu-api 10 | fullscreen=yes # Start in fullscreen 11 | taskbar-progress=no # Disable playback progress rendering in taskbar 12 | force-seekable=yes # If the player thinks that the media is not seekable, force enable seeking 13 | keep-open=always # Don't close the player after finishing the video, 14 | # and playback will never automatically advance to the next file in the playlist 15 | reset-on-next-file=pause # After playing the next file in the playlist, 16 | # it will automatically play the file instead of a paused state 17 | hwdec=vulkan # https://mpv.io/manual/stable/#options-hwdec 18 | dither-depth=8 # This must be set to match your monitor's bit depth 19 | scale-antiring=0.6 # https://mpv.io/manual/stable/#options-scale-antiring 20 | 21 | 22 | # ========== SCALERS AND SHADERS ========== 23 | 24 | scale=ewa_lanczossharp # Luma upscaler 25 | dscale=mitchell # Luma downscaler 26 | cscale=ewa_lanczossharp # Chroma up&downscaler 27 | gpu-shader-cache-dir='~~/shaders/cache' # https://mpv.io/manual/stable/#options-gpu-shader-cache-dir 28 | 29 | # Only use these if you have high-end hardware 30 | # glsl-shader="~~/shaders/nnedi3-nns128-win8x4.hook" 31 | # glsl-shader="~~/shaders/ArtCNN_C4F32.glsl" 32 | 33 | 34 | # ========== DEBANDING ========== 35 | 36 | # Banding is a visual artifact, visual artifacts should never be in a video. 37 | # Example of banding: https://imgur.com/32d77H0 38 | # Debanding is the process of removing said banding. 39 | # 6 minute explanation of what causes banding: https://www.youtube.com/watch?v=h9j89L8eQQk 40 | 41 | deband=no # Turn on only for videos with banding. (Keybind=d) 42 | deband-iterations=2 # https://mpv.io/manual/stable/#options-deband-iterations 43 | deband-threshold=64 # https://mpv.io/manual/stable/#options-deband-threshold 44 | deband-range=17 # https://mpv.io/manual/stable/#options-deband-range 45 | deband-grain=12 # https://mpv.io/manual/stable/#options-deband-grain 46 | 47 | 48 | # ========== ON SCREEN DISPLAY AND ON SCREEN CONTROLLER ========== 49 | 50 | osd-bar=no # Don't show a huge volume box on screen when turning the volume up/down 51 | osc=no # Allows for custom OSC to be used https://github.com/cyl0/mpv-osc-morden-x 52 | border=no # Optional for modern OSC, but recommended 53 | cursor-autohide-fs-only=yes # If this option is given, the cursor is always visible in windowed mode 54 | # In fullscreen mode, the cursor is shown or hidden according to --cursor-autohide 55 | cursor-autohide=300 # Cursor hide in ms 56 | osd-level=1 # https://mpv.io/manual/stable/#options-osd-level 57 | osd-duration=1000 # Set the duration of the OSD messages in ms 58 | hr-seek=yes # Select when to use precise seeks that are not limited to keyframes 59 | # https://mpv.io/manual/stable/#options-hr-seek 60 | 61 | osd-font='Verdana' 62 | osd-font-size=20 63 | osd-color='#FFFFFF' # Hex code for white 64 | osd-border-color='#000000' # Hex code for black 65 | osd-border-size=0.6 # Size for osd text and progress bar 66 | osd-blur=0.2 # Gaussian blur factor. 0 means no blur applied (default) 67 | 68 | 69 | # ========== LANGUAGE PRIORITY ========== 70 | 71 | alang=ja,jp,jpn,en,eng # Audio language priority 72 | slang=en,eng # Subtitle language priority 73 | 74 | 75 | # ========== AUDIO ========== 76 | 77 | volume=100 # default volume, 100 = unchanged 78 | audio-file-auto=fuzzy # Load external audio with (almost) the same name as the video 79 | volume-max=200 # Max volume of the player 80 | audio-pitch-correction=yes # https://mpv.io/manual/stable/#options-audio-pitch-correction 81 | 82 | 83 | # ========== SUBTITLES ========== 84 | 85 | demuxer-mkv-subtitle-preroll=yes # https://mpv.io/manual/stable/#options-demuxer-mkv-subtitle-preroll 86 | sub-fix-timing=no # https://mpv.io/manual/stable/#options-sub-fix-timing 87 | sub-auto=all # https://mpv.io/manual/stable/#options-sub-auto 88 | 89 | # The following options only apply to subtitles without own styling 90 | sub-font='Netflix Sans Medium' # Specify font to use for subtitles that do not themselves specify a particular font 91 | sub-font-size=40 92 | sub-color='#FFFFFFFF' 93 | sub-border-color='#FF000000' 94 | sub-border-size=2.0 95 | sub-shadow-offset=0 96 | sub-spacing=0.0 97 | 98 | 99 | # ========== SCREENSHOT ========== 100 | 101 | screenshot-format=png # Output format of screenshots 102 | screenshot-high-bit-depth=yes # Same output bitdepth as the video. Set it "no" if you want to save disc space 103 | screenshot-png-compression=1 # Compression of the PNG picture (1-9). 104 | # Higher value means better compression, but takes more time 105 | screenshot-directory="~/Pictures/mpv-screenshots" # Output directory 106 | screenshot-template="%f-%wH.%wM.%wS.%wT-#%#00n" # Name format (filename-hour-minute-second-milisecond-number) 107 | 108 | 109 | # ========== INTERPOLATION ========== 110 | 111 | # blend-subtitles=yes # Subtitle blending in scenechanges (smoother effect) 112 | # video-sync=display-resample # Set the fps as the max of your monitor refresh rate (only useful and needed with "interpolation=yes) 113 | # interpolation=yes # Enable interpolation 114 | # tscale=oversample # Interpolation method 115 | 116 | 117 | # ========== CACHE ========== 118 | # cache=yes 119 | # cache-on-disk=yes 120 | # cache-dir="C:\mpv-cache" 121 | # demuxer-max-bytes=1000MiB 122 | # demuxer-readahead-secs=300 123 | # demuxer-max-back-bytes=200MiB 124 | 125 | 126 | # ========== AUTO PROFILES ========== 127 | 128 | # Auto profile that automatically applies for WEB-DL anime that need some debanding 129 | [WEB-DL] 130 | profile-desc=WEB-DL Anime (HatSubs, SubsPlease, HorribleSubs, Erai-raws) 131 | profile-cond=string.match(p.filename, "HatSubs")~=nil or string.match(p.filename, "SubsPlease")~=nil or string.match(p.filename, "HorribleSubs")~=nil or string.match(p.filename, "Erai%-raws")~=nil 132 | deband=yes 133 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mpv config 2 | 3 | ![mpv logo](https://raw.githubusercontent.com/mpv-player/mpv.io/master/source/images/mpv-logo-128.png) 4 | 5 | ## Overview 6 | 7 | **mpv** is a free (as in freedom and free beer), open-source, and cross-platform media player. It supports 8 | a wide variety of media file formats, audio and video codecs, and subtitle types. 9 | 10 | This repo contains my personal mpv configurations and scripts that I use and are significantly better than default mpv, VLC, and MPC. Before installing, please take your time to read this whole README as common issues can be easily solved by simply reading carefully. 11 | 12 | 13 | ## Preview 14 | 15 | [![preview.png](https://i.postimg.cc/8zNHHPHy/preview.png)](https://postimg.cc/VdZnsw2M) 16 | 17 | ## Installation 18 | 19 | ### Windows 20 | 21 | Here are the steps to install mpv and to use my configuration files on Windows: 22 | * Download the latest 64bit mpv Windows build by shinchiro from [mpv.io/installation](https://mpv.io/installation/) or directly from [here](https://sourceforge.net/projects/mpv-player-windows/files/) and extract it wherever you please. This is now your mpv folder 23 | * Run `mpv-install.bat`, which is located in `installer` folder, with administrator priviledges by right-clicking and selecting Run as administrator 24 | * Download this repository as a ZIP file (or you can clone it using git) 25 | * Create a folder named `portable_config` (**this is important**), located at the same directory as `mpv.exe` 26 | * Extract or copy the contents of this repository that you have downloaded to the `portable_config` folder 27 | * To make some scripts work, you need to modify them from the release a little bit: 28 | * In order for the `mpv-gif.lua` script to work, it requires [FFmpeg](https://ffmpeg.org/) with libass enabled and accessible via terminal. See the [installation instructions](https://github.com/Scheliux/mpv-gif-generator#installation) from the script's source repository for further info. 29 | * **(Optional)** By default, the `mpv-gif.lua` script saves GIFs to `C:/Program Files/mpv/gifs`. To modify this, open `gif.conf`, which is located in `portable_config/script-opts` folder, with a text editor and specify the `dir`, which is output directory for GIFs, as you please. For example `dir="C:/Users/USERNAME/Pictures/mpv-gifs"`. 30 | * **(Optional)** Make your own mpv configuration. You can do that by modifying my configuration files and/or making your own from scratch or modifying others' configurations. Check out the [useful links](#useful-links) section for mpv configuration guides. 31 | * You're all set up. 32 | 33 | ### Linux 34 | 35 | Here are the steps to install mpv and to use my configuration files on Linux: 36 | 37 | * Install mpv and xclip (clipboard CLI interface) using the package manager that comes with your Linux distribution. xclip is needed for [copy-time.lua](https://github.com/noelsimbolon/mpv-config/blob/linux/scripts/copy-time.lua) and [seek-to.lua](https://github.com/noelsimbolon/mpv-config/blob/linux/scripts/seek-to.lua) scripts to work properly. The package name for mpv and xclip might also vary depending on your Linux distribution. Here, I will make Arch Linux, that comes with `pacman` as its package manager, as an example 38 | 39 | ``` 40 | sudo pacman -S mpv xclip 41 | ``` 42 | 43 | If you, for example, use Fedora Linux, that comes with `dnf` as its package manager, you can install mpv and xclip with the following command instead. 44 | ``` 45 | sudo dnf install mpv xclip 46 | ``` 47 | 48 | If you use other Linux distributions, please refer to the documentation of your Linux distribution's package manager on how to install packages. 49 | 50 | * Download this repository as a ZIP file (or you can clone it using git) and extract/copy it to your standard mpv configuration directory which is `~/.config/mpv` 51 | * Some things to highlight: 52 | * In order for the `mpv-gif.lua` script to work, it requires [FFmpeg](https://ffmpeg.org/) with libass enabled and accessible via terminal. See the [installation instructions](https://github.com/Scheliux/mpv-gif-generator#installation) from the script's source repository for further info. 53 | * **(Optional)** By default, the `mpv-gif.lua` script saves GIFs to `~/Videos/mpv-gifs`. To modify this, open `gif.conf`, which is located in `portable_config/script-opts` folder, with a text editor and specify the `dir`, which is output directory for GIFs, as you please. For example `dir="~/Videos"`. 54 | * **(Optional)** Make your own mpv configuration. You can do that by modifying my configuration files and/or making your own from scratch or modifying others' configurations. Check out the [useful links](#useful-links) section for mpv configuration guides. 55 | * You're all set up. 56 | 57 | ## Scripts 58 | 59 | Scripts from external sources: 60 | 61 | * audio-visualizer.lua ([source](https://github.com/mfcc64/mpv-scripts#visualizerlua))\ 62 | Various audio visualization. It only works if you open audio files. 63 | 64 | * autoload.lua ([source](https://github.com/mpv-player/mpv/blob/master/TOOLS/lua/autoload.lua))\ 65 | Automatically load playlist entries before and after the currently playing file, by scanning the directory. 66 | 67 | * copy-time.lua ([source](https://github.com/linguisticmind/mpv-scripts/tree/master/copy-time))\ 68 | Copies current timecode in HH:MM:SS.MS format to clipboard. Cross-platform (Mac, Windows, Linux). 69 | 70 | * cycle-commands.lua ([source](https://github.com/CogentRedTester/mpv-scripts#cycle-commands))\ 71 | Cycles through a series of commands on a keypress. Each iteration of the cycle can contain as many commands as one wants. Syntax details are at the top of the file. 72 | 73 | * cycle-profile.lua ([source](https://github.com/CogentRedTester/mpv-scripts#cycle-profile))\ 74 | Cycles through a list of profiles sent via a script message and prints the profile-desc to the OSD. More details at the top of the file. 75 | 76 | * modernz.lua ([source](https://github.com/Samillion/ModernZ))\ 77 | A modern OSC UI replacement for MPV that retains the functionality of the default OSC. 78 | 79 | * mpv-gif.lua ([source](https://github.com/Scheliux/mpv-gif-generator))\ 80 | Script to generate GIFs from video playback. Requires FFmpeg with libass enabled. The exporting GIFs with subtitled currently doesn't work properly. 81 | 82 | * playlistmanager.lua ([source](https://github.com/jonniek/mpv-playlistmanager))\ 83 | Allows you to see and interact with your playlist in an intuitive way. 84 | 85 | * seek-to.lua ([source](https://github.com/dexeonify/mpv-config/blob/main/scripts/seek-to.lua))\ 86 | Seek to an absolute timestamp specified via keyboard input or pasted from clipboard. 87 | 88 | * sponsorblock-minimal.lua ([source](https://codeberg.org/jouni/mpv_sponsorblock_minimal))\ 89 | Skip sponsor segments in YouTube videos. 90 | 91 | * thumbfast.lua ([source](https://github.com/po5/thumbfast))\ 92 | High-performance on-the-fly thumbnailer for mpv. **The script does not display thumbnails on its own**, it is meant to be used alongside a UI script that calls thumbfast. 93 | 94 | Configuration files for these scripts can be found in the `script-opts` folder. I also modified some of these scripts' default keybindings. To see my modifications, look for script keybindings in `input.conf`. 95 | 96 | ## Shaders 97 | 98 | The shaders included in the `shaders` folder: 99 | 100 | * ArtCNN_C4F32 ([source](https://github.com/Artoriuz/ArtCNN/blob/main/GLSL/ArtCNN_C4F32.glsl))\ 101 | Used for luma upscaling. 102 | 103 | * nnedi3-nns128-win8x4 ([source](https://github.com/bjin/mpv-prescalers/tree/master))\ 104 | Used for luma upscaling. 105 | 106 | Use shaders based on your preference and system capabilities. For more info about shaders, read the resources in the [useful links](#useful-links) section. 107 | 108 | ## Useful Links 109 | 110 | * [mpv tutorial](https://thewiki.moe/tutorials/mpv/) by The Wiki 111 | * [mpv.conf guide](https://iamscum.wordpress.com/guides/videoplayback-guide/mpv-conf/) by iamscum 112 | * [mpv Configuration Guide for Watching Videos](https://kokomins.wordpress.com/2019/10/14/mpv-config-guide/) by Kokomins 113 | * [mpv Resampling](https://artoriuz.github.io/blog/mpv_upscaling.html) by João Vitor Chrisóstomo 114 | 115 | ## Official Links 116 | 117 | * [mpv homepage](https://mpv.io/) 118 | * [mpv wiki](https://github.com/mpv-player/mpv/wiki) 119 | * [mpv FAQ](https://github.com/mpv-player/mpv/wiki/FAQ) 120 | * [mpv manual](https://mpv.io/manual/stable/) 121 | * [mpv User Scripts](https://github.com/mpv-player/mpv/wiki/User-Scripts) 122 | -------------------------------------------------------------------------------- /script-opts/modernz.conf: -------------------------------------------------------------------------------- 1 | # Language and display 2 | # set language (for available options, see: https://github.com/Samillion/ModernZ/blob/main/docs/TRANSLATIONS.md) 3 | language=en 4 | # font for the OSC (default: mpv-osd-symbols or the one set in mpv.conf) 5 | font=mpv-osd-symbols 6 | 7 | # show mpv logo when idle 8 | idlescreen=yes 9 | # show OSC window top bar: "auto", "yes", or "no" (borderless/fullscreen) 10 | window_top_bar=auto 11 | # show OSC when windowed 12 | showwindowed=yes 13 | # show OSC when fullscreen 14 | showfullscreen=yes 15 | # show OSC when paused 16 | showonpause=no 17 | # disable OSC hide timeout when paused 18 | keeponpause=yes 19 | # disable Santa hat in December 20 | greenandgrumpy=no 21 | 22 | # OSC behaviour and scaling 23 | # time (in ms) before OSC hides if no mouse movement 24 | hidetimeout=1500 25 | # if seeking should reset the hidetimeout 26 | seek_resets_hidetimeout=yes 27 | # fade-out duration (in ms), set to 0 for no fade 28 | fadeduration=200 29 | # whether to enable fade-in effect 30 | fadein=no 31 | # minimum mouse movement (in pixels) required to show OSC 32 | minmousemove=0 33 | # show OSC only when hovering at the bottom 34 | bottomhover=yes 35 | # height of hover zone for bottomhover (in pixels) 36 | bottomhover_zone=130 37 | # show OSC when seeking 38 | osc_on_seek=no 39 | # show OSC on start of every file 40 | osc_on_start=no 41 | # pause video while seeking with mouse move (on button hold) 42 | mouse_seek_pause=yes 43 | # force show seekbar tooltip on mouse drag, even if not hovering seekbar 44 | force_seek_tooltip=no 45 | 46 | # scale osc with the video 47 | vidscale=auto 48 | # osc scale factor when windowed 49 | scalewindowed=1.0 50 | # osc scale factor when fullscreen 51 | scalefullscreen=1.0 52 | 53 | # Elements display 54 | # show title in the OSC (above seekbar) 55 | show_title=yes 56 | # title above seekbar format: "${media-title}" or "${filename}" 57 | title=${media-title} 58 | # font size of the title text (above seekbar) 59 | title_font_size=24 60 | # chapter title font size 61 | chapter_title_font_size=14 62 | # show cached time information 63 | cache_info=no 64 | # show cache speed per second 65 | cache_info_speed=no 66 | # font size of the cache information 67 | cache_info_font_size=12 68 | 69 | # show chapter title alongside timestamp (below seekbar) 70 | show_chapter_title=yes 71 | # format for chapter display on seekbar hover (set to "no" to disable) 72 | chapter_fmt=%s 73 | 74 | # show total time instead of remaining time 75 | timetotal=yes 76 | # show timecodes with milliseconds 77 | timems=no 78 | # use the Unicode minus sign in remaining time 79 | unicodeminus=no 80 | # "dynamic" or "fixed". dynamic shows MM:SS when possible, fixed always shows HH:MM:SS 81 | time_format=dynamic 82 | # font size of the time display 83 | time_font_size=16 84 | # tooltips font size 85 | tooltip_font_size=14 86 | 87 | # Title bar settings 88 | # show window title in borderless/fullscreen mode 89 | window_title=no 90 | # show window controls (close, minimize, maximize) in borderless/fullscreen 91 | window_controls=yes 92 | # same as title but for window_top_bar 93 | windowcontrols_title=${media-title} 94 | 95 | # Subtitle display settings 96 | # raise subtitles above the OSC when shown 97 | raise_subtitles=yes 98 | # amount by which subtitles are raised when the OSC is shown (in pixels) 99 | raise_subtitle_amount=125 100 | 101 | # Buttons display and functionality 102 | # show the jump backward and forward buttons 103 | jump_buttons=yes 104 | # change the jump amount in seconds 105 | jump_amount=10 106 | # change the jump amount in seconds when right-clicking jump buttons and shift-clicking chapter skip buttons 107 | jump_more_amount=60 108 | # show different icon when jump_amount is set to 5, 10, or 30 109 | jump_icon_number=yes 110 | # seek mode for jump buttons 111 | jump_mode=relative 112 | # enable continuous jumping when holding down seek buttons 113 | jump_softrepeat=yes 114 | # show the chapter skip backward and forward buttons 115 | chapter_skip_buttons=no 116 | # enable continuous skipping when holding down chapter skip buttons 117 | chapter_softrepeat=yes 118 | # show next/previous playlist track buttons 119 | track_nextprev_buttons=yes 120 | 121 | # show mute button and volume slider 122 | volume_control=yes 123 | # volume scale type: "linear" or "logarithmic" 124 | volume_control_type=linear 125 | # show playlist button: Left-click for simple playlist, Right-click for interactive playlist 126 | playlist_button=yes 127 | # hide playlist button when no playlist exists 128 | hide_empty_playlist_button=no 129 | # gray out the playlist button when no playlist exists 130 | gray_empty_playlist_button=no 131 | 132 | # show download button on web videos (requires yt-dlp and ffmpeg) 133 | download_button=yes 134 | # default download directory for videos (https://mpv.io/manual/master/#paths) 135 | download_path=~/Videos/mpv Downloads/ 136 | # show screenshot button 137 | screenshot_button=yes 138 | # flag for screenshot button: "subtitles", "video", "window", "each-frame" 139 | screenshot_flag=subtitles 140 | 141 | # show window on top button 142 | ontop_button=yes 143 | # show loop button 144 | loop_button=yes 145 | # show speed control button 146 | speed_button=yes 147 | # speed change amount per click 148 | speed_button_click=1 149 | # speed change amount on scroll 150 | speed_button_scroll=0.25 151 | # show info button 152 | info_button=yes 153 | # show fullscreen toggle button 154 | fullscreen_button=yes 155 | 156 | # enable looping by right-clicking pause 157 | loop_in_pause=yes 158 | 159 | # force buttons to always be active. can add: playlist_prev,playlist_next 160 | buttons_always_active=none 161 | 162 | # icon size for the play/pause button 163 | playpause_size=28 164 | # icon size for the middle buttons 165 | midbuttons_size=24 166 | # icon size for the side buttons 167 | sidebuttons_size=24 168 | 169 | # show zoom controls in image viewer mode 170 | zoom_control=yes 171 | # maximum zoom in value 172 | zoom_in_max=4 173 | # minimum zoom out value 174 | zoom_out_min=-1 175 | 176 | # Colors and style 177 | # accent color of the OSC and title bar 178 | osc_color=#000000 179 | # color of the title in borderless/fullscreen mode 180 | window_title_color=#FFFFFF 181 | # color of the window controls (close, minimize, maximize) in borderless/fullscreen mode 182 | window_controls_color=#FFFFFF 183 | # color of close window control on hover 184 | windowcontrols_close_hover=#F45C5B 185 | # color of maximize window controls on hover 186 | windowcontrols_max_hover=#F8BC3A 187 | # color of minimize window controls on hover 188 | windowcontrols_min_hover=#43CB44 189 | # color of the title (above seekbar) 190 | title_color=#FFFFFF 191 | # color of the cache information 192 | cache_info_color=#FFFFFF 193 | # color of the seekbar progress and handle 194 | seekbarfg_color=#FB8C00 195 | # color of the remaining seekbar 196 | seekbarbg_color=#94754F 197 | # color of the cache ranges on the seekbar 198 | seekbar_cache_color=#918F8E 199 | # match volume bar color with seekbar color (ignores side_buttons_color) 200 | volumebar_match_seek_color=no 201 | # color of the timestamps (below seekbar) 202 | time_color=#FFFFFF 203 | # color of the chapter title next to timestamp (below seekbar) 204 | chapter_title_color=#FFFFFF 205 | # color of the side buttons (audio, subtitles, playlist, etc.) 206 | side_buttons_color=#FFFFFF 207 | # color of the middle buttons (skip, jump, chapter, etc.) 208 | middle_buttons_color=#FFFFFF 209 | # color of the play/pause button 210 | playpause_color=#FFFFFF 211 | # color of the element when held down (pressed) 212 | held_element_color=#999999 213 | # color of a hovered button when hover_effect includes "color" 214 | hover_effect_color=#FB8C00 215 | # color of the border for thumbnails (with thumbfast) 216 | thumbnail_border_color=#111111 217 | # color of the border outline for thumbnails 218 | thumbnail_border_outline=#404040 219 | 220 | # alpha of the OSC background box 221 | fade_alpha=130 222 | # blur strength for the OSC alpha fade. caution: high values can take a lot of CPU time to render 223 | fade_blur_strength=100 224 | # use with "fade_blur_strength=0" to create a transparency box 225 | fade_transparency_strength=0 226 | # alpha of the window title bar (0 to disable) 227 | window_fade_alpha=100 228 | # blur strength for the window title bar. caution: high values can take a lot of CPU time to render 229 | window_fade_blur_strength=100 230 | # use with "window_fade_blur_strength=0" to create a transparency box 231 | window_fade_transparency_strength=0 232 | # width of the thumbnail border (for thumbfast) 233 | thumbnail_border=3 234 | # rounded corner radius for thumbnail border (0 to disable) 235 | thumbnail_border_radius=3 236 | 237 | # Button hover effects 238 | # active button hover effects: "glow", "size", "color"; can use multiple separated by commas 239 | hover_effect=size,glow,color 240 | # relative size of a hovered button if "size" effect is active 241 | hover_button_size=115 242 | # glow intensity when "glow" hover effect is active 243 | button_glow_amount=5 244 | # apply hover size effect to slider handle 245 | hover_effect_for_sliders=yes 246 | 247 | # Tooltips and hints 248 | # enable tooltips for disabled buttons and elements 249 | tooltips_for_disabled_elements=yes 250 | # enable text hints for info, loop, ontop, and screenshot buttons 251 | tooltip_hints=yes 252 | 253 | # Progress bar settings 254 | # size ratio of the seekbar handle (range: 0 ~ 1) 255 | seek_handle_size=0.8 256 | # show seek range overlay 257 | seekrange=yes 258 | # transparency of the seek range 259 | seekrangealpha=150 260 | # update chapter markers on the seekbar when duration changes 261 | livemarkers=yes 262 | # use keyframes when dragging the seekbar 263 | seekbarkeyframes=no 264 | # top chapter nibbles above seekbar 265 | nibbles_top=yes 266 | # bottom chapter nibbles below seekbar 267 | nibbles_bottom=yes 268 | # chapter nibble style. "triangle", "bar" or "single-bar" 269 | nibbles_style=triangle 270 | 271 | # automatically set keyframes for the seekbar based on video length 272 | automatickeyframemode=yes 273 | # videos longer than this (in seconds) will have keyframes on the seekbar 274 | automatickeyframelimit=600 275 | 276 | # always show a small progress line at the bottom of the screen 277 | persistentprogress=no 278 | # height of the persistent progress bar 279 | persistentprogressheight=17 280 | # show buffer status on web videos in the persistent progress line 281 | persistentbuffer=no 282 | 283 | # Miscellaneous settings 284 | # only used at init to set visibility_mode(...) 285 | visibility=auto 286 | # visibility modes to cycle through, modes are separated by _ 287 | visibility_modes=never_auto_always 288 | # minimum interval between OSC redraws (in seconds) 289 | tick_delay=0.03 290 | # use display FPS as the minimum redraw interval 291 | tick_delay_follow_display_fps=no 292 | 293 | # Elements Position 294 | # Useful when adjusting font size or type 295 | 296 | # title height position above seekbar 297 | title_height=96 298 | # title height position if a chapter title is below it 299 | title_with_chapter_height=108 300 | # chapter title height position above seekbar 301 | chapter_title_height=91 302 | # time codes height position 303 | time_codes_height=35 304 | # time codes height position with portrait window 305 | time_codes_centered_height=57 306 | # tooltip height position offset 307 | tooltip_height_offset=2 308 | # if tooltip contains many characters, it is moved to the left by offset 309 | tooltip_left_offset=5 310 | # portrait window width trigger to move some elements 311 | portrait_window_trigger=1000 312 | # hide volume bar trigger window width 313 | hide_volume_bar_trigger=1150 314 | # osc height offset if title above seekbar is disabled 315 | notitle_osc_h_offset=25 316 | # osc height offset if chapter title is disabled or doesn't exist 317 | nochapter_osc_h_offset=10 318 | # seek hover timecodes tooltip height position offset 319 | seek_hover_tooltip_h_offset=0 320 | # osc height without offsets 321 | osc_height=132 322 | 323 | ## Mouse commands 324 | ## details: https://github.com/Samillion/ModernZ#mouse-commands-user-options 325 | 326 | # title above seekbar mouse actions 327 | title_mbtn_left_command=script-binding stats/display-page-5 328 | title_mbtn_mid_command=show-text ${path} 329 | title_mbtn_right_command=script-binding select/select-watch-history; script-message-to modernz osc-hide 330 | 331 | # playlist button mouse actions 332 | playlist_mbtn_left_command=script-binding select/menu; script-message-to modernz osc-hide 333 | playlist_mbtn_right_command=script-binding select/select-playlist; script-message-to modernz osc-hide 334 | 335 | # volume mouse actions 336 | vol_ctrl_mbtn_left_command=no-osd cycle mute 337 | vol_ctrl_mbtn_right_command=script-binding select/select-audio-device; script-message-to modernz osc-hide 338 | vol_ctrl_wheel_down_command=no-osd add volume -5 339 | vol_ctrl_wheel_up_command=no-osd add volume 5 340 | 341 | # audio button mouse actions 342 | audio_track_mbtn_left_command=script-binding select/select-aid; script-message-to modernz osc-hide 343 | audio_track_mbtn_mid_command=cycle audio down 344 | audio_track_mbtn_right_command=cycle audio 345 | audio_track_wheel_down_command=cycle audio 346 | audio_track_wheel_up_command=cycle audio down 347 | 348 | # subtitle button mouse actions 349 | sub_track_mbtn_left_command=script-binding select/select-sid; script-message-to modernz osc-hide 350 | sub_track_mbtn_mid_command=cycle sub down 351 | sub_track_mbtn_right_command=cycle sub 352 | sub_track_wheel_down_command=cycle sub 353 | sub_track_wheel_up_command=cycle sub down 354 | 355 | # chapter skip buttons mouse actions 356 | chapter_prev_mbtn_left_command=add chapter -1 357 | chapter_prev_mbtn_mid_command=show-text ${chapter-list} 3000 358 | chapter_prev_mbtn_right_command=script-binding select/select-chapter; script-message-to modernz osc-hide 359 | 360 | chapter_next_mbtn_left_command=add chapter 1 361 | chapter_next_mbtn_mid_command=show-text ${chapter-list} 3000 362 | chapter_next_mbtn_right_command=script-binding select/select-chapter; script-message-to modernz osc-hide 363 | 364 | # chapter title (below seekbar) mouse actions 365 | chapter_title_mbtn_left_command=script-binding select/select-chapter; script-message-to modernz osc-hide 366 | chapter_title_mbtn_right_command=show-text ${chapter-list} 3000 367 | 368 | # playlist skip buttons mouse actions 369 | playlist_prev_mbtn_left_command=playlist-prev 370 | playlist_prev_mbtn_mid_command=show-text ${playlist} 3000 371 | playlist_prev_mbtn_right_command=script-binding select/select-playlist; script-message-to modernz osc-hide 372 | 373 | playlist_next_mbtn_left_command=playlist-next 374 | playlist_next_mbtn_mid_command=show-text ${playlist} 3000 375 | playlist_next_mbtn_right_command=script-binding select/select-playlist; script-message-to modernz osc-hide 376 | 377 | # fullscreen button mouse actions 378 | fullscreen_mbtn_left_command=cycle fullscreen 379 | fullscreen_mbtn_right_command=cycle window-maximized 380 | 381 | # info button mouse actions 382 | info_mbtn_left_command=script-binding stats/display-page-1-toggle 383 | -------------------------------------------------------------------------------- /scripts/audio_visualizer.lua: -------------------------------------------------------------------------------- 1 | -- various audio visualization 2 | 3 | local opts = { 4 | mode = "novideo", 5 | -- off disable visualization 6 | -- noalbumart enable visualization when no albumart and no video 7 | -- novideo enable visualization when no video 8 | -- force always enable visualization 9 | 10 | name = "showcqt", 11 | -- off 12 | -- showcqt 13 | -- avectorscope 14 | -- showspectrum 15 | -- showcqtbar 16 | -- showwaves 17 | 18 | quality = "medium", 19 | -- verylow 20 | -- low 21 | -- medium 22 | -- high 23 | -- veryhigh 24 | 25 | height = 6, 26 | -- [4 .. 12] 27 | 28 | forcewindow = true, 29 | -- true (yes) always run visualizer regardless of force-window settings 30 | -- false (no) does not run visualizer when force-window is no 31 | } 32 | 33 | -- key bindings 34 | -- cycle visualizer 35 | local cycle_key = "c" 36 | 37 | if not (mp.get_property("options/lavfi-complex", "") == "") then 38 | return 39 | end 40 | 41 | local visualizer_name_list = { 42 | "off", 43 | "showcqt", 44 | "avectorscope", 45 | "showspectrum", 46 | "showcqtbar", 47 | "showwaves", 48 | } 49 | 50 | local axis_0 = "image/png;base64," .. 51 | "iVBORw0KGgoAAAANSUhEUgAAB4AAAAAgCAQAAABZEK0tAAAACXBIWXMAAA7EAAAOxAGVKw4bAAASO0lEQVR42u2de2wU1xXGV/IfSJEqVUJCQrIUISFFiiqhSFWkKFKFokpB1TqxHROT8ApueDgEE9u4MW4TSqFA" .. 52 | "3TSUQmkSChRwII6BkAQCDSYlBtc1hiSA4/CyMcYGtsZvY3t3vXu719vVPjxzz71zd+wBvnOkdvHZ78w5v7mZmbt7Z9blgsFgMBgMBoPBYDAYDAaDwWAwGAwGg8FgMBgMBoPBYDAYDAaDwWCw+9HYBFbKboe8lE1A" .. 53 | "HHHEEUccccQRRxxxxBFHHPEHNe4KBSJWijjiiCOOOOKII4444ogjjjjiD1icwWAwGAwGg8FgMBgM9hAYJsAwGAwGg8FgMBgMBnsozOVyR7zuQOSPdQeif0UcccQRRxxxxBFHHHHEEUcc8QciHn05KaPuwGDHYEfd" .. 54 | "gUkZRgkQRxxxxBFHHHHEEUccccQRR/w+jhu9FQ6Hw+FwOBwOh8Ph8AfOx3Zz07LTXpmYzl89McuJOJ6e/czcCWkP7u4Gf/AHf/AHf/AHf/AHf/AHf/B/iPm73D99qaW2u7n7RqI/8lz4LWbxw1tVNjQh7dgH/Z6R" .. 55 | "JdjBzmuXKxl7b42sdvqctrORCjqvTc1S3elx9V9vOXNy1+gcP3r+5K6Bu7y8YW/jqZO7PPU5S+Tyx/Fp9lysO/CLV1TqA3/wB3/wB3/wB3/wB3/wB3/wB/8x4e9yL37N+PlYP3o+/BazePVe+c089XL7D4n6qjJZ" .. 56 | "dUlhrO7TLWo7wKj+gbvxkGbMv3sl8T3Ht8vlL8hLVPr6dq7Xqw/8wR/8wR/8wR/8wR/8wR/8wR/8k86f/89bK26eYazjSsXGsJ8ui90Bo+MVG7ua1HZAY1VoZj9Utacof8b8DSU15cGAmn5tcfnG/zaE2+tqUtsB" .. 57 | "8fXv33T6w8EOxpprYt9xs46xgK9qT0Hes/M2rbr13cgA2SOfP+hnrLacZ68t72sNiYNvrbBWH/iDP/iDP/iDP/iDP/iDP/iDP/jbxD/8f3UVjF2v5q8ef9HlXpQbyjAcuxY7Gp8y8uV1878ZO7lLtsDNv+Ul/e5X" .. 58 | "0b9cqlT9JGFypq+XscZTHM3bRaq7IFo/9z+/zZivPxrdsY7Xt35l5N8paV3XGavcLp8/4GMs0t+UrFvf8mESWcKgVh/4gz/4gz/4gz/4gz/4gz/4gz/428Q/vsC1xQFf9b5JGXcvf3/UqIE1bw57az5yuff/uadl" .. 59 | "eZ5sefzzh8ZTsX+ZPmfvO5MzVRCWv8tXhz8xi6O5+pXeDqjaw1hvazTaFNqtjV/Hvn/Xho4ruUut7QCXO/vV4DBja95Ur0+Ff+Fy8Ad/8Ad/8Ad/8Ad/8Ad/8Ad/8FfgHy2wt7Wugs+d284aNxCJ36xTbb+7mbGj" .. 60 | "76uq4p2vYb9U6XIf3sq/LH/qZfUdwOuvq/juM89F/nnD3ndi6gvt1C+06ovfAaGMN9Q6Bn/wB3/wB3/wB3/wB3/wB3/wB3/b+UcLjFjbOeMGRPFHnpu7yBzKPQ9jkduSH39xweKcJTlLFiz+Sbas3uVe9jrf8soC" .. 61 | "rh8eZOzETpXtx9cfvgm7IOb76/6Y+sw8Je3Jl+R3AF/TfrpMXq/LX5yf5k/VR/FX6c8K/4npOUvi61XjT+l1+Yvz0/yp+ij+Kv1Z4f/oC4tyfz7POn9Kr8tfnJ/mT9VH8Vfpz9rxR+0EMPr4Q5+g9I4/Ipc5/oid" .. 62 | "Pv7I9wf+9yf/1MxXc/kCQav8Rfpk8DfPL8dfVJ8Mf9n+MP7vv/HPr29/Nts6f0qfjOt/8/xy1/+i+mSu/2X7szb+017JWWK+qJYe/2K9/vgX5ZcZ/+L66PEv35/Djj/RAgfaG6u6Gs0/gTCPry4aaOdtNf/70ReM" .. 63 | "NtJ/i7GyUv6qII/ffv1/Cxbly+ld7otH+Kr469Xcvd2M9d+OXSFP60fqv8vVzdWe+oCXsYA3+hV5/23GPvyjGaKfZB/a0nTK211/VH4H3DkfGiQ75PU6/On8Yv4y9Yn4S/dnkX9KWs1HXMEXUZj9epmIv4xehz+d" .. 64 | "X8xfpj4Rf+n+LPJPzfz+GOfLWOfVuYvU+cvodfjT+cX8ZeoT8ZfuzyL/sJcU+geHB+ctVucvo9c7/lP5qeM/XZ/4+C/Zn0X+0+cEfGEf9n77qTp/Gb0Ofzq/mL9MfSL+0v1pjP8JabUf8wsvFvzkL1bGP6XXHf/i" .. 65 | "/PT4p+qjxr9Ufxb5tzdE9i/3jBxV/jJ6Hf50fjF/mfpE/KX7szz+95R6e3jBd86b/cCLePzTer3xT+Wnxj9dn3j8S/Znmf9rr/e08Pz9HmvnX1qvx5/KT/Gn6xPzl+xP6/pHbQI8+vpHYgLM12i/t4axugMu94aS" .. 66 | "tm8W5cY3wON/X8fYuYOJF6D3PN7ek7svf8VYTbnRRtrOMla1m796Zm74t564+e+FPwWg9VOz/AOJj7revEp++4lr0J98qb2BsfYfIv/mzzerrTBDdO4gX/3OmPwEeELaUGeowt/K63X40/nF/Gm9mL9Kf1b4f7mN" .. 67 | "sdvnj29vrmbBq1+r85fR6/Cn84v503oxf5X+rPCv2MhYS+2xbRePDA92XlPnL6PX4U/nF/On9WL+Kv1Z4R8m2nmNb9Xst/FE/GX0Ovzp/GL+tF7MX6U/K/yfmcsfqXG58nLlD8e3rlbnL6PX4U/nF/On9WL+Kv1Z" .. 68 | "G/+pmfwpop3Xaj769tDqIvXxT+v1xj+Vnxr/lJ4a//L9WeHf3jDQwffu5cq+NsaM17mI+MvodfjT+cX8ab2Yv0p/Vvgvez043Nd6uqzhy4DPU69+/JHR6/Cn84v503oxf5X+rPBPSeu+4R+s3ne6zD8w0G5856yI" .. 69 | "v4xehz+dX8yf1ov5q/Rn9fpHbQI8+vpHegLscucuTQnN7fnTvmK/5o7G85alpI2+AA3jaP5PwBt9eHfUa/kK8JsRNFOypmbxXRJ5DDetP7QltLsG+ArysPe2hi45z8hvP3EHuNzb1jLm7Y386/SHoX/1zJgfjU9M" .. 70 | "L3931sLI7nq7aGK6ygT4c75O3v/MXHm9Dn86v5g/rRfzV+tPnX/bue7m8OdNjaeGBxO/+aH5y+h1+NP5xfxpvZi/Wn/q/KdmrS0Ovzr/GWPGP4Mu4i+j1+FP5xfzp/Vi/mr9qfPnzu+84ZdXchPgeP4yeh3+dH4x" .. 71 | "f1ov5q/Wnzp/PsGjfjJCxF9Gr8Ofzi/mT+vF/NX6szL++c+CfHPI+MgmM/5pvd74p/JT45/SU+NfpT91/sX5kUl1b2t3szp/Gb0Ofzq/mD+tF/NX60+d/4kdjIUnQ98eYsx4kbuIv4xehz+dX8yf1ov5q/Wnzn/m" .. 72 | "Lxk7/eFIH+WM7Vinyl9Gr8Ofzi/mT+vF/NX6s3b9ozYBHn39Q0yAfzabPxb79vmi/Ih7vo89QEfjK94YfaftuU+CgfC0k4My+hJ8xnz/YGjG31BakprJPzFYnsefJRbJT+mfnce/+G89G/ls7cmX6vYzFgwW54eH" .. 73 | "Ar39+P7eWlGx0VPPF0xE4o/N7LvFv9bfUxr+z6cg785Fxm7GXWKJJnjTsgN+xg5uKsovzt/2+4tHWHD0CnSRXo8/nT+Rf96yWP6UnuKv2p8q/6deDt/dMTG9+8Y9jyz/aHe0Xo8/nV/Mn9JT/FX7szb+p8/ZvGqw" .. 74 | "o7NRnT+tT8b4F+WXGf/mernxL9+fOv+nZ/v6Oq9+9Q+zCR7Fn9Lr8qfyU/zFepq/Wn+q/PkEr/1Sc03LmSPvGU8yxPxpvR5/Or+YP6Wn+Kv2p8p/UoZ/sKvx6dkbSqbPkT//RunRej3+dH4xf0pP8Vftz9rxn196" .. 75 | "m91XJ3P8F+mTcfwX5Zc5/pvr5Y7/8v2p8t+6mrHPt05Ie2LWnfP3/qvOn9br8afzi/lTeoq/an+q/GctDH8r63Lvfcf4m0oxf1qvx5/OL+ZP6Sn+qv1ZPf6Yu9zxx3QCnLuUPzR6tEVShOLRdeMsGEicmTediszl" .. 76 | "d65nrLTEaDOfbuFFcbm3h/9oMbfIwUKsX1v8f2XI+CdtM+aH73fkFl7wTG0/vv6IBXwlhdH3FC739ob/7usN5w8Gd/9BboI4a+Fofo1f/zhddoKqy5+egIr5i/WJ/J+dF+Vf+7Fkf0ngPymj5Qxjxz6Q4990amJ6" .. 77 | "4mWKmT45/M3zy/E308vyF/anzX/Lav63oU6jB03I8Bfpk8FflF+Gv7lejj/Rnyb/H44Hg0X5lduNJ3g0f7Fen784P81fpJfhT/anxf+JWSMLxgYHOxJ/rEGOP6XX5U/lp/iL9TR/if60+OcuDV0sXeCXcEG/0ZM9" .. 78 | "Kf6UXpc/lZ/iL9bT/CX6S8L51+Wu/djq8UesT8751zy/3PnXTC97/hX2p8mf32Tm7Q0O+/tXvGGFv1ivz1+cn+Yv0svwJ/vT4p+S1tfq69+/6eRufpc9fxKyGn9Kr8ufyk/xF+tp/hL9JeX4Y+ayxx/TCfC07PAt" .. 79 | "zGY7ID7e70m8zb/pdOwE9I+/Nt5QacmdC1EQnVc/+lOkRLF+1kK+tG3k8rKLD+/JmXevhHfJ8ODx7TLbT+wvGLznaTr12uuJlxEXDocfZcOt7WxkWSM9wZuW3R3NH/T3tzfsKVWZoOryl5kAi/iL9Yn8J2Uk8lft" .. 80 | "zwr/adn8AfDG9wmM5l9WOvo9Ir0+f1F+Gf7mejn+Kv1Z4f/YzENbQtOMQHP16AOLDH+xXp+/KL8Mf3O9HH+V/lT5/6aQBS8cdrnNJngUf1qvx5/KT/EX62n+qv2pj/+y0tKSiekpafVfMBb74C7Z8U/pdce/OD89" .. 81 | "/kV6mfGv1p8q/02r+F+aq/f+6e6lYDD26aFy/Gm9Hn8qP8VfrKf5q/Zn7fqHP6rGeIGvzPFfrE/G9Y95frnrHzO97PWPfH+q/FMz275hbLAj4AsGyt9V50/r9fhT+Sn+Yj3NX7U/9fFflD/QMfLX0DVWzT718U/p" .. 82 | "dce/OD89/kV6mfGv1p/V44+Zyx5/TCfAuv7NoeBw+AHYfAnygsXm73xs5sqCzauK8uO/xpfX626f8tTMksLNqwryjB6QT00wKdfVJyO/MX/n9GfGf/ZCftdBVZnVvLr6ZOU34++U/kTjn/snf9F7Wp+uXje/aPw7" .. 83 | "oT9j/vyjjb62riZ+kmk7a/yMA5Hr6pOX35i/c/qjxn9BHmOf/dV6fl29bn5q/I93f8b8V7zBHzLHX5UUMnZyl2pWXX3y8hvzd05/ovFflK93htHVJyO/aPw7oT9j/l+8z9i2tSlpU7MaqwL+KVmqWXX1yctvzN85" .. 84 | "/ZmP/ylZ61fmLj25m2/JSmZdfXLym49/Z/RHnX9tcf0U+zcxtvEt/up6dcCnfgEy3no5fxAmwPdnf+uKfb1BP/9s76mX/75u7PVOr8/u/qK+Yx1j5p/x2q93en329Hf5q86r3PlPvd08o35809U7vT67+5ucGbnX" .. 85 | "74M1jO3aMNZ6p9dnd39TsoaHum/wVzlLGPvXP8Za7/T67O4v7GcPitZ32K93en329dfwZdCfOvIAoyN/Y8xskbF9eqfXZ3d/EV/zZsDXdV30qDl79U6vz+7+bPJknAAG2oe6jm///hhjdfvvPz3lxfkXDtcfZay7" .. 86 | "pf5obYX02vKk6Z1en739PTuP37PQ1VR/tP6o5yJjks92S5re6fXZ3Z/L3VzdXP351oObqvcNdfv6jX/mwU690+uzu7+wn9jZfinysInx0Du9Pvv662rsaancvn9TzT5vz1D3tOyx1ju9Prv7c7n5mb3hn59u6bjC" .. 87 | "TJcY26l3en1298f9nqf7+njqnV6fff2Vv8t/4urEzu8+8/V7e1Izx1rv9Prs7u/xF2srTuy4+q9gYKizcPnY651en9392ezJSLJ+5VAXX4DdenZq1v2oF3vl9uhN1v7+2Id1j43e6fXZ29/shfF3RryQM7Z6p9dn" .. 88 | "d38ud/W+yL0Zva3rV4693un12d0f9x+n8x+zZ+yeZ1LGeOidXp+d/ZVv9PaE9293i9kdtnbqnV6f3f3x59u3/Cf84JQv3h8PvdPrs7s/l/uR5/pu6Sxu19U7vT57+6v9OHyG6WuL/tTLWOqdXp+9/WXk8AfMMea5" .. 89 | "+ItXxkPv9Prs7s9mT9YpIHepTvvjrYfD4Waempm7tDj/+QVWl7fo6p1en939wcfXH3luweKifLMfmbFf7/T67O6Pe/ary/MefWH89E6vz+7+4OPpU7Pyls38pfXzi67e6fXZ29/kzOV5OrMLXb3T67O7P1sdBxc4" .. 90 | "HA6Hw+FwOBwOhz8UDgRwOBwOh8PhcDgcDn8oHAjgcDgcDofD4XA4HP4w+P8AQEuXMXpD8/kAAAAASUVORK5CYII=" 91 | 92 | local axis_1 = "image/png;base64," .. 93 | "iVBORw0KGgoAAAANSUhEUgAAB4AAAAAwCAQAAABaxq+2AAAACXBIWXMAAA7EAAAOxAGVKw4bAAATVklEQVR42u2df0xUZ7rHJ+EPkiabbGJiYkLSmJg0aTYxTTZNmiYb09ykZjO0QLHY+quy9Qe1YgG5Re62rlev" .. 94 | "etluXVevt62rrkq1FLG21epW7FqUZRFtq1LqLxAR1FnkNwIzw8x752V27gzDOe/zvuedA0f5fp9kd+SZ7zPv8zlvz5x35pwzLhcEQRAEQRAEQRAEQRAEQRAEQRAEQRAEQRAEQRAEQRAEQRAEQRAEQRAEQRAEQRAE" .. 95 | "QRAEQRAEQRAEQRAEQRAEQRAEQRBkq1gyK2F3Q1HCkpFHHnnkkUceeeSRRx555JFH/lHNu0KJiEqQRx555JFHHnnkkUceeeSRR/4RyzMIgiAIgiAIgiAImgTCAhiCIAiCIAiCIAiaFHK53JGoq4j8sa4i+lfkkUce" .. 96 | "eeSRRx555JFHHnnkkX8k8tGHU9PrKgY7BjvqKqamGxVAHnnkkUceeeSRRx555JFHHvmHOG/0VAQCgUAgEAgEAoFAIB65AAIEAoFAIBAIBAKBQEyKAAIEAoFAIBAIBAKBQEyKGN+Xm5mV+tqUNP7oqblOxPHsvOcW" .. 97 | "JKc+upsb/MEf/MEf/MEf/MEf/MEf/MF/EvN3uX/5Skttd3P3rfh47IXwU8zyR3eovFBy6omP+j0jN+EKdt64WsnYB+tlvbPmt52PjKDzxoxM1Y0+avw3W86d3ju2xs9ePL134D4f3rC38czpvZ767OVy9UfxafZc" .. 98 | "rqv49Wsq4wN/8Ad/8Ad/8Ad/8Ad/8Ad/8Af/ceHvci97w/gXkn72YvgpZvnqA/Iv88yr7T/F+6tKZd3FBbG+z7erbQCj8Q/cHw1p9qL71+Kfc3KXXP383Hinr2/PJr3xgT/4gz/4gz/4gz/4gz/4gz/4g3/C+fP/" .. 99 | "eWf17XOMdVwr3xKOs6WxG2BsvnxLV5PaBmisCq3sh6r2F+bNXrS5uKYsGFDzbygq2/LPhnB7XU1qG2D0+A9tPfvxYAdjzTWxz7hdx1jAV7U/P/f5hVvX3vlhZILsl68f9DNWW8ar15b1tYbMwXdWWxsf+IM/+IM/" .. 100 | "+IM/+IM/+IM/+IM/+NvEP/x/deWM3azmj5582eVemhOqMBx7LnY0P33ky+vmvzN2eq/sALf9jg/pP/89+pcrlaqfJEzL8PUy1niGo3m3UHUTRMfP44/vMubrj2Z3b+Tj27Qm8u+k1K6bjFXukq8f8DEW6W965p3v" .. 101 | "+TSJnMKgNj7wB3/wB3/wB3/wB3/wB3/wB3/wt4n/6AFuKAr4qg9OTb9/9cfjRg2sf3vYW/OJy33ojz0tq3Jlh8c/f2g8E/uXWfMPvDctQwVh2fv87PCn5nI017/R2wBV+xnrbY1mm0KbtfHb2Ofv3dxxLWeFtQ3g" .. 102 | "cme9HhxmbP3b6uNT4V+wCvzBH/zBH/zBH/zBH/zBH/zBH/wV+EcH2NtaV87Xzm3njRuI5G/Xqbbf3czY8Q9VXaODn8N+pdLlPrqDf1n+zKvqG4CPv678hy88l/nnDQfeixlfaKN+pTW+0RsgVPGWWsfgD/7gD/7g" .. 103 | "D/7gD/7gD/7gD/7gbzv/6AAjartg3IAo/9gLC5aaQ3ngYSxyWfKTLy9elr08e/niZb/IkvW73Cvf5K+8Jp/7hwcZO7VH5fVHjz98EXZ+zPfX/THjM4uk1Kdfkd8A/Jz2s6Xyfl3+4vo0f2p8FH+V/qzwn5KWvXz0" .. 104 | "eNX4U35d/uL6NH9qfBR/lf6s8H/8paU5/7bQOn/Kr8tfXJ/mT42P4q/Sn7X9j9obwNj9D/0Gpbf/EYXM/kcc9P5Hvj/wfzj5p2S8nsNPELTKX+RPBH/z+nL8ReOT4S/bH+b/wzf/+fHtr+ZZ50/5E3H8b15f7vhf" .. 105 | "ND6Z43/Z/qzN/9TXspebn1RLz3+xX3/+i+rLzH/x+Oj5L9+fw/Y/0QEOtDdWdTWafwJhnl9XONDO22r+++MvGb1I/x3GSkv4o/xcfvn1vxQszJPzu9yXj/Gz4m9W8/B2M9Z/N/YMedo/Mv773N1c7akPeBkLeKNf" .. 106 | "kfffZezj35sh+kXWke1NZ7zd9cflN8C9i6FJslver8Ofri/mLzM+EX/p/izyT0qt+YQ7+EkUZr9eJuIv49fhT9cX85cZn4i/dH8W+adk/HiC82Ws8/qCper8Zfw6/On6Yv4y4xPxl+7PIv9wFBf4B4cHFy5T5y/j" .. 107 | "19v/U/Wp/T89PvH+X7I/i/xnzQ/4wjHs/f5zdf4yfh3+dH0xf5nxifhL96cx/5NTaz/lB14s+NmfrMx/yq87/8X16flPjY+a/1L9WeTf3hDZvjzSs1X5y/h1+NP1xfxlxifiL92f5fm/v8Tbwwd876LZD7yI5z/t" .. 108 | "15v/VH1q/tPjE89/yf4s83/jzZ4WXr/fY+39l/br8afqU/zp8Yn5S/andfyjtgAee/wjsQDm52h/sJ6xugqXe3Nx23dLc0Y3wPN/3sjYhcPxB6APPN7e0/uufsNYTZnRi7SdZ6xqH3/03ILwbz1x+R+EPwWg/TMy" .. 109 | "/QPxt7retlb+9ePPQX/6lfYGxtp/ivyb39+sttwM0YXD/Ox3xuQXwMmpQ52hEf5O3q/Dn64v5k/7xfxV+rPC/+udjN29eHJXczULXv9Wnb+MX4c/XV/Mn/aL+av0Z4V/+RbGWmpP7Lx8bHiw84Y6fxm/Dn+6vpg/" .. 110 | "7RfzV+nPCv8w0c4b/FXNfhtPxF/Gr8Ofri/mT/vF/FX6s8L/uQX8lhpXK69W/nRyxzp1/jJ+Hf50fTF/2i/mr9KftfmfksHvItp5o+aT74+sK1Sf/7Rfb/5T9an5T/mp+S/fnxX+7Q0DHXzrXq3sa2PM+DwXEX8Z" .. 111 | "vw5/ur6YP+0X81fpzwr/lW8Gh/taz5Y2fB3weerV9z8yfh3+dH0xf9ov5q/SnxX+Sandt/yD1QfPlvoHBtqNr5wV8Zfx6/Cn64v5034xf5X+rB7/qC2Axx7/SC+AXe6cFUmhtT2/21fs19zRfO7KpNSxB6BhHM3/" .. 112 | "CHijN++ORi0/A/x2BM30zBmZfJNEbsNN+49sD22uAX4GeTh6W0OHnOfkXz9+A7jcOzcw5u2N/Ovsx6F/9cxeFM1PSSt7f+6SyOZ6t3BKmsoC+Et+nrz/uQXyfh3+dH0xf9ov5q/Wnzr/tgvdzeHPmxrPDA/Gf/ND" .. 113 | "85fx6/Cn64v5034xf7X+1PnPyNxQFH508QvGjH8GXcRfxq/Dn64v5k/7xfzV+lPnz4NfecMPr+QWwKP5y/h1+NP1xfxpv5i/Wn/q/PkCj/rJCBF/Gb8Of7q+mD/tF/NX68/K/Oc/C/LdEeM9m8z8p/1685+qT81/" .. 114 | "yk/Nf5X+1PkX5UUW1b2t3c3q/GX8Ovzp+mL+tF/MX60/df6ndjMWXgx9f4Qx45PcRfxl/Dr86fpi/rRfzF+tP3X+c37D2NmPR/ooY2z3RlX+Mn4d/nR9MX/aL+av1p+14x+1BfDY4x9iAfyrefy22HcvFuZFwvNj" .. 115 | "7A46ml/91tgrbS98FgyEl50clNGX4LMX+QdDK/6GkuKUDP6Jwapcfi+xSH3K//xC/sV/6/nIZ2tPv1J3iLFgsCgvPBXo1x/d3zury7d46vkJE5H8E3P67vCv9feXhP/zyc+9d5mx26MOsUQLvJlZAT9jh7cW5hXl" .. 116 | "7fyvy8dYcOwZ6CK/Hn+6fjz/3JWx/Ck/xV+1P1X+z7wavrpjSlr3rQceWf7R7mi/Hn+6vpg/5af4q/Znbf7Pmr9t7WBHZ6M6f9qfiPkvqi8z/839cvNfvj91/s/O8/V1Xv/mL2YLPIo/5dflT9Wn+Iv9NH+1/lT5" .. 117 | "8wVe+5XmmpZzxz4wXmSI+dN+Pf50fTF/yk/xV+1Plf/UdP9gV+Oz8zYXz5ov//4bpUf79fjT9cX8KT/FX7U/a/t/fuhtdl2dzP5f5E/E/l9UX2b/b+6X2//L96fKf8c6xr7ckZz61Nx7Fx/8U50/7dfjT9cX86f8" .. 118 | "FH/V/lT5z10S/lbW5T7wnvE3lWL+tF+PP11fzJ/yU/xV+7O6/zEPuf2P6QI4ZwW/afRYRUqE8tHzxlkwEL8ybzoTWcvv2cRYSbHRy3y+nQ+K2709/EeLuSI7C7F/Q9G/nCHxT9pmLwpf78gVPuGZev3R448o4Csu" .. 119 | "iD6nYJW3N/x3X2+4fjC477/lFohzl4zl1/jtz9NkF6i6/OkFqJi/2B/P//mFUf61n0r2lwD+U9NbzjF24iM5/k1npqTFH6aY+RPD37y+HH8zvyx/YX/a/Lev438b6jS60YQMf5E/EfxF9WX4m/vl+BP9afL/6WQw" .. 120 | "WJhXuct4gUfzF/v1+Yvr0/xFfhn+ZH9a/J+aO3LC2OBgR/yPNcjxp/y6/Kn6FH+xn+Yv0Z8W/5wVoYOlS/wQLug3urMnxZ/y6/Kn6lP8xX6av0R/CXj/dblrP7W6/xH7E/P+a15f7v3XzC/7/ivsT5M/v8jM2xsc" .. 121 | "9vevfssKf7Ffn7+4Ps1f5JfhT/anxT8pta/V139o6+l9/Cp7fidkNf6UX5c/VZ/iL/bT/CX6S8j+xyxk9z+mC+CZWeFLmM02wOh8vyf+Mv+ms7EL0N//h/ELlRTfuxQF0Xn9kz9Ehij2z13CT20bObzs4tN7Wsb9" .. 122 | "a+FNMjx4cpfM68f3Fww+8DSdeePN+MOIS0fDt7LhajsfOa2RXuDNzOqO1g/6+9sb9peoLFB1+cssgEX8xf54/lPT4/mr9meF/8wsfgN44+sExvIvLRn7HJFfn7+ovgx/c78cf5X+rPB/Ys6R7aFlRqC5euyORYa/" .. 123 | "2K/PX1Rfhr+5X46/Sn+q/H9bwIKXjrrcZgs8ij/t1+NP1af4i/00f9X+1Od/aUlJ8ZS0pNT6rxiLvXGX7Pyn/LrzX1yfnv8iv8z8V+tPlf/WtfwvzdUH/nD/SjAYe/dQOf60X48/VZ/iL/bT/FX7s3b8w29VY3yC" .. 124 | "r8z+X+xPxPGPeX254x8zv+zxj3x/qvxTMtq+Y2ywI+ALBsreV+dP+/X4U/Up/mI/zV+1P/X5X5g30DHy19AxVs1B9flP+XXnv7g+Pf9Ffpn5r9af1f2PWcjuf0wXwLrx3ZHgcPgG2PwU5MXLzJ/5xJw1+dvWFuaN" .. 125 | "/hpf3q/7+lSkZBQXbFubn2t0g3xqgUmFrj8R9Y35O6c/M/7zlvCrDqpKrdbV9Seqvhl/p/Qnmv88PvuT3t36dP269UXz3wn9GfPnH230tXU18TeZtvPG9zgQha4/cfWN+TunP2r+5+cy9sX/WK+v69etT83/ie7P" .. 126 | "mP/qt/hN5vij4gLGTu9VrarrT1x9Y/7O6U80/wvz9N5hdP2JqC+a/07oz5j/Vx8ytnNDUuqMzMaqgH96pmpVXX/i6hvzd05/5vN/euamNTkrTu/jr2Slsq4/MfXN578z+qPef20J/RKHtjK25R3+6GZ1wKd+ADLR" .. 127 | "frl4FBbAD2d/G4t8vUE//2zvmVf/vHH8/U4fn939RWP3RsbMP+O13+/08dnT39VvOq/z4D/1dvuc+v5N1+/08dnd37SMyLV+H61nbO/m8fY7fXx29zc9c3io+xZ/lL2csb/9Zbz9Th+f3f2F4/xh0fkd9vudPj77" .. 128 | "+mv4OuhPGbmB0bH/ZczsJGP7/E4fn939RWL92wFf103Rrebs9Tt9fHb3Z1Mk4g1goH2o6+SuH08wVnfo4fNTUZR36Wj9cca6W+qP15ZLn1ueML/Tx2dvf88v5NcsdDXVH68/7rnMmOS93RLmd/r47O7P5W6ubq7+" .. 129 | "csfhrdUHh7p9/cY/82Cn3+njs7u/cJza034lcrOJifA7fXz29dfV2NNSuevQ1pqD3p6h7plZ4+13+vjs7s/l5u/sDX/9fHvHNWZ6irGdfqePz+7+eDzwdN+cSL/Tx2dff2Xv85+4OrXnhy98/d6elIzx9jt9fHb3" .. 130 | "9+TLteWndl//WzAw1Fmwavz9Th+f3f3ZHIkosmnNUBc/Abv1/IzMh9Evjspd0Yus/f2xN+seH7/Tx2dvf/OWjL4y4qXs8fU7fXx29+dyVx+MXJvR27ppzfj7nT4+u/vj8fM0/mP2jD3wTE2fCL/Tx2dnf2VbvD3h" .. 131 | "7dvdYnaFrZ1+p4/P7v74/e1b/hG+ccpXH06E3+njs7s/l/uxF/ru6Jzcrut3+vjs7a/20/A7TF9b9KdextPv9PHZ2196Nr/BHGOey79+bSL8Th+f3f3ZHIl6C8hZodP+RPsRCIRZpGTkrCjKe3Gx1dNbdP1OH5/d" .. 132 | "/SEmNh57YfGywjyzH5mx3+/08dndH4+s11flPv7SxPmdPj67+0NMZMzIzF055zfW3190/U4fn739TctYlauzutD1O318dvdna2DngkAgEAgEAoFAIBCISRFAgEAgEAgEAoFAIBCISRHRh1PT6yqGOoc66yqMr6NC" .. 133 | "HnnkkUceeeSRRx555JFHHvmHOB99WFcRuZGO8b00kUceeeSRRx555JFHHnnkkUf+Ic4zCIIgCIIgCIIgCJoEwgIYgiAIgiAIgiAImhRyRcVK/v+vJS4DIY888sgjjzzyyCOPPPLII4/8o5B3seTQU+6EooQlI488" .. 134 | "8sgjjzzyyCOPPPLII4/8o5qHIAiCIAiCIAiCIAiCIAiCIAiCIAiCIAiCIAiCIAiCIAiCIAiCIAiCIAiCIAiCIAiCIAiCIAiCIAiCIAiCIAiCIAiCEqf/A/SNfayCCBqGAAAAAElFTkSuQmCC" 135 | 136 | local options = require 'mp.options' 137 | local msg = require 'mp.msg' 138 | 139 | options.read_options(opts) 140 | opts.height = math.min(12, math.max(4, opts.height)) 141 | opts.height = math.floor(opts.height) 142 | 143 | if not opts.forcewindow and mp.get_property('force-window') == "no" then 144 | return 145 | end 146 | 147 | local function get_visualizer(name, quality, vtrack) 148 | local w, h, fps 149 | 150 | if quality == "verylow" then 151 | w = 640 152 | fps = 30 153 | elseif quality == "low" then 154 | w = 960 155 | fps = 30 156 | elseif quality == "medium" then 157 | w = 1280 158 | fps = 60 159 | elseif quality == "high" then 160 | w = 1920 161 | fps = 60 162 | elseif quality == "veryhigh" then 163 | w = 2560 164 | fps = 60 165 | else 166 | msg.log("error", "invalid quality") 167 | return "" 168 | end 169 | 170 | h = w * opts.height / 16 171 | 172 | if name == "showcqt" then 173 | local count = math.ceil(w * 180 / 1920 / fps) 174 | 175 | return "[aid1] asplit [ao]," .. 176 | "aformat = channel_layouts = stereo," .. 177 | "firequalizer =" .. 178 | "gain = '1.4884e8 * f*f*f / (f*f + 424.36) / (f*f + 1.4884e8) / sqrt(f*f + 25122.25)':" .. 179 | "scale = linlin:" .. 180 | "wfunc = tukey:" .. 181 | "zero_phase = on:" .. 182 | "fft2 = on," .. 183 | "showcqt =" .. 184 | "fps =" .. fps .. ":" .. 185 | "size =" .. w .. "x" .. h .. ":" .. 186 | "count =" .. count .. ":" .. 187 | "csp = bt709:" .. 188 | "bar_g = 2:" .. 189 | "sono_g = 4:" .. 190 | "bar_v = 9:" .. 191 | "sono_v = 17:" .. 192 | "axisfile = data\\\\:'" .. axis_0 .. "':" .. 193 | "font = 'Nimbus Mono L,Courier New,mono|bold':" .. 194 | "fontcolor = 'st(0, (midi(f)-53.5)/12); st(1, 0.5 - 0.5 * cos(PI*ld(0))); r(1-ld(1)) + b(ld(1))':" .. 195 | "tc = 0.33:" .. 196 | "attack = 0.033:" .. 197 | "tlength = 'st(0,0.17); 384*tc / (384 / ld(0) + tc*f /(1-ld(0))) + 384*tc / (tc*f / ld(0) + 384 /(1-ld(0)))'," .. 198 | "format = yuv420p [vo]" 199 | 200 | 201 | elseif name == "avectorscope" then 202 | return "[aid1] asplit [ao]," .. 203 | "aformat =" .. 204 | "sample_rates = 192000," .. 205 | "avectorscope =" .. 206 | "size =" .. w .. "x" .. h .. ":" .. 207 | "r =" .. fps .. "," .. 208 | "format = rgb0 [vo]" 209 | 210 | 211 | elseif name == "showspectrum" then 212 | return "[aid1] asplit [ao]," .. 213 | "showspectrum =" .. 214 | "size =" .. w .. "x" .. h .. ":" .. 215 | "win_func = blackman [vo]" 216 | 217 | 218 | elseif name == "showcqtbar" then 219 | local axis_h = math.ceil(w * 12 / 1920) * 4 220 | 221 | return "[aid1] asplit [ao]," .. 222 | "aformat = channel_layouts = stereo," .. 223 | "firequalizer =" .. 224 | "gain = '1.4884e8 * f*f*f / (f*f + 424.36) / (f*f + 1.4884e8) / sqrt(f*f + 25122.25)':" .. 225 | "scale = linlin:" .. 226 | "wfunc = tukey:" .. 227 | "zero_phase = on:" .. 228 | "fft2 = on," .. 229 | "showcqt =" .. 230 | "fps =" .. fps .. ":" .. 231 | "size =" .. w .. "x" .. (h + axis_h)/2 .. ":" .. 232 | "count = 1:" .. 233 | "csp = bt709:" .. 234 | "bar_g = 2:" .. 235 | "sono_g = 4:" .. 236 | "bar_v = 9:" .. 237 | "sono_v = 17:" .. 238 | "sono_h = 0:" .. 239 | "axisfile = data\\\\:'" .. axis_1 .. "':" .. 240 | "axis_h =" .. axis_h .. ":" .. 241 | "font = 'Nimbus Mono L,Courier New,mono|bold':" .. 242 | "fontcolor = 'st(0, (midi(f)-53.5)/12); st(1, 0.5 - 0.5 * cos(PI*ld(0))); r(1-ld(1)) + b(ld(1))':" .. 243 | "tc = 0.33:" .. 244 | "attack = 0.033:" .. 245 | "tlength = 'st(0,0.17); 384*tc / (384 / ld(0) + tc*f /(1-ld(0))) + 384*tc / (tc*f / ld(0) + 384 /(1-ld(0)))'," .. 246 | "format = yuv420p," .. 247 | "split [v0]," .. 248 | "crop =" .. 249 | "h =" .. (h - axis_h)/2 .. ":" .. 250 | "y = 0," .. 251 | "vflip [v1];" .. 252 | "[v0][v1] vstack [vo]" 253 | 254 | 255 | elseif name == "showwaves" then 256 | return "[aid1] asplit [ao]," .. 257 | "showwaves =" .. 258 | "size =" .. w .. "x" .. h .. ":" .. 259 | "r =" .. fps .. ":" .. 260 | "mode = p2p," .. 261 | "format = rgb0 [vo]" 262 | elseif name == "off" then 263 | local hasvideo = false 264 | for id, track in ipairs(mp.get_property_native("track-list")) do 265 | if track.type == "video" then 266 | hasvideo = true 267 | break 268 | end 269 | end 270 | if hasvideo then 271 | return "[aid1] asetpts=PTS [ao]; [vid1] setpts=PTS [vo]" 272 | else 273 | return "[aid1] asetpts=PTS [ao];" .. 274 | "color =" .. 275 | "c = Black:" .. 276 | "s =" .. w .. "x" .. h .. "," .. 277 | "format = yuv420p [vo]" 278 | end 279 | end 280 | 281 | msg.log("error", "invalid visualizer name") 282 | return "" 283 | end 284 | 285 | local function select_visualizer(vtrack) 286 | if opts.mode == "off" then 287 | return "" 288 | elseif opts.mode == "force" then 289 | return get_visualizer(opts.name, opts.quality, vtrack) 290 | elseif opts.mode == "noalbumart" then 291 | if vtrack == nil then 292 | return get_visualizer(opts.name, opts.quality, vtrack) 293 | end 294 | return "" 295 | elseif opts.mode == "novideo" then 296 | if vtrack == nil or vtrack.albumart then 297 | return get_visualizer(opts.name, opts.quality, vtrack) 298 | end 299 | return "" 300 | end 301 | 302 | msg.log("error", "invalid mode") 303 | return "" 304 | end 305 | 306 | local function visualizer_hook() 307 | local count = mp.get_property_number("track-list/count", -1) 308 | if count <= 0 then 309 | return 310 | end 311 | 312 | local atrack = mp.get_property_native("current-tracks/audio") 313 | local vtrack = mp.get_property_native("current-tracks/video") 314 | 315 | --no tracks selected (yet) 316 | if atrack == nil and vtrack == nil then 317 | for id, track in ipairs(mp.get_property_native("track-list")) do 318 | if track.type == "video" and (vtrack == nil or vtrack.albumart == true) and mp.get_property("vid") ~= "no" then 319 | vtrack = track 320 | elseif track.type == "audio" then 321 | atrack = track 322 | end 323 | end 324 | end 325 | 326 | local lavfi = select_visualizer(vtrack) 327 | --prevent endless loop 328 | if lavfi ~= mp.get_property("options/lavfi-complex", "") then 329 | mp.set_property("options/lavfi-complex", lavfi) 330 | end 331 | end 332 | 333 | mp.add_hook("on_preloaded", 50, visualizer_hook) 334 | mp.observe_property("current-tracks/audio", "native", visualizer_hook) 335 | mp.observe_property("current-tracks/video", "native", visualizer_hook) 336 | 337 | local function cycle_visualizer() 338 | local i, index = 1 339 | for i = 1, #visualizer_name_list do 340 | if (visualizer_name_list[i] == opts.name) then 341 | index = i + 1 342 | if index > #visualizer_name_list then 343 | index = 1 344 | end 345 | break 346 | end 347 | end 348 | opts.name = visualizer_name_list[index] 349 | visualizer_hook() 350 | end 351 | 352 | mp.add_key_binding(cycle_key, "cycle-visualizer", cycle_visualizer) 353 | -------------------------------------------------------------------------------- /scripts/thumbfast.lua: -------------------------------------------------------------------------------- 1 | -- thumbfast.lua 2 | -- 3 | -- High-performance on-the-fly thumbnailer 4 | -- 5 | -- Built for easy integration in third-party UIs. 6 | 7 | --[[ 8 | This Source Code Form is subject to the terms of the Mozilla Public 9 | License, v. 2.0. If a copy of the MPL was not distributed with this 10 | file, You can obtain one at https://mozilla.org/MPL/2.0/. 11 | ]] 12 | 13 | local options = { 14 | -- Socket path (leave empty for auto) 15 | socket = "", 16 | 17 | -- Thumbnail path (leave empty for auto) 18 | thumbnail = "", 19 | 20 | -- Maximum thumbnail size in pixels (scaled down to fit) 21 | -- Values are scaled when hidpi is enabled 22 | max_height = 200, 23 | max_width = 200, 24 | 25 | -- Apply tone-mapping, no to disable 26 | tone_mapping = "auto", 27 | 28 | -- Overlay id 29 | overlay_id = 42, 30 | 31 | -- Spawn thumbnailer on file load for faster initial thumbnails 32 | spawn_first = false, 33 | 34 | -- Close thumbnailer process after an inactivity period in seconds, 0 to disable 35 | quit_after_inactivity = 0, 36 | 37 | -- Enable on network playback 38 | network = false, 39 | 40 | -- Enable on audio playback 41 | audio = false, 42 | 43 | -- Enable hardware decoding 44 | hwdec = false, 45 | 46 | -- Windows only: use native Windows API to write to pipe (requires LuaJIT) 47 | direct_io = false, 48 | 49 | -- Custom path to the mpv executable 50 | mpv_path = "mpv" 51 | } 52 | 53 | mp.utils = require "mp.utils" 54 | mp.options = require "mp.options" 55 | mp.options.read_options(options, "thumbfast") 56 | 57 | local properties = {} 58 | local pre_0_30_0 = mp.command_native_async == nil 59 | local pre_0_33_0 = true 60 | 61 | function subprocess(args, async, callback) 62 | callback = callback or function() end 63 | 64 | if not pre_0_30_0 then 65 | if async then 66 | return mp.command_native_async({name = "subprocess", playback_only = true, args = args}, callback) 67 | else 68 | return mp.command_native({name = "subprocess", playback_only = false, capture_stdout = true, args = args}) 69 | end 70 | else 71 | if async then 72 | return mp.utils.subprocess_detached({args = args}, callback) 73 | else 74 | return mp.utils.subprocess({args = args}) 75 | end 76 | end 77 | end 78 | 79 | local winapi = {} 80 | if options.direct_io then 81 | local ffi_loaded, ffi = pcall(require, "ffi") 82 | if ffi_loaded then 83 | winapi = { 84 | ffi = ffi, 85 | C = ffi.C, 86 | bit = require("bit"), 87 | socket_wc = "", 88 | 89 | -- WinAPI constants 90 | CP_UTF8 = 65001, 91 | GENERIC_WRITE = 0x40000000, 92 | OPEN_EXISTING = 3, 93 | FILE_FLAG_WRITE_THROUGH = 0x80000000, 94 | FILE_FLAG_NO_BUFFERING = 0x20000000, 95 | PIPE_NOWAIT = ffi.new("unsigned long[1]", 0x00000001), 96 | 97 | INVALID_HANDLE_VALUE = ffi.cast("void*", -1), 98 | 99 | -- don't care about how many bytes WriteFile wrote, so allocate something to store the result once 100 | _lpNumberOfBytesWritten = ffi.new("unsigned long[1]"), 101 | } 102 | -- cache flags used in run() to avoid bor() call 103 | winapi._createfile_pipe_flags = winapi.bit.bor(winapi.FILE_FLAG_WRITE_THROUGH, winapi.FILE_FLAG_NO_BUFFERING) 104 | 105 | ffi.cdef[[ 106 | void* __stdcall CreateFileW(const wchar_t *lpFileName, unsigned long dwDesiredAccess, unsigned long dwShareMode, void *lpSecurityAttributes, unsigned long dwCreationDisposition, unsigned long dwFlagsAndAttributes, void *hTemplateFile); 107 | bool __stdcall WriteFile(void *hFile, const void *lpBuffer, unsigned long nNumberOfBytesToWrite, unsigned long *lpNumberOfBytesWritten, void *lpOverlapped); 108 | bool __stdcall CloseHandle(void *hObject); 109 | bool __stdcall SetNamedPipeHandleState(void *hNamedPipe, unsigned long *lpMode, unsigned long *lpMaxCollectionCount, unsigned long *lpCollectDataTimeout); 110 | int __stdcall MultiByteToWideChar(unsigned int CodePage, unsigned long dwFlags, const char *lpMultiByteStr, int cbMultiByte, wchar_t *lpWideCharStr, int cchWideChar); 111 | ]] 112 | 113 | winapi.MultiByteToWideChar = function(MultiByteStr) 114 | if MultiByteStr then 115 | local utf16_len = winapi.C.MultiByteToWideChar(winapi.CP_UTF8, 0, MultiByteStr, -1, nil, 0) 116 | if utf16_len > 0 then 117 | local utf16_str = winapi.ffi.new("wchar_t[?]", utf16_len) 118 | if winapi.C.MultiByteToWideChar(winapi.CP_UTF8, 0, MultiByteStr, -1, utf16_str, utf16_len) > 0 then 119 | return utf16_str 120 | end 121 | end 122 | end 123 | return "" 124 | end 125 | 126 | else 127 | options.direct_io = false 128 | end 129 | end 130 | 131 | local file = nil 132 | local file_bytes = 0 133 | local spawned = false 134 | local disabled = false 135 | local force_disabled = false 136 | local spawn_waiting = false 137 | local spawn_working = false 138 | local script_written = false 139 | 140 | local dirty = false 141 | 142 | local x = nil 143 | local y = nil 144 | local last_x = x 145 | local last_y = y 146 | 147 | local last_seek_time = nil 148 | 149 | local effective_w = options.max_width 150 | local effective_h = options.max_height 151 | local real_w = nil 152 | local real_h = nil 153 | local last_real_w = nil 154 | local last_real_h = nil 155 | 156 | local script_name = nil 157 | 158 | local show_thumbnail = false 159 | 160 | local filters_reset = {["lavfi-crop"]=true, ["crop"]=true} 161 | local filters_runtime = {["hflip"]=true, ["vflip"]=true} 162 | local filters_all = {["hflip"]=true, ["vflip"]=true, ["lavfi-crop"]=true, ["crop"]=true} 163 | 164 | local tone_mappings = {["none"]=true, ["clip"]=true, ["linear"]=true, ["gamma"]=true, ["reinhard"]=true, ["hable"]=true, ["mobius"]=true} 165 | local last_tone_mapping = nil 166 | 167 | local last_vf_reset = "" 168 | local last_vf_runtime = "" 169 | 170 | local last_rotate = 0 171 | 172 | local par = "" 173 | local last_par = "" 174 | 175 | local last_has_vid = 0 176 | local has_vid = 0 177 | 178 | local file_timer = nil 179 | local file_check_period = 1/60 180 | 181 | local allow_fast_seek = true 182 | 183 | local client_script = [=[ 184 | #!/usr/bin/env bash 185 | MPV_IPC_FD=0; MPV_IPC_PATH="%s" 186 | trap "kill 0" EXIT 187 | while [[ $# -ne 0 ]]; do case $1 in --mpv-ipc-fd=*) MPV_IPC_FD=${1/--mpv-ipc-fd=/} ;; esac; shift; done 188 | if echo "print-text thumbfast" >&"$MPV_IPC_FD"; then echo -n > "$MPV_IPC_PATH"; tail -f "$MPV_IPC_PATH" >&"$MPV_IPC_FD" & while read -r -u "$MPV_IPC_FD" 2>/dev/null; do :; done; fi 189 | ]=] 190 | 191 | local function get_os() 192 | local raw_os_name = "" 193 | 194 | if jit and jit.os and jit.arch then 195 | raw_os_name = jit.os 196 | else 197 | if package.config:sub(1,1) == "\\" then 198 | -- Windows 199 | local env_OS = os.getenv("OS") 200 | if env_OS then 201 | raw_os_name = env_OS 202 | end 203 | else 204 | raw_os_name = subprocess({"uname", "-s"}).stdout 205 | end 206 | end 207 | 208 | raw_os_name = (raw_os_name):lower() 209 | 210 | local os_patterns = { 211 | ["windows"] = "windows", 212 | ["linux"] = "linux", 213 | 214 | ["osx"] = "darwin", 215 | ["mac"] = "darwin", 216 | ["darwin"] = "darwin", 217 | 218 | ["^mingw"] = "windows", 219 | ["^cygwin"] = "windows", 220 | 221 | ["bsd$"] = "darwin", 222 | ["sunos"] = "darwin" 223 | } 224 | 225 | -- Default to linux 226 | local str_os_name = "linux" 227 | 228 | for pattern, name in pairs(os_patterns) do 229 | if raw_os_name:match(pattern) then 230 | str_os_name = name 231 | break 232 | end 233 | end 234 | 235 | return str_os_name 236 | end 237 | 238 | local os_name = mp.get_property("platform") or get_os() 239 | 240 | local path_separator = os_name == "windows" and "\\" or "/" 241 | 242 | if options.socket == "" then 243 | if os_name == "windows" then 244 | options.socket = "thumbfast" 245 | else 246 | options.socket = "/tmp/thumbfast" 247 | end 248 | end 249 | 250 | if options.thumbnail == "" then 251 | if os_name == "windows" then 252 | options.thumbnail = os.getenv("TEMP").."\\thumbfast.out" 253 | else 254 | options.thumbnail = "/tmp/thumbfast.out" 255 | end 256 | end 257 | 258 | local unique = mp.utils.getpid() 259 | 260 | options.socket = options.socket .. unique 261 | options.thumbnail = options.thumbnail .. unique 262 | 263 | if options.direct_io then 264 | if os_name == "windows" then 265 | winapi.socket_wc = winapi.MultiByteToWideChar("\\\\.\\pipe\\" .. options.socket) 266 | end 267 | 268 | if winapi.socket_wc == "" then 269 | options.direct_io = false 270 | end 271 | end 272 | 273 | local mpv_path = options.mpv_path 274 | 275 | if mpv_path == "mpv" and os_name == "darwin" and unique then 276 | -- TODO: look into ~~osxbundle/ 277 | mpv_path = string.gsub(subprocess({"ps", "-o", "comm=", "-p", tostring(unique)}).stdout, "[\n\r]", "") 278 | if mpv_path ~= "mpv" then 279 | mpv_path = string.gsub(mpv_path, "/mpv%-bundle$", "/mpv") 280 | local mpv_bin = mp.utils.file_info("/usr/local/mpv") 281 | if mpv_bin and mpv_bin.is_file then 282 | mpv_path = "/usr/local/mpv" 283 | else 284 | local mpv_app = mp.utils.file_info("/Applications/mpv.app/Contents/MacOS/mpv") 285 | if mpv_app and mpv_app.is_file then 286 | mp.msg.warn("symlink mpv to fix Dock icons: `sudo ln -s /Applications/mpv.app/Contents/MacOS/mpv /usr/local/mpv`") 287 | else 288 | mp.msg.warn("drag to your Applications folder and symlink mpv to fix Dock icons: `sudo ln -s /Applications/mpv.app/Contents/MacOS/mpv /usr/local/mpv`") 289 | end 290 | end 291 | end 292 | end 293 | 294 | local function vo_tone_mapping() 295 | local passes = mp.get_property_native("vo-passes") 296 | if passes and passes["fresh"] then 297 | for k, v in pairs(passes["fresh"]) do 298 | for k2, v2 in pairs(v) do 299 | if k2 == "desc" and v2 then 300 | local tone_mapping = string.match(v2, "([0-9a-z.-]+) tone map") 301 | if tone_mapping then 302 | return tone_mapping 303 | end 304 | end 305 | end 306 | end 307 | end 308 | end 309 | 310 | local function vf_string(filters, full) 311 | local vf = "" 312 | local vf_table = properties["vf"] 313 | 314 | if vf_table and #vf_table > 0 then 315 | for i = #vf_table, 1, -1 do 316 | if filters[vf_table[i].name] then 317 | local args = "" 318 | for key, value in pairs(vf_table[i].params) do 319 | if args ~= "" then 320 | args = args .. ":" 321 | end 322 | args = args .. key .. "=" .. value 323 | end 324 | vf = vf .. vf_table[i].name .. "=" .. args .. "," 325 | end 326 | end 327 | end 328 | 329 | if (full and options.tone_mapping ~= "no") or options.tone_mapping == "auto" then 330 | if properties["video-params"] and properties["video-params"]["primaries"] == "bt.2020" then 331 | local tone_mapping = options.tone_mapping 332 | if tone_mapping == "auto" then 333 | tone_mapping = last_tone_mapping or properties["tone-mapping"] 334 | if tone_mapping == "auto" and properties["current-vo"] == "gpu-next" then 335 | tone_mapping = vo_tone_mapping() 336 | end 337 | end 338 | if not tone_mappings[tone_mapping] then 339 | tone_mapping = "hable" 340 | end 341 | last_tone_mapping = tone_mapping 342 | vf = vf .. "zscale=transfer=linear,format=gbrpf32le,tonemap="..tone_mapping..",zscale=transfer=bt709," 343 | end 344 | end 345 | 346 | if full then 347 | vf = vf.."scale=w="..effective_w..":h="..effective_h..par..",pad=w="..effective_w..":h="..effective_h..":x=-1:y=-1,format=bgra" 348 | end 349 | 350 | return vf 351 | end 352 | 353 | local function calc_dimensions() 354 | local width = properties["video-out-params"] and properties["video-out-params"]["dw"] 355 | local height = properties["video-out-params"] and properties["video-out-params"]["dh"] 356 | if not width or not height then return end 357 | 358 | local scale = properties["display-hidpi-scale"] or 1 359 | 360 | if width / height > options.max_width / options.max_height then 361 | effective_w = math.floor(options.max_width * scale + 0.5) 362 | effective_h = math.floor(height / width * effective_w + 0.5) 363 | else 364 | effective_h = math.floor(options.max_height * scale + 0.5) 365 | effective_w = math.floor(width / height * effective_h + 0.5) 366 | end 367 | 368 | local v_par = properties["video-out-params"] and properties["video-out-params"]["par"] or 1 369 | if v_par == 1 then 370 | par = ":force_original_aspect_ratio=decrease" 371 | else 372 | par = "" 373 | end 374 | end 375 | 376 | local info_timer = nil 377 | 378 | local function info(w, h) 379 | local rotate = properties["video-params"] and properties["video-params"]["rotate"] 380 | local image = properties["current-tracks/video"] and properties["current-tracks/video"]["image"] 381 | local albumart = image and properties["current-tracks/video"]["albumart"] 382 | 383 | disabled = (w or 0) == 0 or (h or 0) == 0 or 384 | has_vid == 0 or 385 | (properties["demuxer-via-network"] and not options.network) or 386 | (albumart and not options.audio) or 387 | (image and not albumart) or 388 | force_disabled 389 | 390 | if info_timer then 391 | info_timer:kill() 392 | info_timer = nil 393 | elseif has_vid == 0 or (rotate == nil and not disabled) then 394 | info_timer = mp.add_timeout(0.05, function() info(w, h) end) 395 | end 396 | 397 | local json, err = mp.utils.format_json({width=w, height=h, disabled=disabled, available=true, socket=options.socket, thumbnail=options.thumbnail, overlay_id=options.overlay_id}) 398 | if pre_0_30_0 then 399 | mp.command_native({"script-message", "thumbfast-info", json}) 400 | else 401 | mp.command_native_async({"script-message", "thumbfast-info", json}, function() end) 402 | end 403 | end 404 | 405 | local function remove_thumbnail_files() 406 | if file then 407 | file:close() 408 | file = nil 409 | file_bytes = 0 410 | end 411 | os.remove(options.thumbnail) 412 | os.remove(options.thumbnail..".bgra") 413 | end 414 | 415 | local activity_timer 416 | 417 | local function spawn(time) 418 | if disabled then return end 419 | 420 | local path = properties["path"] 421 | if path == nil then return end 422 | 423 | if options.quit_after_inactivity > 0 then 424 | if show_thumbnail or activity_timer:is_enabled() then 425 | activity_timer:kill() 426 | end 427 | activity_timer:resume() 428 | end 429 | 430 | local open_filename = properties["stream-open-filename"] 431 | local ytdl = open_filename and properties["demuxer-via-network"] and path ~= open_filename 432 | if ytdl then 433 | path = open_filename 434 | end 435 | 436 | remove_thumbnail_files() 437 | 438 | local vid = properties["vid"] 439 | has_vid = vid or 0 440 | 441 | local args = { 442 | mpv_path, "--no-config", "--msg-level=all=no", "--idle", "--pause", "--keep-open=always", "--really-quiet", "--no-terminal", 443 | "--load-scripts=no", "--osc=no", "--ytdl=no", "--load-stats-overlay=no", "--load-osd-console=no", "--load-auto-profiles=no", 444 | "--edition="..(properties["edition"] or "auto"), "--vid="..(vid or "auto"), "--no-sub", "--no-audio", 445 | "--start="..time, allow_fast_seek and "--hr-seek=no" or "--hr-seek=yes", 446 | "--ytdl-format=worst", "--demuxer-readahead-secs=0", "--demuxer-max-bytes=128KiB", 447 | "--vd-lavc-skiploopfilter=all", "--vd-lavc-software-fallback=1", "--vd-lavc-fast", "--vd-lavc-threads=2", "--hwdec="..(options.hwdec and "auto" or "no"), 448 | "--vf="..vf_string(filters_all, true), 449 | "--sws-scaler=fast-bilinear", 450 | "--video-rotate="..last_rotate, 451 | "--ovc=rawvideo", "--of=image2", "--ofopts=update=1", "--o="..options.thumbnail 452 | } 453 | 454 | if not pre_0_30_0 then 455 | table.insert(args, "--sws-allow-zimg=no") 456 | end 457 | 458 | if os_name == "darwin" and properties["macos-app-activation-policy"] then 459 | table.insert(args, "--macos-app-activation-policy=accessory") 460 | end 461 | 462 | if os_name == "windows" or pre_0_33_0 then 463 | table.insert(args, "--input-ipc-server="..options.socket) 464 | elseif not script_written then 465 | local client_script_path = options.socket..".run" 466 | local script = io.open(client_script_path, "w+") 467 | if script == nil then 468 | mp.msg.error("client script write failed") 469 | return 470 | else 471 | script_written = true 472 | script:write(string.format(client_script, options.socket)) 473 | script:close() 474 | subprocess({"chmod", "+x", client_script_path}, true) 475 | table.insert(args, "--scripts="..client_script_path) 476 | end 477 | else 478 | local client_script_path = options.socket..".run" 479 | table.insert(args, "--scripts="..client_script_path) 480 | end 481 | 482 | table.insert(args, "--") 483 | table.insert(args, path) 484 | 485 | spawned = true 486 | spawn_waiting = true 487 | 488 | subprocess(args, true, 489 | function(success, result) 490 | if spawn_waiting and (success == false or (result.status ~= 0 and result.status ~= -2)) then 491 | spawned = false 492 | spawn_waiting = false 493 | options.tone_mapping = "no" 494 | mp.msg.error("mpv subprocess create failed") 495 | if not spawn_working then -- notify users of required configuration 496 | if options.mpv_path == "mpv" then 497 | if properties["current-vo"] == "libmpv" then 498 | if options.mpv_path == mpv_path then -- attempt to locate ImPlay 499 | mpv_path = "ImPlay" 500 | spawn(time) 501 | else -- ImPlay not in path 502 | if os_name ~= "darwin" then 503 | force_disabled = true 504 | info(real_w or effective_w, real_h or effective_h) 505 | end 506 | mp.commandv("show-text", "thumbfast: ERROR! cannot create mpv subprocess", 5000) 507 | mp.commandv("script-message-to", "implay", "show-message", "thumbfast initial setup", "Set mpv_path=PATH_TO_ImPlay in thumbfast config:\n" .. string.gsub(mp.command_native({"expand-path", "~~/script-opts/thumbfast.conf"}), "[/\\]", path_separator).."\nand restart ImPlay") 508 | end 509 | else 510 | mp.commandv("show-text", "thumbfast: ERROR! cannot create mpv subprocess", 5000) 511 | if os_name == "windows" then 512 | mp.commandv("script-message-to", "mpvnet", "show-text", "thumbfast: ERROR! install standalone mpv, see README", 5000, 20) 513 | mp.commandv("script-message", "mpv.net", "show-text", "thumbfast: ERROR! install standalone mpv, see README", 5000, 20) 514 | end 515 | end 516 | else 517 | mp.commandv("show-text", "thumbfast: ERROR! cannot create mpv subprocess", 5000) 518 | -- found ImPlay but not defined in config 519 | mp.commandv("script-message-to", "implay", "show-message", "thumbfast", "Set mpv_path=PATH_TO_ImPlay in thumbfast config:\n" .. string.gsub(mp.command_native({"expand-path", "~~/script-opts/thumbfast.conf"}), "[/\\]", path_separator).."\nand restart ImPlay") 520 | end 521 | end 522 | elseif success == true and (result.status == 0 or result.status == -2) then 523 | if not spawn_working and properties["current-vo"] == "libmpv" and options.mpv_path ~= mpv_path then 524 | mp.commandv("script-message-to", "implay", "show-message", "thumbfast initial setup", "Set mpv_path=ImPlay in thumbfast config:\n" .. string.gsub(mp.command_native({"expand-path", "~~/script-opts/thumbfast.conf"}), "[/\\]", path_separator).."\nand restart ImPlay") 525 | end 526 | spawn_working = true 527 | spawn_waiting = false 528 | end 529 | end 530 | ) 531 | end 532 | 533 | local function run(command) 534 | if not spawned then return end 535 | 536 | if options.direct_io then 537 | local hPipe = winapi.C.CreateFileW(winapi.socket_wc, winapi.GENERIC_WRITE, 0, nil, winapi.OPEN_EXISTING, winapi._createfile_pipe_flags, nil) 538 | if hPipe ~= winapi.INVALID_HANDLE_VALUE then 539 | local buf = command .. "\n" 540 | winapi.C.SetNamedPipeHandleState(hPipe, winapi.PIPE_NOWAIT, nil, nil) 541 | winapi.C.WriteFile(hPipe, buf, #buf + 1, winapi._lpNumberOfBytesWritten, nil) 542 | winapi.C.CloseHandle(hPipe) 543 | end 544 | 545 | return 546 | end 547 | 548 | local command_n = command.."\n" 549 | 550 | if os_name == "windows" then 551 | if file and file_bytes + #command_n >= 4096 then 552 | file:close() 553 | file = nil 554 | file_bytes = 0 555 | end 556 | if not file then 557 | file = io.open("\\\\.\\pipe\\"..options.socket, "r+b") 558 | end 559 | elseif pre_0_33_0 then 560 | subprocess({"/usr/bin/env", "sh", "-c", "echo '" .. command .. "' | socat - " .. options.socket}) 561 | return 562 | elseif not file then 563 | file = io.open(options.socket, "r+") 564 | end 565 | if file then 566 | file_bytes = file:seek("end") 567 | file:write(command_n) 568 | file:flush() 569 | end 570 | end 571 | 572 | local function draw(w, h, script) 573 | if not w or not show_thumbnail then return end 574 | if x ~= nil then 575 | if pre_0_30_0 then 576 | mp.command_native({"overlay-add", options.overlay_id, x, y, options.thumbnail..".bgra", 0, "bgra", w, h, (4*w)}) 577 | else 578 | mp.command_native_async({"overlay-add", options.overlay_id, x, y, options.thumbnail..".bgra", 0, "bgra", w, h, (4*w)}, function() end) 579 | end 580 | elseif script then 581 | local json, err = mp.utils.format_json({width=w, height=h, x=x, y=y, socket=options.socket, thumbnail=options.thumbnail, overlay_id=options.overlay_id}) 582 | mp.commandv("script-message-to", script, "thumbfast-render", json) 583 | end 584 | end 585 | 586 | local function real_res(req_w, req_h, filesize) 587 | local count = filesize / 4 588 | local diff = (req_w * req_h) - count 589 | 590 | if (properties["video-params"] and properties["video-params"]["rotate"] or 0) % 180 == 90 then 591 | req_w, req_h = req_h, req_w 592 | end 593 | 594 | if diff == 0 then 595 | return req_w, req_h 596 | else 597 | local threshold = 5 -- throw out results that change too much 598 | local long_side, short_side = req_w, req_h 599 | if req_h > req_w then 600 | long_side, short_side = req_h, req_w 601 | end 602 | for a = short_side, short_side - threshold, -1 do 603 | if count % a == 0 then 604 | local b = count / a 605 | if long_side - b < threshold then 606 | if req_h < req_w then return b, a else return a, b end 607 | end 608 | end 609 | end 610 | return nil 611 | end 612 | end 613 | 614 | local function move_file(from, to) 615 | if os_name == "windows" then 616 | os.remove(to) 617 | end 618 | -- move the file because it can get overwritten while overlay-add is reading it, and crash the player 619 | os.rename(from, to) 620 | end 621 | 622 | local function seek(fast) 623 | if last_seek_time then 624 | run("async seek " .. last_seek_time .. (fast and " absolute+keyframes" or " absolute+exact")) 625 | end 626 | end 627 | 628 | local seek_period = 3/60 629 | local seek_period_counter = 0 630 | local seek_timer 631 | seek_timer = mp.add_periodic_timer(seek_period, function() 632 | if seek_period_counter == 0 then 633 | seek(allow_fast_seek) 634 | seek_period_counter = 1 635 | else 636 | if seek_period_counter == 2 then 637 | if allow_fast_seek then 638 | seek_timer:kill() 639 | seek() 640 | end 641 | else seek_period_counter = seek_period_counter + 1 end 642 | end 643 | end) 644 | seek_timer:kill() 645 | 646 | local function request_seek() 647 | if seek_timer:is_enabled() then 648 | seek_period_counter = 0 649 | else 650 | seek_timer:resume() 651 | seek(allow_fast_seek) 652 | seek_period_counter = 1 653 | end 654 | end 655 | 656 | local function check_new_thumb() 657 | -- the slave might start writing to the file after checking existance and 658 | -- validity but before actually moving the file, so move to a temporary 659 | -- location before validity check to make sure everything stays consistant 660 | -- and valid thumbnails don't get overwritten by invalid ones 661 | local tmp = options.thumbnail..".tmp" 662 | move_file(options.thumbnail, tmp) 663 | local finfo = mp.utils.file_info(tmp) 664 | if not finfo then return false end 665 | spawn_waiting = false 666 | local w, h = real_res(effective_w, effective_h, finfo.size) 667 | if w then -- only accept valid thumbnails 668 | move_file(tmp, options.thumbnail..".bgra") 669 | 670 | real_w, real_h = w, h 671 | if real_w and (real_w ~= last_real_w or real_h ~= last_real_h) then 672 | last_real_w, last_real_h = real_w, real_h 673 | info(real_w, real_h) 674 | end 675 | if not show_thumbnail then 676 | file_timer:kill() 677 | end 678 | return true 679 | end 680 | 681 | return false 682 | end 683 | 684 | file_timer = mp.add_periodic_timer(file_check_period, function() 685 | if check_new_thumb() then 686 | draw(real_w, real_h, script_name) 687 | end 688 | end) 689 | file_timer:kill() 690 | 691 | local function clear() 692 | file_timer:kill() 693 | seek_timer:kill() 694 | if options.quit_after_inactivity > 0 then 695 | if show_thumbnail or activity_timer:is_enabled() then 696 | activity_timer:kill() 697 | end 698 | activity_timer:resume() 699 | end 700 | last_seek_time = nil 701 | show_thumbnail = false 702 | last_x = nil 703 | last_y = nil 704 | if script_name then return end 705 | if pre_0_30_0 then 706 | mp.command_native({"overlay-remove", options.overlay_id}) 707 | else 708 | mp.command_native_async({"overlay-remove", options.overlay_id}, function() end) 709 | end 710 | end 711 | 712 | local function quit() 713 | activity_timer:kill() 714 | if show_thumbnail then 715 | activity_timer:resume() 716 | return 717 | end 718 | run("quit") 719 | spawned = false 720 | real_w, real_h = nil, nil 721 | clear() 722 | end 723 | 724 | activity_timer = mp.add_timeout(options.quit_after_inactivity, quit) 725 | activity_timer:kill() 726 | 727 | local function thumb(time, r_x, r_y, script) 728 | if disabled then return end 729 | 730 | time = tonumber(time) 731 | if time == nil then return end 732 | 733 | if r_x == "" or r_y == "" then 734 | x, y = nil, nil 735 | else 736 | x, y = math.floor(r_x + 0.5), math.floor(r_y + 0.5) 737 | end 738 | 739 | script_name = script 740 | if last_x ~= x or last_y ~= y or not show_thumbnail then 741 | show_thumbnail = true 742 | last_x = x 743 | last_y = y 744 | draw(real_w, real_h, script) 745 | end 746 | 747 | if options.quit_after_inactivity > 0 then 748 | if show_thumbnail or activity_timer:is_enabled() then 749 | activity_timer:kill() 750 | end 751 | activity_timer:resume() 752 | end 753 | 754 | if time == last_seek_time then return end 755 | last_seek_time = time 756 | if not spawned then spawn(time) end 757 | request_seek() 758 | if not file_timer:is_enabled() then file_timer:resume() end 759 | end 760 | 761 | local function watch_changes() 762 | if not dirty or not properties["video-out-params"] then return end 763 | dirty = false 764 | 765 | local old_w = effective_w 766 | local old_h = effective_h 767 | 768 | calc_dimensions() 769 | 770 | local vf_reset = vf_string(filters_reset) 771 | local rotate = properties["video-rotate"] or 0 772 | 773 | local resized = old_w ~= effective_w or 774 | old_h ~= effective_h or 775 | last_vf_reset ~= vf_reset or 776 | (last_rotate % 180) ~= (rotate % 180) or 777 | par ~= last_par 778 | 779 | if resized then 780 | last_rotate = rotate 781 | info(effective_w, effective_h) 782 | elseif last_has_vid ~= has_vid and has_vid ~= 0 then 783 | info(effective_w, effective_h) 784 | end 785 | 786 | if spawned then 787 | if resized then 788 | -- mpv doesn't allow us to change output size 789 | local seek_time = last_seek_time 790 | run("quit") 791 | clear() 792 | spawned = false 793 | spawn(seek_time or mp.get_property_number("time-pos", 0)) 794 | file_timer:resume() 795 | else 796 | if rotate ~= last_rotate then 797 | run("set video-rotate "..rotate) 798 | end 799 | local vf_runtime = vf_string(filters_runtime) 800 | if vf_runtime ~= last_vf_runtime then 801 | run("vf set "..vf_string(filters_all, true)) 802 | last_vf_runtime = vf_runtime 803 | end 804 | end 805 | else 806 | last_vf_runtime = vf_string(filters_runtime) 807 | end 808 | 809 | last_vf_reset = vf_reset 810 | last_rotate = rotate 811 | last_par = par 812 | last_has_vid = has_vid 813 | 814 | if not spawned and not disabled and options.spawn_first and resized then 815 | spawn(mp.get_property_number("time-pos", 0)) 816 | file_timer:resume() 817 | end 818 | end 819 | 820 | local function update_property(name, value) 821 | properties[name] = value 822 | end 823 | 824 | local function update_property_dirty(name, value) 825 | properties[name] = value 826 | dirty = true 827 | if name == "tone-mapping" then 828 | last_tone_mapping = nil 829 | end 830 | end 831 | 832 | local function update_tracklist(name, value) 833 | -- current-tracks shim 834 | for _, track in ipairs(value) do 835 | if track.type == "video" and track.selected then 836 | properties["current-tracks/video"] = track 837 | return 838 | end 839 | end 840 | end 841 | 842 | local function sync_changes(prop, val) 843 | update_property(prop, val) 844 | if val == nil then return end 845 | 846 | if type(val) == "boolean" then 847 | if prop == "vid" then 848 | has_vid = 0 849 | last_has_vid = 0 850 | info(effective_w, effective_h) 851 | clear() 852 | return 853 | end 854 | val = val and "yes" or "no" 855 | end 856 | 857 | if prop == "vid" then 858 | has_vid = 1 859 | end 860 | 861 | if not spawned then return end 862 | 863 | run("set "..prop.." "..val) 864 | dirty = true 865 | end 866 | 867 | local function file_load() 868 | clear() 869 | spawned = false 870 | real_w, real_h = nil, nil 871 | last_real_w, last_real_h = nil, nil 872 | last_tone_mapping = nil 873 | last_seek_time = nil 874 | if info_timer then 875 | info_timer:kill() 876 | info_timer = nil 877 | end 878 | 879 | calc_dimensions() 880 | info(effective_w, effective_h) 881 | end 882 | 883 | local function shutdown() 884 | run("quit") 885 | remove_thumbnail_files() 886 | if os_name ~= "windows" then 887 | os.remove(options.socket) 888 | os.remove(options.socket..".run") 889 | end 890 | end 891 | 892 | local function on_duration(prop, val) 893 | allow_fast_seek = (val or 30) >= 30 894 | end 895 | 896 | mp.observe_property("current-tracks/video", "native", function(name, value) 897 | if pre_0_33_0 then 898 | mp.unobserve_property(update_tracklist) 899 | pre_0_33_0 = false 900 | end 901 | update_property(name, value) 902 | end) 903 | 904 | mp.observe_property("track-list", "native", update_tracklist) 905 | mp.observe_property("display-hidpi-scale", "native", update_property_dirty) 906 | mp.observe_property("video-out-params", "native", update_property_dirty) 907 | mp.observe_property("video-params", "native", update_property_dirty) 908 | mp.observe_property("vf", "native", update_property_dirty) 909 | mp.observe_property("tone-mapping", "native", update_property_dirty) 910 | mp.observe_property("demuxer-via-network", "native", update_property) 911 | mp.observe_property("stream-open-filename", "native", update_property) 912 | mp.observe_property("macos-app-activation-policy", "native", update_property) 913 | mp.observe_property("current-vo", "native", update_property) 914 | mp.observe_property("video-rotate", "native", update_property) 915 | mp.observe_property("path", "native", update_property) 916 | mp.observe_property("vid", "native", sync_changes) 917 | mp.observe_property("edition", "native", sync_changes) 918 | mp.observe_property("duration", "native", on_duration) 919 | 920 | mp.register_script_message("thumb", thumb) 921 | mp.register_script_message("clear", clear) 922 | 923 | mp.register_event("file-loaded", file_load) 924 | mp.register_event("shutdown", shutdown) 925 | 926 | mp.register_idle(watch_changes) 927 | -------------------------------------------------------------------------------- /scripts/playlistmanager.lua: -------------------------------------------------------------------------------- 1 | local settings = { 2 | 3 | -- #### FUNCTIONALITY SETTINGS 4 | 5 | --navigation keybindings force override only while playlist is visible 6 | --if "no" then you can display the playlist by any of the navigation keys 7 | dynamic_binds = true, 8 | 9 | -- to bind multiple keys separate them by a space 10 | 11 | -- main key to show playlist 12 | key_showplaylist = "SHIFT+ENTER", 13 | 14 | -- display playlist while key is held down 15 | key_peek_at_playlist = "", 16 | 17 | -- dynamic keys 18 | key_moveup = "UP", 19 | key_movedown = "DOWN", 20 | key_movepageup = "PGUP", 21 | key_movepagedown = "PGDWN", 22 | key_movebegin = "HOME", 23 | key_moveend = "END", 24 | key_selectfile = "RIGHT LEFT", 25 | key_unselectfile = "", 26 | key_playfile = "ENTER", 27 | key_removefile = "BS", 28 | key_closeplaylist = "ESC SHIFT+ENTER", 29 | 30 | -- extra functionality keys 31 | key_sortplaylist = "", 32 | key_shuffleplaylist = "", 33 | key_reverseplaylist = "", 34 | key_loadfiles = "", 35 | key_saveplaylist = "", 36 | 37 | --replaces matches on filenames based on extension, put as empty string to not replace anything 38 | --replace rules are executed in provided order 39 | --replace rule key is the pattern and value is the replace value 40 | --uses :gsub('pattern', 'replace'), read more http://lua-users.org/wiki/StringLibraryTutorial 41 | --'all' will match any extension or protocol if it has one 42 | --uses json and parses it into a lua table to be able to support .conf file 43 | 44 | filename_replace = [[ 45 | [ 46 | { 47 | "protocol": { "all": true }, 48 | "rules": [ 49 | { "%%(%x%x)": "hex_to_char" } 50 | ] 51 | } 52 | ] 53 | ]], 54 | 55 | --[=====[ START OF SAMPLE REPLACE - Remove this line to use it 56 | --Sample replace: replaces underscore to space on all files 57 | --for mp4 and webm; remove extension, remove brackets and surrounding whitespace, change dot between alphanumeric to space 58 | filename_replace = [[ 59 | [ 60 | { 61 | "ext": { "all": true}, 62 | "rules": [ 63 | { "_" : " " } 64 | ] 65 | },{ 66 | "ext": { "mp4": true, "mkv": true }, 67 | "rules": [ 68 | { "^(.+)%..+$": "%1" }, 69 | { "%s*[%[%(].-[%]%)]%s*": "" }, 70 | { "(%w)%.(%w)": "%1 %2" } 71 | ] 72 | },{ 73 | "protocol": { "http": true, "https": true }, 74 | "rules": [ 75 | { "^%a+://w*%.?": "" } 76 | ] 77 | } 78 | ] 79 | ]], 80 | --END OF SAMPLE REPLACE ]=====] 81 | 82 | --json array of filetypes to search from directory 83 | loadfiles_filetypes = [[ 84 | [ 85 | "jpg", "jpeg", "png", "tif", "tiff", "gif", "webp", "svg", "bmp", 86 | "mp3", "wav", "ogm", "flac", "m4a", "wma", "ogg", "opus", 87 | "mkv", "avi", "mp4", "ogv", "webm", "rmvb", "flv", "wmv", "mpeg", "mpg", "m4v", "3gp" 88 | ] 89 | ]], 90 | 91 | --loadfiles at startup if 1 or more items in playlist 92 | loadfiles_on_start = false, 93 | -- loadfiles from working directory on idle startup 94 | loadfiles_on_idle_start = false, 95 | --always put loaded files after currently playing file 96 | loadfiles_always_append = false, 97 | 98 | --sort playlist when files are added to playlist 99 | sortplaylist_on_file_add = false, 100 | 101 | --default sorting method, must be one of: "name-asc", "name-desc", "date-asc", "date-desc", "size-asc", "size-desc". 102 | default_sort = "name-asc", 103 | 104 | --"linux | windows | auto" 105 | system = "auto", 106 | 107 | --Use ~ for home directory. Leave as empty to use mpv/playlists 108 | playlist_savepath = "", 109 | 110 | -- constant filename to save playlist as. Note that it will override existing playlist. Leave empty for generated name. 111 | playlist_save_filename = "", 112 | 113 | --save playlist automatically after current file was unloaded 114 | save_playlist_on_file_end = false, 115 | 116 | 117 | --show playlist or filename every time a new file is loaded 118 | --2 shows playlist, 1 shows current file(filename strip applied) as osd text, 0 shows nothing 119 | --instead of using this you can also call script-message playlistmanager show playlist/filename 120 | --ex. KEY playlist-next ; script-message playlistmanager show playlist 121 | show_playlist_on_fileload = 0, 122 | 123 | --sync cursor when file is loaded from outside reasons(file-ending, playlist-next shortcut etc.) 124 | --has the sideeffect of moving cursor if file happens to change when navigating 125 | --good side is cursor always following current file when going back and forth files with playlist-next/prev 126 | sync_cursor_on_load = true, 127 | 128 | --allow the playlist cursor to loop from end to start and vice versa 129 | loop_cursor = true, 130 | 131 | --youtube-dl executable for title resolving if enabled, probably "youtube-dl" or "yt-dlp", can be absolute path 132 | youtube_dl_executable = "youtube-dl", 133 | 134 | -- allow playlistmanager to write watch later config when navigating between files 135 | allow_write_watch_later_config = true, 136 | 137 | -- reset cursor navigation when closing or opening playlist 138 | reset_cursor_on_close = true, 139 | reset_cursor_on_open = true, 140 | 141 | --#### VISUAL SETTINGS 142 | 143 | --prefer to display titles for following files: "all", "url", "none". Sorting still uses filename. 144 | prefer_titles = "url", 145 | 146 | --call youtube-dl to resolve the titles of urls in the playlist 147 | resolve_url_titles = false, 148 | 149 | --call ffprobe to resolve the titles of local files in the playlist (if they exist in the metadata) 150 | resolve_local_titles = false, 151 | 152 | -- timeout in seconds for url title resolving 153 | resolve_title_timeout = 15, 154 | 155 | -- how many url titles can be resolved at a time. Higher number might lead to stutters. 156 | concurrent_title_resolve_limit = 10, 157 | 158 | --osd timeout on inactivity in seconds, use 0 for no timeout 159 | playlist_display_timeout = 0, 160 | 161 | -- when peeking at playlist, show playlist at the very least for display timeout 162 | peek_respect_display_timeout = false, 163 | 164 | -- the maximum amount of lines playlist will render. Optimal value depends on font/video size etc. 165 | showamount = 9, 166 | 167 | --font size scales by window, if false requires larger font and padding sizes 168 | scale_playlist_by_window=true, 169 | --playlist ass style overrides inside curly brackets, \keyvalue is one field, extra \ for escape in lua 170 | --example {\\fnUbuntu\\fs10\\b0\\bord1} equals: font=Ubuntu, size=10, bold=no, border=1 171 | --read http://docs.aegisub.org/3.2/ASS_Tags/ for reference of tags 172 | --undeclared tags will use default osd settings 173 | --these styles will be used for the whole playlist 174 | style_ass_tags = "{}", 175 | --paddings from top left corner 176 | text_padding_x = 10, 177 | text_padding_y = 30, 178 | 179 | --screen dim when menu is open 0.0 - 1.0 (0 is no dim, 1 is black) 180 | curtain_opacity=0, 181 | 182 | --set title of window with stripped name 183 | set_title_stripped = false, 184 | title_prefix = "", 185 | title_suffix = " - mpv", 186 | 187 | --slice long filenames, and how many chars to show 188 | slice_longfilenames = false, 189 | slice_longfilenames_amount = 70, 190 | 191 | --Playlist header template 192 | --%mediatitle or %filename = title or name of playing file 193 | --%pos = position of playing file 194 | --%cursor = position of navigation 195 | --%plen = playlist length 196 | --%N = newline 197 | playlist_header = "[%cursor/%plen]", 198 | 199 | --Playlist file templates 200 | --%pos = position of file with leading zeros 201 | --%name = title or name of file 202 | --%N = newline 203 | --you can also use the ass tags mentioned above. For example: 204 | -- selected_file="{\\c&HFF00FF&}➔ %name" | to add a color for selected file. However, if you 205 | -- use ass tags you need to reset them for every line (see https://github.com/jonniek/mpv-playlistmanager/issues/20) 206 | normal_file = "○ %name", 207 | hovered_file = "● %name", 208 | selected_file = "➔ %name", 209 | playing_file = "▷ %name", 210 | playing_hovered_file = "▶ %name", 211 | playing_selected_file = "➤ %name", 212 | 213 | 214 | -- what to show when playlist is truncated 215 | playlist_sliced_prefix = "...", 216 | playlist_sliced_suffix = "...", 217 | 218 | --output visual feedback to OSD for tasks 219 | display_osd_feedback = true, 220 | } 221 | local opts = require("mp.options") 222 | opts.read_options(settings, "playlistmanager", function(list) update_opts(list) end) 223 | 224 | local utils = require("mp.utils") 225 | local msg = require("mp.msg") 226 | local assdraw = require("mp.assdraw") 227 | 228 | 229 | --check os 230 | if settings.system=="auto" then 231 | local o = {} 232 | if mp.get_property_native('options/vo-mmcss-profile', o) ~= o then 233 | settings.system = "windows" 234 | else 235 | settings.system = "linux" 236 | end 237 | end 238 | 239 | --global variables 240 | local playlist_visible = false 241 | local strippedname = nil 242 | local path = nil 243 | local directory = nil 244 | local filename = nil 245 | local pos = 0 246 | local plen = 0 247 | local cursor = 0 248 | --table for saved media titles for later if we prefer them 249 | local title_table = {} 250 | -- table for urls and local file paths that we have requested to be resolved to titles 251 | local requested_titles = {} 252 | 253 | local filetype_lookup = {} 254 | 255 | function update_opts(changelog) 256 | msg.verbose('updating options') 257 | 258 | --parse filename json 259 | if changelog.filename_replace then 260 | if(settings.filename_replace~="") then 261 | settings.filename_replace = utils.parse_json(settings.filename_replace) 262 | else 263 | settings.filename_replace = false 264 | end 265 | end 266 | 267 | --parse loadfiles json 268 | if changelog.loadfiles_filetypes then 269 | settings.loadfiles_filetypes = utils.parse_json(settings.loadfiles_filetypes) 270 | 271 | filetype_lookup = {} 272 | --create loadfiles set 273 | for _, ext in ipairs(settings.loadfiles_filetypes) do 274 | filetype_lookup[ext] = true 275 | end 276 | end 277 | 278 | if changelog.resolve_url_titles then 279 | resolve_titles() 280 | end 281 | 282 | if changelog.resolve_local_titles then 283 | resolve_titles() 284 | end 285 | 286 | if changelog.playlist_display_timeout then 287 | keybindstimer = mp.add_periodic_timer(settings.playlist_display_timeout, remove_keybinds) 288 | keybindstimer:kill() 289 | end 290 | 291 | if playlist_visible then showplaylist() end 292 | end 293 | 294 | update_opts({filename_replace = true, loadfiles_filetypes = true}) 295 | 296 | local sort_modes = { 297 | { 298 | id="name-asc", 299 | title="name ascending", 300 | sort_fn=function (a, b, playlist) 301 | return alphanumsort(playlist[a].string, playlist[b].string) 302 | end, 303 | }, 304 | { 305 | id="name-desc", 306 | title="name descending", 307 | sort_fn=function (a, b, playlist) 308 | return alphanumsort(playlist[b].string, playlist[a].string) 309 | end, 310 | }, 311 | { 312 | id="date-asc", 313 | title="date ascending", 314 | sort_fn=function (a, b) 315 | return (get_file_info(a).mtime or 0) < (get_file_info(b).mtime or 0) 316 | end, 317 | }, 318 | { 319 | id="date-desc", 320 | title="date descending", 321 | sort_fn=function (a, b) 322 | return (get_file_info(a).mtime or 0) > (get_file_info(b).mtime or 0) 323 | end, 324 | }, 325 | { 326 | id="size-asc", 327 | title="size ascending", 328 | sort_fn=function (a, b) 329 | return (get_file_info(a).size or 0) < (get_file_info(b).size or 0) 330 | end, 331 | }, 332 | { 333 | id="size-desc", 334 | title="size descending", 335 | sort_fn=function (a, b) 336 | return (get_file_info(a).size or 0) > (get_file_info(b).size or 0) 337 | end, 338 | }, 339 | } 340 | 341 | local sort_mode = 1 342 | for mode, sort_data in pairs(sort_modes) do 343 | if sort_data.id == settings.default_sort then 344 | sort_mode = mode 345 | end 346 | end 347 | 348 | function is_protocol(path) 349 | return type(path) == 'string' and path:match('^%a[%a%d-_]+://') ~= nil 350 | end 351 | 352 | function on_file_loaded() 353 | refresh_globals() 354 | filename = mp.get_property("filename") 355 | path = mp.get_property('path') 356 | local media_title = mp.get_property("media-title") 357 | if is_protocol(path) and not title_table[path] and path ~= media_title then 358 | title_table[path] = media_title 359 | end 360 | 361 | if settings.sync_cursor_on_load then 362 | cursor=pos 363 | --refresh playlist if cursor moved 364 | if playlist_visible then draw_playlist() end 365 | end 366 | 367 | strippedname = stripfilename(mp.get_property('media-title')) 368 | if settings.show_playlist_on_fileload == 2 then 369 | showplaylist() 370 | elseif settings.show_playlist_on_fileload == 1 then 371 | mp.commandv('show-text', strippedname) 372 | end 373 | if settings.set_title_stripped then 374 | mp.set_property("title", settings.title_prefix..strippedname..settings.title_suffix) 375 | end 376 | end 377 | 378 | function on_start_file() 379 | refresh_globals() 380 | filename = mp.get_property("filename") 381 | path = mp.get_property('path') 382 | --if not a url then join path with working directory 383 | if not is_protocol(path) then 384 | path = utils.join_path(mp.get_property('working-directory'), path) 385 | directory = utils.split_path(path) 386 | else 387 | directory = nil 388 | end 389 | 390 | if settings.loadfiles_on_start and plen == 1 then 391 | local ext = filename:match("%.([^%.]+)$") 392 | -- a directory or playlist has been loaded, let's not do anything as mpv will expand it into files 393 | if ext and filetype_lookup[ext:lower()] then 394 | msg.info("Loading files from playing files directory") 395 | playlist() 396 | end 397 | end 398 | end 399 | 400 | function on_end_file() 401 | if settings.save_playlist_on_file_end then save_playlist() end 402 | strippedname = nil 403 | path = nil 404 | directory = nil 405 | filename = nil 406 | if playlist_visible then showplaylist() end 407 | end 408 | 409 | function refresh_globals() 410 | pos = mp.get_property_number('playlist-pos', 0) 411 | plen = mp.get_property_number('playlist-count', 0) 412 | end 413 | 414 | function escapepath(dir, escapechar) 415 | return string.gsub(dir, escapechar, '\\'..escapechar) 416 | end 417 | 418 | function replace_table_has_value(value, valid_values) 419 | if value == nil or valid_values == nil then 420 | return false 421 | end 422 | return valid_values['all'] or valid_values[value] 423 | end 424 | 425 | local filename_replace_functions = { 426 | --decode special characters in url 427 | hex_to_char = function(x) return string.char(tonumber(x, 16)) end 428 | } 429 | 430 | --strip a filename based on its extension or protocol according to rules in settings 431 | function stripfilename(pathfile, media_title) 432 | if pathfile == nil then return '' end 433 | local ext = pathfile:match("%.([^%.]+)$") 434 | local protocol = pathfile:match("^(%a%a+)://") 435 | if not ext then ext = "" end 436 | local tmp = pathfile 437 | if settings.filename_replace and not media_title then 438 | for k,v in ipairs(settings.filename_replace) do 439 | if replace_table_has_value(ext, v['ext']) or replace_table_has_value(protocol, v['protocol']) then 440 | for ruleindex, indexrules in ipairs(v['rules']) do 441 | for rule, override in pairs(indexrules) do 442 | override = filename_replace_functions[override] or override 443 | tmp = tmp:gsub(rule, override) 444 | end 445 | end 446 | end 447 | end 448 | end 449 | if settings.slice_longfilenames and tmp:len()>settings.slice_longfilenames_amount+5 then 450 | tmp = tmp:sub(1, settings.slice_longfilenames_amount).." ..." 451 | end 452 | return tmp 453 | end 454 | 455 | --gets the file info of an item 456 | function get_file_info(item) 457 | local path = mp.get_property('playlist/' .. item - 1 .. '/filename') 458 | if is_protocol(path) then return {} end 459 | local file_info = utils.file_info(path) 460 | if not file_info then 461 | msg.warn('failed to read file info for', path) 462 | return {} 463 | end 464 | 465 | return file_info 466 | end 467 | 468 | --gets a nicename of playlist entry at 0-based position i 469 | function get_name_from_index(i, notitle) 470 | refresh_globals() 471 | if plen <= i then msg.error("no index in playlist", i, "length", plen); return nil end 472 | local _, name = nil 473 | local title = mp.get_property('playlist/'..i..'/title') 474 | local name = mp.get_property('playlist/'..i..'/filename') 475 | 476 | local should_use_title = settings.prefer_titles == 'all' or is_protocol(name) and settings.prefer_titles == 'url' 477 | --check if file has a media title stored or as property 478 | if not title and should_use_title then 479 | local mtitle = mp.get_property('media-title') 480 | if i == pos and mp.get_property('filename') ~= mtitle then 481 | if not title_table[name] then 482 | title_table[name] = mtitle 483 | end 484 | title = mtitle 485 | elseif title_table[name] then 486 | title = title_table[name] 487 | end 488 | end 489 | 490 | --if we have media title use a more conservative strip 491 | if title and not notitle and should_use_title then 492 | -- Escape a string for verbatim display on the OSD 493 | -- Ref: https://github.com/mpv-player/mpv/blob/94677723624fb84756e65c8f1377956667244bc9/player/lua/stats.lua#L145 494 | return stripfilename(title, true):gsub("\\", '\\\239\187\191'):gsub("{", "\\{"):gsub("^ ", "\\h") 495 | end 496 | 497 | --remove paths if they exist, keeping protocols for stripping 498 | if string.sub(name, 1, 1) == '/' or name:match("^%a:[/\\]") then 499 | _, name = utils.split_path(name) 500 | end 501 | return stripfilename(name):gsub("\\", '\\\239\187\191'):gsub("{", "\\{"):gsub("^ ", "\\h") 502 | end 503 | 504 | function parse_header(string) 505 | local esc_title = stripfilename(mp.get_property("media-title"), true):gsub("%%", "%%%%") 506 | local esc_file = stripfilename(mp.get_property("filename")):gsub("%%", "%%%%") 507 | return string:gsub("%%N", "\\N") 508 | :gsub("%%pos", mp.get_property_number("playlist-pos",0)+1) 509 | :gsub("%%plen", mp.get_property("playlist-count")) 510 | :gsub("%%cursor", cursor+1) 511 | :gsub("%%mediatitle", esc_title) 512 | :gsub("%%filename", esc_file) 513 | -- undo name escape 514 | :gsub("%%%%", "%%") 515 | end 516 | 517 | function parse_filename(string, name, index) 518 | local base = tostring(plen):len() 519 | local esc_name = stripfilename(name):gsub("%%", "%%%%") 520 | return string:gsub("%%N", "\\N") 521 | :gsub("%%pos", string.format("%0"..base.."d", index+1)) 522 | :gsub("%%name", esc_name) 523 | -- undo name escape 524 | :gsub("%%%%", "%%") 525 | end 526 | 527 | function parse_filename_by_index(index) 528 | local template = settings.normal_file 529 | 530 | local is_idle = mp.get_property_native('idle-active') 531 | local position = is_idle and -1 or pos 532 | 533 | if index == position then 534 | if index == cursor then 535 | if selection then 536 | template = settings.playing_selected_file 537 | else 538 | template = settings.playing_hovered_file 539 | end 540 | else 541 | template = settings.playing_file 542 | end 543 | elseif index == cursor then 544 | if selection then 545 | template = settings.selected_file 546 | else 547 | template = settings.hovered_file 548 | end 549 | end 550 | 551 | return parse_filename(template, get_name_from_index(index), index) 552 | end 553 | 554 | 555 | function draw_playlist() 556 | refresh_globals() 557 | local ass = assdraw.ass_new() 558 | 559 | local _, _, a = mp.get_osd_size() 560 | local h = 360 561 | local w = h * a 562 | 563 | if settings.curtain_opacity ~= nil and settings.curtain_opacity ~= 0 and settings.curtain_opacity < 1.0 then 564 | -- curtain dim from https://github.com/christoph-heinrich/mpv-quality-menu/blob/501794bfbef468ee6a61e54fc8821fe5cd72c4ed/quality-menu.lua#L699-L707 565 | local alpha = 255 - math.ceil(255 * settings.curtain_opacity) 566 | ass.text = string.format('{\\pos(0,0)\\rDefault\\an7\\1c&H000000&\\alpha&H%X&}', alpha) 567 | ass:draw_start() 568 | ass:rect_cw(0, 0, w, h) 569 | ass:draw_stop() 570 | ass:new_event() 571 | end 572 | 573 | ass:append(settings.style_ass_tags) 574 | 575 | -- TODO: padding should work even on different osd alignments 576 | if mp.get_property("osd-align-x") == "left" and mp.get_property("osd-align-y") == "top" then 577 | ass:pos(settings.text_padding_x, settings.text_padding_y) 578 | end 579 | 580 | if settings.playlist_header ~= "" then 581 | ass:append(parse_header(settings.playlist_header).."\\N") 582 | end 583 | 584 | -- (visible index, playlist index) pairs of playlist entries that should be rendered 585 | local visible_indices = {} 586 | 587 | local one_based_cursor = cursor + 1 588 | table.insert(visible_indices, one_based_cursor) 589 | 590 | local offset = 1; 591 | local visible_indices_length = 1; 592 | while visible_indices_length < settings.showamount and visible_indices_length < plen do 593 | -- add entry for offset steps below the cursor 594 | local below = one_based_cursor + offset 595 | if below <= plen then 596 | table.insert(visible_indices, below) 597 | visible_indices_length = visible_indices_length + 1; 598 | end 599 | 600 | -- add entry for offset steps above the cursor 601 | -- also need to double check that there is still space, this happens if we have even numbered limit 602 | local above = one_based_cursor - offset 603 | if above >= 1 and visible_indices_length < settings.showamount and visible_indices_length < plen then 604 | table.insert(visible_indices, 1, above) 605 | visible_indices_length = visible_indices_length + 1; 606 | end 607 | 608 | offset = offset + 1 609 | end 610 | 611 | -- both indices are 1 based 612 | for display_index, playlist_index in pairs(visible_indices) do 613 | if display_index == 1 and playlist_index ~= 1 then 614 | ass:append(settings.playlist_sliced_prefix.."\\N") 615 | elseif display_index == settings.showamount and playlist_index ~= plen then 616 | ass:append(settings.playlist_sliced_suffix) 617 | else 618 | -- parse_filename_by_index expects 0 based index 619 | ass:append(parse_filename_by_index(playlist_index - 1).."\\N") 620 | end 621 | end 622 | 623 | if settings.scale_playlist_by_window then w,h = 0, 0 end 624 | mp.set_osd_ass(w, h, ass.text) 625 | end 626 | 627 | local peek_display_timer = nil 628 | local peek_button_pressed = false 629 | 630 | function peek_timeout() 631 | peek_display_timer:kill() 632 | if not peek_button_pressed and not playlist_visible then 633 | remove_keybinds() 634 | end 635 | end 636 | 637 | function handle_complex_playlist_toggle(table) 638 | local event = table["event"] 639 | if event == "press" then 640 | msg.error("Complex key event not supported. Falling back to normal playlist display.") 641 | showplaylist() 642 | elseif event == "down" then 643 | showplaylist(1000000) 644 | if settings.peek_respect_display_timeout then 645 | peek_button_pressed = true 646 | peek_display_timer = mp.add_periodic_timer(settings.playlist_display_timeout, peek_timeout) 647 | end 648 | elseif event == "up" then 649 | -- set playlist state to not visible, doesn't actually hide playlist yet 650 | -- this will allow us to check if other functionality has rendered playlist before removing binds 651 | playlist_visible = false 652 | 653 | function remove_keybinds_after_timeout() 654 | -- if playlist is still not visible then lets actually hide it 655 | -- this lets other keys that interupt the peek to render playlist without peek up event closing it 656 | if not playlist_visible then 657 | remove_keybinds() 658 | end 659 | end 660 | 661 | if settings.peek_respect_display_timeout then 662 | peek_button_pressed = false 663 | if not peek_display_timer:is_enabled() then 664 | mp.add_timeout(0.01, remove_keybinds_after_timeout) 665 | end 666 | else 667 | -- use small delay to let dynamic binds run before keys are potentially unbound 668 | mp.add_timeout(0.01, remove_keybinds_after_timeout) 669 | end 670 | end 671 | end 672 | 673 | function toggle_playlist(show_function) 674 | local show = show_function or showplaylist 675 | if playlist_visible then 676 | remove_keybinds() 677 | else 678 | -- toggle always shows without timeout 679 | show(0) 680 | end 681 | end 682 | 683 | function showplaylist(duration) 684 | refresh_globals() 685 | if plen == 0 then return end 686 | if not playlist_visible and settings.reset_cursor_on_open then 687 | resetcursor() 688 | end 689 | 690 | playlist_visible = true 691 | add_keybinds() 692 | 693 | draw_playlist() 694 | keybindstimer:kill() 695 | 696 | local dur = duration or settings.playlist_display_timeout 697 | if dur > 0 then 698 | keybindstimer = mp.add_periodic_timer(dur, remove_keybinds) 699 | end 700 | end 701 | 702 | function showplaylist_non_interactive(duration) 703 | refresh_globals() 704 | if plen == 0 then return end 705 | if not playlist_visible and settings.reset_cursor_on_open then 706 | resetcursor() 707 | end 708 | playlist_visible = true 709 | draw_playlist() 710 | keybindstimer:kill() 711 | 712 | local dur = duration or settings.playlist_display_timeout 713 | if dur > 0 then 714 | keybindstimer = mp.add_periodic_timer(dur, remove_keybinds) 715 | end 716 | end 717 | 718 | selection=nil 719 | function selectfile() 720 | refresh_globals() 721 | if plen == 0 then return end 722 | if not selection then 723 | selection=cursor 724 | else 725 | selection=nil 726 | end 727 | showplaylist() 728 | end 729 | 730 | function unselectfile() 731 | selection=nil 732 | showplaylist() 733 | end 734 | 735 | function resetcursor() 736 | selection = nil 737 | cursor = mp.get_property_number('playlist-pos', 1) 738 | end 739 | 740 | function removefile() 741 | refresh_globals() 742 | if plen == 0 then return end 743 | selection = nil 744 | if cursor==pos then mp.command("script-message unseenplaylist mark true \"playlistmanager avoid conflict when removing file\"") end 745 | mp.commandv("playlist-remove", cursor) 746 | if cursor==plen-1 then cursor = cursor - 1 end 747 | if plen == 1 then 748 | remove_keybinds() 749 | else 750 | showplaylist() 751 | end 752 | end 753 | 754 | function moveup() 755 | refresh_globals() 756 | if plen == 0 then return end 757 | if cursor~=0 then 758 | if selection then mp.commandv("playlist-move", cursor,cursor-1) end 759 | cursor = cursor-1 760 | elseif settings.loop_cursor then 761 | if selection then mp.commandv("playlist-move", cursor,plen) end 762 | cursor = plen-1 763 | end 764 | showplaylist() 765 | end 766 | 767 | function movedown() 768 | refresh_globals() 769 | if plen == 0 then return end 770 | if cursor ~= plen-1 then 771 | if selection then mp.commandv("playlist-move", cursor,cursor+2) end 772 | cursor = cursor + 1 773 | elseif settings.loop_cursor then 774 | if selection then mp.commandv("playlist-move", cursor,0) end 775 | cursor = 0 776 | end 777 | showplaylist() 778 | end 779 | 780 | function movepageup() 781 | refresh_globals() 782 | if plen == 0 or cursor == 0 then return end 783 | local prev_cursor = cursor 784 | cursor = cursor - settings.showamount 785 | if cursor < 0 then cursor = 0 end 786 | if selection then mp.commandv("playlist-move", prev_cursor, cursor) end 787 | showplaylist() 788 | end 789 | 790 | function movepagedown() 791 | refresh_globals() 792 | if plen == 0 or cursor == plen-1 then return end 793 | local prev_cursor = cursor 794 | cursor = cursor + settings.showamount 795 | if cursor >= plen then cursor = plen-1 end 796 | if selection then mp.commandv("playlist-move", prev_cursor, cursor+1) end 797 | showplaylist() 798 | end 799 | 800 | function movebegin() 801 | refresh_globals() 802 | if plen == 0 or cursor == 0 then return end 803 | local prev_cursor = cursor 804 | cursor = 0 805 | if selection then mp.commandv("playlist-move", prev_cursor, cursor) end 806 | showplaylist() 807 | end 808 | 809 | function moveend() 810 | refresh_globals() 811 | if plen == 0 or cursor == plen-1 then return end 812 | local prev_cursor = cursor 813 | cursor = plen-1 814 | if selection then mp.commandv("playlist-move", prev_cursor, cursor+1) end 815 | showplaylist() 816 | end 817 | 818 | function write_watch_later(force_write) 819 | if settings.allow_write_watch_later_config then 820 | if mp.get_property_bool("save-position-on-quit") or force_write then 821 | mp.command("write-watch-later-config") 822 | end 823 | end 824 | end 825 | 826 | function playlist_next(force_write) 827 | write_watch_later(force_write) 828 | mp.commandv("playlist-next", "weak") 829 | end 830 | 831 | function playlist_prev(force_write) 832 | write_watch_later(force_write) 833 | mp.commandv("playlist-prev", "weak") 834 | end 835 | 836 | function playfile() 837 | refresh_globals() 838 | if plen == 0 then return end 839 | selection = nil 840 | local is_idle = mp.get_property_native('idle-active') 841 | if cursor ~= pos or is_idle then 842 | write_watch_later() 843 | mp.set_property("playlist-pos", cursor) 844 | else 845 | if cursor~=plen-1 then 846 | cursor = cursor + 1 847 | end 848 | write_watch_later() 849 | mp.commandv("playlist-next", "weak") 850 | end 851 | if settings.show_playlist_on_fileload ~= 2 then 852 | remove_keybinds() 853 | end 854 | end 855 | 856 | function file_filter(filenames) 857 | local files = {} 858 | for i = 1, #filenames do 859 | local file = filenames[i] 860 | local ext = file:match('%.([^%.]+)$') 861 | if ext and filetype_lookup[ext:lower()] then 862 | table.insert(files, file) 863 | end 864 | end 865 | return files 866 | end 867 | 868 | function get_playlist_filenames_set() 869 | local filenames = {} 870 | for n=0,plen-1,1 do 871 | local filename = mp.get_property('playlist/'..n..'/filename') 872 | local _, file = utils.split_path(filename) 873 | filenames[file] = true 874 | end 875 | return filenames 876 | end 877 | 878 | --Creates a playlist of all files in directory, will keep the order and position 879 | --For exaple, Folder has 12 files, you open the 5th file and run this, the remaining 7 are added behind the 5th file and prior 4 files before it 880 | function playlist(force_dir) 881 | refresh_globals() 882 | if not directory and plen > 0 then return end 883 | local hasfile = true 884 | if plen == 0 then 885 | hasfile = false 886 | dir = mp.get_property('working-directory') 887 | else 888 | dir = directory 889 | end 890 | 891 | if dir == "." then dir = "" end 892 | if force_dir then dir = force_dir end 893 | 894 | local files = file_filter(utils.readdir(dir, "files")) 895 | table.sort(files, alphanumsort) 896 | 897 | if files == nil then 898 | msg.verbose("no files in directory") 899 | return 900 | end 901 | 902 | local filenames = get_playlist_filenames_set() 903 | local c, c2 = 0,0 904 | if files then 905 | local cur = false 906 | local filename = mp.get_property("filename") 907 | for _, file in ipairs(files) do 908 | if file == nil or file[1] == "." then 909 | break 910 | end 911 | local appendstr = "append" 912 | if not hasfile then 913 | cur = true 914 | appendstr = "append-play" 915 | hasfile = true 916 | end 917 | if filename == file then 918 | cur = true 919 | elseif filenames[file] then 920 | -- skip files already in playlist 921 | elseif cur == true or settings.loadfiles_always_append then 922 | mp.commandv("loadfile", utils.join_path(dir, file), appendstr) 923 | msg.info("Appended to playlist: " .. file) 924 | c2 = c2 + 1 925 | else 926 | mp.commandv("loadfile", utils.join_path(dir, file), appendstr) 927 | msg.info("Prepended to playlist: " .. file) 928 | mp.commandv("playlist-move", mp.get_property_number("playlist-count", 1)-1, c) 929 | c = c + 1 930 | end 931 | end 932 | if c2 > 0 or c>0 then 933 | msg.info("Added "..c + c2.." files to playlist") 934 | else 935 | msg.info("No additional files found") 936 | end 937 | cursor = mp.get_property_number('playlist-pos', 1) 938 | else 939 | msg.error("Could not scan for files: "..(error or "")) 940 | end 941 | refresh_globals() 942 | if playlist_visible then 943 | showplaylist() 944 | elseif settings.display_osd_feedback then 945 | if c2 > 0 or c>0 then 946 | mp.osd_message("Added "..c + c2.." files to playlist") 947 | else 948 | mp.osd_message("No additional files found") 949 | end 950 | end 951 | return c + c2 952 | end 953 | 954 | function parse_home(path) 955 | if not path:find("^~") then 956 | return path 957 | end 958 | local home_dir = os.getenv("HOME") or os.getenv("USERPROFILE") 959 | if not home_dir then 960 | local drive = os.getenv("HOMEDRIVE") 961 | local path = os.getenv("HOMEPATH") 962 | if drive and path then 963 | home_dir = utils.join_path(drive, path) 964 | else 965 | msg.error("Couldn't find home dir.") 966 | return nil 967 | end 968 | end 969 | local result = path:gsub("^~", home_dir) 970 | return result 971 | end 972 | 973 | local interactive_save = false 974 | function activate_playlist_save() 975 | if interactive_save then 976 | remove_keybinds() 977 | mp.command("script-message playlistmanager-save-interactive \"start interactive filenaming process\"") 978 | else 979 | save_playlist() 980 | end 981 | end 982 | 983 | --saves the current playlist into a m3u file 984 | function save_playlist(filename) 985 | local length = mp.get_property_number('playlist-count', 0) 986 | if length == 0 then return end 987 | 988 | --get playlist save path 989 | local savepath 990 | if settings.playlist_savepath == nil or settings.playlist_savepath == "" then 991 | savepath = mp.command_native({"expand-path", "~~home/"}).."/playlists" 992 | else 993 | savepath = parse_home(settings.playlist_savepath) 994 | if savepath == nil then return end 995 | end 996 | 997 | --create savepath if it doesn't exist 998 | if utils.readdir(savepath) == nil then 999 | local windows_args = {'powershell', '-NoProfile', '-Command', 'mkdir', savepath} 1000 | local unix_args = { 'mkdir', savepath } 1001 | local args = settings.system == 'windows' and windows_args or unix_args 1002 | local res = utils.subprocess({ args = args, cancellable = false }) 1003 | if res.status ~= 0 then 1004 | msg.error("Failed to create playlist save directory "..savepath..". Error: "..(res.error or "unknown")) 1005 | return 1006 | end 1007 | end 1008 | 1009 | local name = filename 1010 | if name == nil then 1011 | if settings.playlist_save_filename == nil or settings.playlist_save_filename == "" then 1012 | local date = os.date("*t") 1013 | local datestring = ("%02d-%02d-%02d_%02d-%02d-%02d"):format(date.year, date.month, date.day, date.hour, date.min, date.sec) 1014 | 1015 | name = datestring.."_playlist-size_"..length..".m3u" 1016 | else 1017 | name = settings.playlist_save_filename 1018 | end 1019 | end 1020 | 1021 | local savepath = utils.join_path(savepath, name) 1022 | local file, err = io.open(savepath, "w") 1023 | if not file then 1024 | msg.error("Error in creating playlist file, check permissions. Error: "..(err or "unknown")) 1025 | else 1026 | file:write("#EXTM3U\n") 1027 | local i=0 1028 | while i < length do 1029 | local pwd = mp.get_property("working-directory") 1030 | local filename = mp.get_property('playlist/'..i..'/filename') 1031 | local fullpath = filename 1032 | if not is_protocol(filename) then 1033 | fullpath = utils.join_path(pwd, filename) 1034 | end 1035 | local title = mp.get_property('playlist/'..i..'/title') or title_table[filename] 1036 | if title then 1037 | file:write("#EXTINF:,"..title.."\n") 1038 | end 1039 | file:write(fullpath, "\n") 1040 | i=i+1 1041 | end 1042 | local saved_msg = "Playlist written to: "..savepath 1043 | if settings.display_osd_feedback then mp.osd_message(saved_msg) end 1044 | msg.info(saved_msg) 1045 | file:close() 1046 | end 1047 | end 1048 | 1049 | function alphanumsort(a, b) 1050 | local function padnum(d) 1051 | local dec, n = string.match(d, "(%.?)0*(.+)") 1052 | return #dec > 0 and ("%.12f"):format(d) or ("%s%03d%s"):format(dec, #n, n) 1053 | end 1054 | return tostring(a):lower():gsub("%.?%d+",padnum)..("%3d"):format(#b) 1055 | < tostring(b):lower():gsub("%.?%d+",padnum)..("%3d"):format(#a) 1056 | end 1057 | 1058 | -- fast sort algo from https://github.com/zsugabubus/dotfiles/blob/master/.config/mpv/scripts/playlist-filtersort.lua 1059 | function sortplaylist(startover) 1060 | local playlist = mp.get_property_native('playlist') 1061 | if #playlist < 2 then return end 1062 | 1063 | local order = {} 1064 | for i=1, #playlist do 1065 | order[i] = i 1066 | playlist[i].string = get_name_from_index(i - 1, true) 1067 | end 1068 | 1069 | table.sort(order, function(a, b) 1070 | return sort_modes[sort_mode].sort_fn(a, b, playlist) 1071 | end) 1072 | 1073 | for i=1, #playlist do 1074 | playlist[order[i]].new_pos = i 1075 | end 1076 | 1077 | for i=1, #playlist do 1078 | while true do 1079 | local j = playlist[i].new_pos 1080 | if i == j then 1081 | break 1082 | end 1083 | mp.commandv('playlist-move', (i) - 1, (j + 1) - 1) 1084 | mp.commandv('playlist-move', (j - 1) - 1, (i) - 1) 1085 | playlist[j], playlist[i] = playlist[i], playlist[j] 1086 | end 1087 | end 1088 | 1089 | for i = 1, #playlist do 1090 | local filename = mp.get_property('playlist/' .. i - 1 .. '/filename') 1091 | local ext = filename:match("%.([^%.]+)$") 1092 | if not ext or not filetype_lookup[ext:lower()] then 1093 | --move the directory to the end of the playlist 1094 | mp.commandv('playlist-move', i - 1, #playlist) 1095 | end 1096 | end 1097 | 1098 | cursor = mp.get_property_number('playlist-pos', 0) 1099 | if startover then 1100 | mp.set_property('playlist-pos', 0) 1101 | end 1102 | if playlist_visible then 1103 | showplaylist() 1104 | end 1105 | if settings.display_osd_feedback then 1106 | mp.osd_message("Playlist sorted with "..sort_modes[sort_mode].title) 1107 | end 1108 | end 1109 | 1110 | function reverseplaylist() 1111 | local length = mp.get_property_number('playlist-count', 0) 1112 | if length < 2 then return end 1113 | for outer=1, length-1, 1 do 1114 | mp.commandv('playlist-move', outer, 0) 1115 | end 1116 | if playlist_visible then 1117 | showplaylist() 1118 | elseif settings.display_osd_feedback then 1119 | mp.osd_message("Playlist reversed") 1120 | end 1121 | end 1122 | 1123 | function shuffleplaylist() 1124 | refresh_globals() 1125 | if plen < 2 then return end 1126 | mp.command("playlist-shuffle") 1127 | math.randomseed(os.time()) 1128 | mp.commandv("playlist-move", pos, math.random(0, plen-1)) 1129 | 1130 | local playlist = mp.get_property_native('playlist') 1131 | for i = 1, #playlist do 1132 | local filename = mp.get_property('playlist/' .. i - 1 .. '/filename') 1133 | local ext = filename:match("%.([^%.]+)$") 1134 | if not ext or not filetype_lookup[ext:lower()] then 1135 | --move the directory to the end of the playlist 1136 | mp.commandv('playlist-move', i - 1, #playlist) 1137 | end 1138 | end 1139 | 1140 | mp.set_property('playlist-pos', 0) 1141 | refresh_globals() 1142 | if playlist_visible then 1143 | showplaylist() 1144 | elseif settings.display_osd_feedback then 1145 | mp.osd_message("Playlist shuffled") 1146 | end 1147 | end 1148 | 1149 | function bind_keys(keys, name, func, opts) 1150 | if keys == nil or keys == "" then 1151 | mp.add_key_binding(keys, name, func, opts) 1152 | return 1153 | end 1154 | local i = 1 1155 | for key in keys:gmatch("[^%s]+") do 1156 | local prefix = i == 1 and '' or i 1157 | mp.add_key_binding(key, name..prefix, func, opts) 1158 | i = i + 1 1159 | end 1160 | end 1161 | 1162 | function bind_keys_forced(keys, name, func, opts) 1163 | if keys == nil or keys == "" then 1164 | mp.add_forced_key_binding(keys, name, func, opts) 1165 | return 1166 | end 1167 | local i = 1 1168 | for key in keys:gmatch("[^%s]+") do 1169 | local prefix = i == 1 and '' or i 1170 | mp.add_forced_key_binding(key, name..prefix, func, opts) 1171 | i = i + 1 1172 | end 1173 | end 1174 | 1175 | function unbind_keys(keys, name) 1176 | if keys == nil or keys == "" then 1177 | mp.remove_key_binding(name) 1178 | return 1179 | end 1180 | local i = 1 1181 | for key in keys:gmatch("[^%s]+") do 1182 | local prefix = i == 1 and '' or i 1183 | mp.remove_key_binding(name..prefix) 1184 | i = i + 1 1185 | end 1186 | end 1187 | 1188 | function add_keybinds() 1189 | bind_keys_forced(settings.key_moveup, 'moveup', moveup, "repeatable") 1190 | bind_keys_forced(settings.key_movedown, 'movedown', movedown, "repeatable") 1191 | bind_keys_forced(settings.key_movepageup, 'movepageup', movepageup, "repeatable") 1192 | bind_keys_forced(settings.key_movepagedown, 'movepagedown', movepagedown, "repeatable") 1193 | bind_keys_forced(settings.key_movebegin, 'movebegin', movebegin, "repeatable") 1194 | bind_keys_forced(settings.key_moveend, 'moveend', moveend, "repeatable") 1195 | bind_keys_forced(settings.key_selectfile, 'selectfile', selectfile) 1196 | bind_keys_forced(settings.key_unselectfile, 'unselectfile', unselectfile) 1197 | bind_keys_forced(settings.key_playfile, 'playfile', playfile) 1198 | bind_keys_forced(settings.key_removefile, 'removefile', removefile, "repeatable") 1199 | bind_keys_forced(settings.key_closeplaylist, 'closeplaylist', remove_keybinds) 1200 | end 1201 | 1202 | function remove_keybinds() 1203 | keybindstimer:kill() 1204 | keybindstimer = mp.add_periodic_timer(settings.playlist_display_timeout, remove_keybinds) 1205 | keybindstimer:kill() 1206 | mp.set_osd_ass(0, 0, "") 1207 | playlist_visible = false 1208 | if settings.reset_cursor_on_close then 1209 | resetcursor() 1210 | end 1211 | if settings.dynamic_binds then 1212 | unbind_keys(settings.key_moveup, 'moveup') 1213 | unbind_keys(settings.key_movedown, 'movedown') 1214 | unbind_keys(settings.key_movepageup, 'movepageup') 1215 | unbind_keys(settings.key_movepagedown, 'movepagedown') 1216 | unbind_keys(settings.key_movebegin, 'movebegin') 1217 | unbind_keys(settings.key_moveend, 'moveend') 1218 | unbind_keys(settings.key_selectfile, 'selectfile') 1219 | unbind_keys(settings.key_unselectfile, 'unselectfile') 1220 | unbind_keys(settings.key_playfile, 'playfile') 1221 | unbind_keys(settings.key_removefile, 'removefile') 1222 | unbind_keys(settings.key_closeplaylist, 'closeplaylist') 1223 | end 1224 | end 1225 | 1226 | keybindstimer = mp.add_periodic_timer(settings.playlist_display_timeout, remove_keybinds) 1227 | keybindstimer:kill() 1228 | 1229 | if not settings.dynamic_binds then 1230 | add_keybinds() 1231 | end 1232 | 1233 | if settings.loadfiles_on_idle_start and mp.get_property_number('playlist-count', 0) == 0 then 1234 | playlist() 1235 | end 1236 | 1237 | mp.observe_property('playlist-count', "number", function(_, plcount) 1238 | --if we promised to listen and sort on playlist size increase do it 1239 | if settings.sortplaylist_on_file_add and (plcount > plen) then 1240 | msg.info("Added files will be automatically sorted") 1241 | refresh_globals() 1242 | sortplaylist() 1243 | end 1244 | if playlist_visible then showplaylist() end 1245 | resolve_titles() 1246 | end) 1247 | 1248 | 1249 | url_request_queue = {} 1250 | function url_request_queue.push(item) table.insert(url_request_queue, item) end 1251 | function url_request_queue.pop() return table.remove(url_request_queue, 1) end 1252 | local url_titles_to_fetch = url_request_queue 1253 | local ongoing_url_requests = {} 1254 | 1255 | function url_fetching_throttler() 1256 | if #url_titles_to_fetch == 0 then 1257 | url_title_fetch_timer:kill() 1258 | end 1259 | 1260 | local ongoing_url_requests_count = 0 1261 | for _, ongoing in pairs(ongoing_url_requests) do 1262 | if ongoing then 1263 | ongoing_url_requests_count = ongoing_url_requests_count + 1 1264 | end 1265 | end 1266 | 1267 | -- start resolving some url titles if there is available slots 1268 | local amount_to_fetch = math.max(0, settings.concurrent_title_resolve_limit - ongoing_url_requests_count) 1269 | for index=1,amount_to_fetch,1 do 1270 | local file = url_titles_to_fetch.pop() 1271 | if file then 1272 | ongoing_url_requests[file] = true 1273 | resolve_ytdl_title(file) 1274 | end 1275 | end 1276 | end 1277 | 1278 | url_title_fetch_timer = mp.add_periodic_timer(0.1, url_fetching_throttler) 1279 | url_title_fetch_timer:kill() 1280 | 1281 | local_request_queue = {} 1282 | function local_request_queue.push(item) table.insert(local_request_queue, item) end 1283 | function local_request_queue.pop() return table.remove(local_request_queue, 1) end 1284 | local local_titles_to_fetch = local_request_queue 1285 | local ongoing_local_request = false 1286 | 1287 | -- this will only allow 1 concurrent local title resolve process 1288 | function local_fetching_throttler() 1289 | if not ongoing_local_request then 1290 | local file = local_titles_to_fetch.pop() 1291 | if file then 1292 | ongoing_local_request = true 1293 | resolve_ffprobe_title(file) 1294 | end 1295 | end 1296 | end 1297 | 1298 | function resolve_titles() 1299 | if settings.prefer_titles == 'none' then return end 1300 | if not settings.resolve_url_titles and not settings.resolve_local_titles then return end 1301 | 1302 | local length = mp.get_property_number('playlist-count', 0) 1303 | if length < 2 then return end 1304 | -- loop all items in playlist because we can't predict how it has changed 1305 | local added_urls = false 1306 | local added_local = false 1307 | for i=0,length - 1,1 do 1308 | local filename = mp.get_property('playlist/'..i..'/filename') 1309 | local title = mp.get_property('playlist/'..i..'/title') 1310 | if i ~= pos 1311 | and filename 1312 | and not title 1313 | and not title_table[filename] 1314 | and not requested_titles[filename] 1315 | then 1316 | requested_titles[filename] = true 1317 | if filename:match('^https?://') then 1318 | url_titles_to_fetch.push(filename) 1319 | added_urls = true 1320 | elseif settings.prefer_titles == "all" then 1321 | local_titles_to_fetch.push(filename) 1322 | added_local = true 1323 | end 1324 | end 1325 | end 1326 | if added_urls then 1327 | url_title_fetch_timer:resume() 1328 | end 1329 | if added_local then 1330 | local_fetching_throttler() 1331 | end 1332 | end 1333 | 1334 | function resolve_ytdl_title(filename) 1335 | local args = { 1336 | settings.youtube_dl_executable, 1337 | '--no-playlist', 1338 | '--flat-playlist', 1339 | '-sJ', 1340 | '--no-config', 1341 | filename, 1342 | } 1343 | local req = mp.command_native_async( 1344 | { 1345 | name = "subprocess", 1346 | args = args, 1347 | playback_only = false, 1348 | capture_stdout = true 1349 | }, 1350 | function (success, res) 1351 | ongoing_url_requests[filename] = false 1352 | if res.killed_by_us then 1353 | msg.verbose('Request to resolve url title ' .. filename .. ' timed out') 1354 | return 1355 | end 1356 | if res.status == 0 then 1357 | local json, err = utils.parse_json(res.stdout) 1358 | if not err then 1359 | local is_playlist = json['_type'] and json['_type'] == 'playlist' 1360 | local title = (is_playlist and '[playlist]: ' or '') .. json['title'] 1361 | msg.verbose(filename .. " resolved to '" .. title .. "'") 1362 | title_table[filename] = title 1363 | refresh_globals() 1364 | if playlist_visible then showplaylist() end 1365 | else 1366 | msg.error("Failed parsing json, reason: "..(err or "unknown")) 1367 | end 1368 | else 1369 | msg.error("Failed to resolve url title "..filename.." Error: "..(res.error or "unknown")) 1370 | end 1371 | end 1372 | ) 1373 | 1374 | mp.add_timeout( 1375 | settings.resolve_title_timeout, 1376 | function() 1377 | mp.abort_async_command(req) 1378 | ongoing_url_requests[filename] = false 1379 | end 1380 | ) 1381 | end 1382 | 1383 | function resolve_ffprobe_title(filename) 1384 | local args = { "ffprobe", "-show_format", "-show_entries", "format=tags", "-loglevel", "quiet", filename } 1385 | local req = mp.command_native_async( 1386 | { 1387 | name = "subprocess", 1388 | args = args, 1389 | playback_only = false, 1390 | capture_stdout = true 1391 | }, 1392 | function (success, res) 1393 | ongoing_local_request = false 1394 | local_fetching_throttler() 1395 | if res.killed_by_us then 1396 | msg.verbose('Request to resolve local title ' .. filename .. ' timed out') 1397 | return 1398 | end 1399 | if res.status == 0 then 1400 | local title = string.match(res.stdout, "title=([^\n\r]+)") 1401 | if title then 1402 | msg.verbose(filename .. " resolved to '" .. title .. "'") 1403 | title_table[filename] = title 1404 | refresh_globals() 1405 | if playlist_visible then showplaylist() end 1406 | end 1407 | else 1408 | msg.error("Failed to resolve local title "..filename.." Error: "..(res.error or "unknown")) 1409 | end 1410 | end 1411 | ) 1412 | end 1413 | 1414 | --script message handler 1415 | function handlemessage(msg, value, value2) 1416 | if msg == "show" and value == "playlist" then 1417 | if value2 ~= "toggle" then 1418 | showplaylist(value2) 1419 | return 1420 | else 1421 | toggle_playlist(showplaylist) 1422 | return 1423 | end 1424 | end 1425 | if msg == "show" and value == "playlist-nokeys" then 1426 | if value2 ~= "toggle" then 1427 | showplaylist_non_interactive(value2) 1428 | return 1429 | else 1430 | toggle_playlist(showplaylist_non_interactive) 1431 | return 1432 | end 1433 | end 1434 | if msg == "show" and value == "filename" and strippedname and value2 then 1435 | mp.commandv('show-text', strippedname, tonumber(value2)*1000 ) ; return 1436 | end 1437 | if msg == "show" and value == "filename" and strippedname then 1438 | mp.commandv('show-text', strippedname ) ; return 1439 | end 1440 | if msg == "sort" then sortplaylist(value) ; return end 1441 | if msg == "shuffle" then shuffleplaylist() ; return end 1442 | if msg == "reverse" then reverseplaylist() ; return end 1443 | if msg == "loadfiles" then playlist(value) ; return end 1444 | if msg == "save" then save_playlist(value) ; return end 1445 | if msg == "playlist-next" then playlist_next(true) ; return end 1446 | if msg == "playlist-prev" then playlist_prev(true) ; return end 1447 | if msg == "enable-interactive-save" then interactive_save = true end 1448 | if msg == "close" then remove_keybinds() end 1449 | end 1450 | 1451 | mp.register_script_message("playlistmanager", handlemessage) 1452 | 1453 | bind_keys(settings.key_sortplaylist, "sortplaylist", function() 1454 | sortplaylist() 1455 | sort_mode = sort_mode + 1 1456 | if sort_mode > #sort_modes then sort_mode = 1 end 1457 | end) 1458 | bind_keys(settings.key_shuffleplaylist, "shuffleplaylist", shuffleplaylist) 1459 | bind_keys(settings.key_reverseplaylist, "reverseplaylist", reverseplaylist) 1460 | bind_keys(settings.key_loadfiles, "loadfiles", playlist) 1461 | bind_keys(settings.key_saveplaylist, "saveplaylist", activate_playlist_save) 1462 | bind_keys(settings.key_showplaylist, "showplaylist", showplaylist) 1463 | bind_keys( 1464 | settings.key_peek_at_playlist, 1465 | "peek_at_playlist", 1466 | handle_complex_playlist_toggle, 1467 | { complex=true } 1468 | ) 1469 | 1470 | mp.register_event("start-file", on_start_file) 1471 | mp.register_event("file-loaded", on_file_loaded) 1472 | mp.register_event("end-file", on_end_file) 1473 | --------------------------------------------------------------------------------