summaryrefslogtreecommitdiff
path: root/mpv/scripts
diff options
context:
space:
mode:
Diffstat (limited to 'mpv/scripts')
-rw-r--r--mpv/scripts/auto-audio-device.lua36
-rw-r--r--mpv/scripts/auto-profiles.lua198
-rw-r--r--mpv/scripts/auto-save-state.lua55
-rw-r--r--mpv/scripts/autodeint.lua158
-rw-r--r--mpv/scripts/betterchapters.lua21
-rw-r--r--mpv/scripts/blacklist-extensions.lua80
-rw-r--r--mpv/scripts/cycle-profiles.lua103
-rw-r--r--mpv/scripts/music-mode.lua192
-rw-r--r--mpv/scripts/user-input.lua664
-rw-r--r--mpv/scripts/youtube-quality.lua275
-rw-r--r--mpv/scripts/youtube-serarch.lua149
11 files changed, 1931 insertions, 0 deletions
diff --git a/mpv/scripts/auto-audio-device.lua b/mpv/scripts/auto-audio-device.lua
new file mode 100644
index 0000000..92fcdf8
--- /dev/null
+++ b/mpv/scripts/auto-audio-device.lua
@@ -0,0 +1,36 @@
+local mp = require 'mp'
+
+
+local auto_change = true
+local av_map = {
+ ["default"] = "auto",
+ ["DELL U2312HM"] = "coreaudio/AppleUSBAudioEngine:FiiO:DigiHug USB Audio:1a160000:3", -- FiiO DAC
+ ["SONY TV *00"] = "coreaudio/AppleGFXHDAEngineOutputDP:0:{D94D-8204-01010101}", -- HDMI
+ ["Color LCD"] = "coreaudio/AppleHDAEngineOutput:1B,0,1,1:0", -- Built-in
+}
+
+function set_audio_device(obs_display)
+ if obs_display ~= nil and not auto_change then
+ return
+ end
+
+ local display = obs_display or mp.get_property_native("display-names")
+ if not display or not display[1] then
+ --print("Invalid display return value: " .. tostring(display))
+ return
+ end
+
+ local new_adev = av_map[display[1]] or av_map["default"]
+ local current_adev = mp.get_property("audio-device", av_map["default"])
+ if new_adev ~= current_adev then
+ mp.osd_message("Audio device: " .. new_adev)
+ mp.set_property("audio-device", new_adev)
+ end
+end
+
+mp.observe_property("display-names", "native", function(name, value) set_audio_device(value) end)
+mp.add_key_binding("", "set-audio-device", function() set_audio_device(nil) end)
+mp.add_key_binding("", "toggle-switching", function()
+ auto_change = not auto_change
+ mp.osd_message("Audio device switching: " .. tostring(auto_change))
+end)
diff --git a/mpv/scripts/auto-profiles.lua b/mpv/scripts/auto-profiles.lua
new file mode 100644
index 0000000..32e1809
--- /dev/null
+++ b/mpv/scripts/auto-profiles.lua
@@ -0,0 +1,198 @@
+--[[
+
+Automatically apply profiles based on runtime conditions.
+At least mpv 0.21.0 is required.
+
+This script queries the list of loaded config profiles, and checks the
+"profile-desc" field of each profile. If it starts with "cond:", the script
+parses the string as Lua expression, and evaluates it. If the expression
+returns true, the profile is applied, if it returns false, it is ignored.
+
+Expressions can reference properties by accessing "p". For example, "p.pause"
+would return the current pause status. If the variable name contains any "_"
+characters, they are turned into "-". For example, "playback_time" would
+return the property "playback-time". (Although you can also just write
+p["playback-time"].)
+
+Note that if a property is not available, it will return nil, which can cause
+errors if used in expressions. These are printed and ignored, and the
+expression is considered to be false. You can also write e.g.
+get("playback-time", 0) instead of p.playback_time to default to 0.
+
+Whenever a property referenced by a profile condition changes, the condition
+is re-evaluated. If the return value of the condition changes from false or
+error to true, the profile is applied.
+
+Note that profiles cannot be "unapplied", so you may have to define inverse
+profiles with inverse conditions do undo a profile.
+
+Using profile-desc is just a hack - maybe it will be changed later.
+
+Supported --script-opts:
+
+ auto-profiles: if set to "no", the script disables itself (but will still
+ listen to property notifications etc. - if you set it to
+ "yes" again, it will re-evaluate the current state)
+
+Example profiles:
+
+# the profile names aren't used (except for logging), but must not clash with
+# other profiles
+[test]
+profile-desc=cond:p.playback_time>10
+video-zoom=2
+
+# you would need this to actually "unapply" the "test" profile
+[test-revert]
+profile-desc=cond:p.playback_time<=10
+video-zoom=0
+
+--]]
+
+local lua_modules = mp.find_config_file('lua-modules')
+if lua_modules then
+ package.path = package.path .. ';' .. lua_modules .. '/?.lua'
+end
+
+local f = require 'auto-profiles-functions'
+local utils = require 'mp.utils'
+local msg = require 'mp.msg'
+
+local profiles = {}
+local watched_properties = {} -- indexed by property name (used as a set)
+local cached_properties = {} -- property name -> last known raw value
+local properties_to_profiles = {} -- property name -> set of profiles using it
+local have_dirty_profiles = false -- at least one profile is marked dirty
+
+-- Used during evaluation of the profile condition, and should contain the
+-- profile the condition is evaluated for.
+local current_profile = nil
+
+local function evaluate(profile)
+ msg.verbose("Re-evaluate auto profile " .. profile.name)
+
+ current_profile = profile
+ local status, res = pcall(profile.cond)
+ current_profile = nil
+
+ if not status then
+ -- errors can be "normal", e.g. in case properties are unavailable
+ msg.info("Error evaluating: " .. res)
+ res = false
+ elseif type(res) ~= "boolean" then
+ msg.error("Profile '" .. profile.name .. "' did not return a boolean.")
+ res = false
+ end
+ if res ~= profile.status and res == true then
+ msg.info("Applying profile " .. profile.name)
+ mp.commandv("apply-profile", profile.name)
+ end
+ profile.status = res
+ profile.dirty = false
+end
+
+local function on_property_change(name, val)
+ cached_properties[name] = val
+ -- Mark all profiles reading this property as dirty, so they get re-evaluated
+ -- the next time the script goes back to sleep.
+ local dependent_profiles = properties_to_profiles[name]
+ if dependent_profiles then
+ for profile, _ in pairs(dependent_profiles) do
+ assert(profile.cond) -- must be a profile table
+ profile.dirty = true
+ have_dirty_profiles = true
+ end
+ end
+end
+
+local function on_idle()
+ if mp.get_opt("auto-profiles") == "no" then
+ return
+ end
+
+ -- When events and property notifications stop, re-evaluate all dirty profiles.
+ if have_dirty_profiles then
+ for _, profile in ipairs(profiles) do
+ if profile.dirty then
+ evaluate(profile)
+ end
+ end
+ end
+ have_dirty_profiles = false
+end
+
+mp.register_idle(on_idle)
+
+local evil_meta_magic = {
+ __index = function(table, key)
+ -- interpret everything as property, unless it already exists as
+ -- a non-nil global value
+ local v = _G[key]
+ if type(v) ~= "nil" then
+ return v
+ end
+ -- Lua identifiers can't contain "-", so in order to match with mpv
+ -- property conventions, replace "_" to "-"
+ key = string.gsub(key, "_", "-")
+ -- Normally, we use the cached value only (to reduce CPU usage I guess?)
+ if not watched_properties[key] then
+ watched_properties[key] = true
+ mp.observe_property(key, "native", on_property_change)
+ cached_properties[key] = mp.get_property_native(key)
+ end
+ -- The first time the property is read we need add it to the
+ -- properties_to_profiles table, which will be used to mark the profile
+ -- dirty if a property referenced by it changes.
+ if current_profile then
+ local map = properties_to_profiles[key]
+ if not map then
+ map = {}
+ properties_to_profiles[key] = map
+ end
+ map[current_profile] = true
+ end
+ return cached_properties[key]
+ end,
+}
+
+local evil_magic = {}
+setmetatable(evil_magic, evil_meta_magic)
+
+local function compile_cond(name, s)
+ chunk, err = loadstring("return " .. s, "profile " .. name .. " condition")
+ if not chunk then
+ msg.error("Profile '" .. name .. "' condition: " .. err)
+ return function() return false end
+ end
+ return chunk
+end
+
+for i, v in ipairs(mp.get_property_native("profile-list")) do
+ local desc = v["profile-desc"]
+ if desc and desc:sub(1, 5) == "cond:" then
+ local profile = {
+ name = v.name,
+ cond = compile_cond(v.name, desc:sub(6)),
+ properties = {},
+ status = nil,
+ dirty = true, -- need re-evaluate
+ }
+ profiles[#profiles + 1] = profile
+ have_dirty_profiles = true
+ end
+end
+
+-- these definitions are for use by the condition expressions
+
+p = evil_magic
+
+function get(property_name, default)
+ local val = p[property_name]
+ if val == nil then
+ val = default
+ end
+ return val
+end
+
+-- re-evaluate all profiles immediately
+on_idle()
diff --git a/mpv/scripts/auto-save-state.lua b/mpv/scripts/auto-save-state.lua
new file mode 100644
index 0000000..a4ec3ec
--- /dev/null
+++ b/mpv/scripts/auto-save-state.lua
@@ -0,0 +1,55 @@
+-- Save watch-later conditionally.
+-- a) Always for playlists (so mpv remembers the position within this playlist)
+-- b) Never for files shorter than `min_length` seconds
+-- c) When the current playback position is > `thresh_start` and < `thresh_end`
+
+
+local opts = require 'mp.options'
+local o = {
+ min_length = 600,
+ thresh_end = 180,
+ thresh_start = 60,
+}
+opts.read_options(o)
+
+
+-- Return true when multiple files are being played
+function check_playlist()
+ local pcount, err = mp.get_property_number("playlist-count")
+ if not pcount then
+ print("error: " .. err)
+ pcount = 1
+ end
+
+ return pcount > 1
+end
+
+
+-- Return true when the current playback time is not too close to the start or end
+-- Always return false for short files, no matter the playback time
+function check_time()
+ local duration = mp.get_property_number("duration", 9999)
+ if duration < o.min_length then
+ return false
+ end
+
+ local remaining, err = mp.get_property_number("time-remaining")
+ if not remaining then
+ print("error: " .. err)
+ remaining = -math.huge
+ end
+ local pos, err = mp.get_property_number("time-pos")
+ if not pos then
+ print("error: " .. err)
+ pos = -math.huge
+ end
+
+ return pos > o.thresh_start and remaining > o.thresh_end
+end
+
+
+mp.add_key_binding("q", "quit-watch-later-conditional",
+ function()
+ mp.set_property_bool("options/save-position-on-quit", check_playlist() or check_time())
+ mp.command("quit")
+ end)
diff --git a/mpv/scripts/autodeint.lua b/mpv/scripts/autodeint.lua
new file mode 100644
index 0000000..16f56ea
--- /dev/null
+++ b/mpv/scripts/autodeint.lua
@@ -0,0 +1,158 @@
+-- From: https://github.com/mpv-player/mpv/tree/master/TOOLS/lua
+-- (with slight modifications)
+--
+-- This script uses the lavfi idet filter to automatically insert the
+-- appropriate deinterlacing filter based on a short section of the
+-- currently playing video.
+--
+-- It registers the key-binding ctrl+d, which when pressed, inserts the filters
+-- ``vf=lavfi=idet,pullup,vf=lavfi=idet``. After 4 seconds, it removes these
+-- filters and decides whether the content is progressive, interlaced, or
+-- telecined and the interlacing field dominance.
+--
+-- Based on this information, it may set mpv's ``deinterlace`` property (which
+-- usually inserts the yadif filter), or insert the ``pullup`` filter if the
+-- content is telecined. It also sets mpv's ``field-dominance`` property.
+--
+-- OPTIONS:
+-- The default detection time may be overridden by adding
+--
+-- --script-opts=autodeint.detect_seconds=<number of seconds>
+--
+-- to mpv's arguments. This may be desirable to allow idet more
+-- time to collect data.
+--
+-- To see counts of the various types of frames for each detection phase,
+-- the verbosity can be increased with
+--
+-- --msg-level autodeint=v
+--
+-- This script requires a recent version of ffmpeg for which the idet
+-- filter adds the required metadata.
+
+require "mp.msg"
+
+script_name = mp.get_script_name()
+detect_label = string.format("%s-detect", script_name)
+pullup_label = string.format("%s", script_name)
+ivtc_detect_label = string.format("%s-ivtc-detect", script_name)
+
+-- number of seconds to gather cropdetect data
+detect_seconds = tonumber(mp.get_opt(string.format("%s.detect_seconds", script_name)))
+if not detect_seconds then
+ detect_seconds = 4
+end
+
+function del_filter_if_present(label)
+ -- necessary because mp.command('vf del @label:filter') raises an
+ -- error if the filter doesn't exist
+ local vfs = mp.get_property_native("vf")
+
+ for i,vf in pairs(vfs) do
+ if vf["label"] == label then
+ table.remove(vfs, i)
+ mp.set_property_native("vf", vfs)
+ return true
+ end
+ end
+ return false
+end
+
+function start_detect()
+ -- exit if detection is already in progress
+ if timer then
+ mp.msg.warn("already detecting!")
+ mp.osd_message("autodeint: already detecting!")
+ return
+ end
+
+ mp.set_property("deinterlace","no")
+ del_filter_if_present(pullup_label)
+
+ -- insert the detection filter
+ local cmd = string.format('vf add @%s:lavfi=graph="idet",@%s:pullup,@%s:lavfi=graph="idet"',
+ detect_label, pullup_label, ivtc_detect_label)
+ if not mp.command(cmd) then
+ mp.msg.error("failed to insert detection filters")
+ mp.osd_message("autodeint: failed to insert detection filters")
+ return
+ end
+
+ -- wait to gather data
+ mp.osd_message("autodeint: starting detection")
+ timer = mp.add_timeout(detect_seconds, select_filter)
+end
+
+function stop_detect()
+ del_filter_if_present(detect_label)
+ del_filter_if_present(ivtc_detect_label)
+ timer = nil
+end
+
+progressive, interlaced_tff, interlaced_bff, interlaced = 0, 1, 2, 3, 4
+
+function judge(label)
+ -- get the metadata
+ local result = mp.get_property_native(string.format("vf-metadata/%s", label))
+ -- filter might have been removed by third party
+ if not result or next(result) == nil then
+ return progressive
+ end
+ num_tff = tonumber(result["lavfi.idet.multiple.tff"])
+ num_bff = tonumber(result["lavfi.idet.multiple.bff"])
+ num_progressive = tonumber(result["lavfi.idet.multiple.progressive"])
+ num_undetermined = tonumber(result["lavfi.idet.multiple.undetermined"])
+ num_interlaced = num_tff + num_bff
+ num_determined = num_interlaced + num_progressive
+
+ mp.msg.verbose(label .. " progressive = "..num_progressive)
+ mp.msg.verbose(label .. " interlaced-tff = "..num_tff)
+ mp.msg.verbose(label .. " interlaced-bff = "..num_bff)
+ mp.msg.verbose(label .. " undetermined = "..num_undetermined)
+
+ if num_determined < num_undetermined then
+ mp.msg.warn("majority undetermined frames")
+ end
+ if num_progressive > 20*num_interlaced then
+ return progressive
+ elseif num_tff > 10*num_bff then
+ return interlaced_tff
+ elseif num_bff > 10*num_tff then
+ return interlaced_bff
+ else
+ return interlaced
+ end
+end
+
+function select_filter()
+ -- handle the first detection filter results
+ verdict = judge(detect_label)
+ if verdict == progressive then
+ mp.msg.info("progressive: doing nothing")
+ mp.osd_message("autodeint: no deinterlacing required")
+ stop_detect()
+ return
+ elseif verdict == interlaced_tff then
+ mp.set_property("field-dominance", "top")
+ elseif verdict == interlaced_bff then
+ mp.set_property("field-dominance", "bottom")
+ elseif verdict == interlaced then
+ mp.set_property("field-dominance", "auto")
+ end
+
+ -- handle the ivtc detection filter results
+ verdict = judge(ivtc_detect_label)
+ if verdict == progressive then
+ mp.msg.info(string.format("telecinied with %s field dominance: using pullup", mp.get_property("field-dominance")))
+ mp.osd_message("autodeint: using pullup")
+ stop_detect()
+ else
+ mp.msg.info(string.format("interlaced with %s field dominance: setting deinterlace property", mp.get_property("field-dominance")))
+ del_filter_if_present(pullup_label)
+ mp.osd_message(string.format("autodeint: setting deinterlace property (%s)", mp.get_property("field-dominance")))
+ mp.set_property("deinterlace", "yes")
+ stop_detect()
+ end
+end
+
+mp.add_key_binding("ctrl+d", script_name, start_detect)
diff --git a/mpv/scripts/betterchapters.lua b/mpv/scripts/betterchapters.lua
new file mode 100644
index 0000000..4b871c8
--- /dev/null
+++ b/mpv/scripts/betterchapters.lua
@@ -0,0 +1,21 @@
+-- From: https://github.com/mpv-player/mpv/issues/4738#issuecomment-321298846
+
+function chapter_seek(direction)
+ local chapters = mp.get_property_number("chapters")
+ if chapters == nil then chapters = 0 end
+ local chapter = mp.get_property_number("chapter")
+ if chapter == nil then chapter = 0 end
+ if chapter+direction < 0 then
+ mp.command("playlist_prev")
+ mp.commandv("script-message", "osc-playlist")
+ elseif chapter+direction >= chapters then
+ mp.command("playlist_next")
+ mp.commandv("script-message", "osc-playlist")
+ else
+ mp.commandv("add", "chapter", direction)
+ mp.commandv("script-message", "osc-chapterlist")
+ end
+end
+
+mp.add_key_binding(nil, "chapterplaylist-next", function() chapter_seek(1) end)
+mp.add_key_binding(nil, "chapterplaylist-prev", function() chapter_seek(-1) end) \ No newline at end of file
diff --git a/mpv/scripts/blacklist-extensions.lua b/mpv/scripts/blacklist-extensions.lua
new file mode 100644
index 0000000..a8ec638
--- /dev/null
+++ b/mpv/scripts/blacklist-extensions.lua
@@ -0,0 +1,80 @@
+-- From: https://github.com/occivink/mpv-scripts
+
+opts = {
+ blacklist="",
+ whitelist="",
+ remove_files_without_extension = false,
+ oneshot = true,
+}
+(require 'mp.options').read_options(opts)
+local msg = require 'mp.msg'
+
+function split(input)
+ local ret = {}
+ for str in string.gmatch(input, "([^,]+)") do
+ ret[#ret + 1] = str
+ end
+ return ret
+end
+
+opts.blacklist = split(opts.blacklist)
+opts.whitelist = split(opts.whitelist)
+
+local exclude
+if #opts.whitelist > 0 then
+ exclude = function(extension)
+ for _, ext in pairs(opts.whitelist) do
+ if extension == ext then
+ return false
+ end
+ end
+ return true
+ end
+elseif #opts.blacklist > 0 then
+ exclude = function(extension)
+ for _, ext in pairs(opts.blacklist) do
+ if extension == ext then
+ return true
+ end
+ end
+ return false
+ end
+else
+ return
+end
+
+function should_remove(filename)
+ if string.find(filename, "://") then
+ return false
+ end
+ local extension = string.match(filename, "%.([^./]+)$")
+ if not extension and opts.remove_file_without_extension then
+ return true
+ end
+ if extension and exclude(string.lower(extension)) then
+ return true
+ end
+ return false
+end
+
+function process(playlist_count)
+ if playlist_count < 2 then return end
+ if opts.oneshot then
+ mp.unobserve_property(observe)
+ end
+ local playlist = mp.get_property_native("playlist")
+ local removed = 0
+ for i = #playlist, 1, -1 do
+ if should_remove(playlist[i].filename) then
+ mp.commandv("playlist-remove", i-1)
+ removed = removed + 1
+ end
+ end
+ if removed == #playlist then
+ msg.warn("Removed eveything from the playlist")
+ end
+end
+
+function observe(k,v) process(v) end
+
+mp.observe_property("playlist-count", "number", observe)
diff --git a/mpv/scripts/cycle-profiles.lua b/mpv/scripts/cycle-profiles.lua
new file mode 100644
index 0000000..da846c5
--- /dev/null
+++ b/mpv/scripts/cycle-profiles.lua
@@ -0,0 +1,103 @@
+--[[
+ script to cycle profiles with a keybind, accomplished through script messages
+ available at: https://github.com/CogentRedTester/mpv-scripts
+ syntax:
+ script-message cycle-profiles "profile1;profile2;profile3"
+ You must use semicolons to separate the profiles, do not include any spaces that are not part of the profile name.
+ The script will print the profile description to the screen when switching, if there is no profile description, then it just prints the name
+]]--
+
+--change this to change what character separates the profile names
+seperator = ";"
+
+msg = require 'mp.msg'
+
+--splits the profiles string into an array of profile names
+--function taken from: https://stackoverflow.com/questions/1426954/split-string-in-lua/7615129#7615129
+function mysplit (inputstr, sep)
+ if sep == nil then
+ sep = "%s"
+ end
+ local t={}
+ for str in string.gmatch(inputstr, "([^"..sep.."]+)") do
+ table.insert(t, str)
+ end
+ return t
+end
+
+--table of all available profiles and options
+profileList = mp.get_property_native('profile-list')
+
+--keeps track of current profile for every unique cycle
+iterator = {}
+
+--stores descriptions for profiles
+--once requested a description is stored here so it does not need to be found again
+profilesDescs = {}
+
+--if trying to cycle to an unknown profile this function is run to find a description to print
+function findDesc(profile)
+ msg.verbose('unknown profile ' .. profile .. ', searching for description')
+
+ for i = 1, #profileList, 1 do
+ if profileList[i]['name'] == profile then
+ msg.verbose('profile found')
+ local desc = profileList[i]['profile-desc']
+
+ if desc ~= nil then
+ msg.verbose('description found')
+ profilesDescs[profile] = desc
+ else
+ msg.verbose('no description, will use name')
+ profilesDescs[profile] = profile
+ end
+ return
+ end
+ end
+
+ msg.verbose('profile not found')
+ profilesDescs[profile] = "no profile '" .. profile .. "'"
+end
+
+--prints the profile description to the OSD
+--if the profile has not been requested before during the session then it runs findDesc()
+function printProfileDesc(profile)
+ local desc = profilesDescs[profile]
+ if desc == nil then
+ findDesc(profile)
+ desc = profilesDescs[profile]
+ end
+
+ msg.verbose('profile description: ' .. desc)
+ mp.osd_message(desc)
+end
+
+function main(profileStr)
+ --if there is not already an iterator for this cycle then it creates one
+ if iterator[profileStr] == nil then
+ msg.verbose('unknown cycle, creating new iterator')
+ iterator[profileStr] = 1
+ end
+ local i = iterator[profileStr]
+
+ --converts the string into an array of profile names
+ local profiles = mysplit(profileStr, seperator)
+ msg.verbose('cycling ' .. tostring(profiles))
+ msg.verbose("number of profiles: " .. tostring(#profiles))
+
+ --sends the command to apply the profile
+ msg.info("applying profile " .. profiles[i])
+ mp.commandv('apply-profile', profiles[i])
+
+ --prints the profile description to the OSD
+ printProfileDesc(profiles[i])
+
+ --moves the iterator
+ iterator[profileStr] = iterator[profileStr] + 1
+ if iterator[profileStr] > #profiles then
+ msg.verbose('reached end of profiles, wrapping back to start')
+ iterator[profileStr] = 1
+ end
+end
+
+mp.register_script_message('cycle-profiles', main)
diff --git a/mpv/scripts/music-mode.lua b/mpv/scripts/music-mode.lua
new file mode 100644
index 0000000..4c57456
--- /dev/null
+++ b/mpv/scripts/music-mode.lua
@@ -0,0 +1,192 @@
+local mp = require 'mp'
+local msg = require 'mp.msg'
+local opt = require 'mp.options'
+
+--script options, set these in script-opts
+local o = {
+ --change to disable automatic mode switching
+ auto = true,
+
+ --profile to call when valid extension is found
+ profile = "music",
+
+ --runs this profile when in music mode and a non-audio file is loaded
+ --you should essentially put all your defaults that the music profile changed in here
+ undo_profile = "",
+
+ --start playback in music mode. This setting is only applied when the player is initially started,
+ --changing this option during runtime does nothing.
+ --probably only useful if auto is disabled
+ enable = false,
+
+ --the script will also enable the following input section when music mode is enabled
+ --see the mpv manual for details on sections
+ input_section = "music",
+
+ --dispays the metadata of the track on the osd when music mode is on
+ --there is also a script message to enable this seperately
+ show_metadata = false
+}
+
+opt.read_options(o, 'musicmode', function() msg.verbose('options updated') end)
+
+--a music file is one where mpv returns an audio stream or coverart as the first track
+local function is_audio_file()
+ local track_list = mp.get_property_native("track-list")
+
+ local has_audio = false
+ for _, track in ipairs(track_list) do
+ if track.type == "audio" then has_audio = true
+ elseif not track.albumart and (track["demux-fps"] or 2) > 1 then return false end
+ end
+ return has_audio
+end
+
+local metadata = mp.create_osd_overlay('ass-events')
+metadata.hidden = not o.show_metadata
+
+local function update_metadata()
+ metadata.data = mp.get_property_osd('filtered-metadata')
+ metadata:update()
+end
+
+local function enable_metadata()
+ metadata.hidden = false
+ metadata:update()
+end
+
+local function disable_metadata()
+ metadata.hidden = true
+ metadata:remove()
+end
+
+--changes visibility of metadata
+local function show_metadata(command)
+ if command == "on" or command == nil then
+ enable_metadata()
+ elseif command == "off" then
+ disable_metadata()
+ elseif command == "toggle" then
+ if metadata.hidden then
+ enable_metadata()
+ else
+ disable_metadata()
+ end
+ else
+ msg.warn('unknown command "' .. command .. '"')
+ end
+end
+
+--to prevent superfluous loading of profiles the script keeps track of when music mode is enabled
+local musicMode = false
+
+--enables music mode
+local function activate()
+ mp.commandv('apply-profile', o.profile)
+ mp.commandv('enable-section', o.input_section, "allow-vo-dragging+allow-hide-cursor")
+ mp.osd_message('Music Mode enabled')
+
+ if o.show_metadata then
+ show_metadata("on")
+ end
+
+ musicMode = true
+end
+
+--disables music mode
+local function deactivate()
+ mp.commandv('apply-profile', o.undo_profile)
+ mp.commandv('disable-section', o.input_section)
+ mp.osd_message('Music Mode disabled')
+
+ if o.show_metadata then
+ show_metadata('off')
+ end
+
+ musicMode = false
+end
+
+local function main()
+ --if the file is an audio file then the music profile is loaded
+ if is_audio_file() then
+ msg.verbose('audio file, applying profile "' .. o.profile .. '"')
+ if not musicMode then
+ activate()
+ end
+ elseif o.undo_profile ~= "" and musicMode then
+ msg.verbose('video file, applying undo profile "' .. o.undo_profile .. '"')
+ deactivate()
+ end
+end
+
+--sets music mode from script-message
+local function script_message(command)
+ if command == "on" or command == nil then
+ activate()
+ elseif command == "off" then
+ deactivate()
+ elseif command == "toggle" then
+ if musicMode then
+ deactivate()
+ else
+ activate()
+ end
+ else
+ msg.warn('unknown command "' .. command .. '"')
+ end
+end
+
+local function lock()
+ o.auto = false
+ msg.info('Music Mode locked')
+ mp.osd_message('Music Mode locked')
+end
+
+local function unLock()
+ o.auto = true
+ msg.info('Music Mode unlocked')
+ mp.osd_message('Music Mode unlocked')
+end
+
+--toggles lock
+local function lock_script_message(command)
+ if command == "on" or command == nil then
+ lock()
+ elseif command == "off" then
+ unLock()
+ elseif command == "toggle" then
+ if o.auto then
+ lock()
+ else
+ unLock()
+ end
+ else
+ msg.warn('unknown command "' .. command .. '"')
+ end
+end
+
+--runs when the file is loaded, if script is locked it will do nothing
+local function file_loaded()
+ if o.auto then
+ main()
+ end
+end
+
+if o.enable then
+ activate()
+end
+
+--sets music mode
+--accepts arguments: 'on', 'off', 'toggle'
+mp.register_script_message('music-mode', script_message)
+
+--stops the script from switching modes on file loads
+----accepts arguments: 'on', 'off', 'toggle'
+mp.register_script_message('music-mode-lock', lock_script_message)
+
+--shows file metadata on osc
+--accepts arguments: 'on' 'off' 'toggle'
+mp.register_script_message('show-metadata', show_metadata)
+
+mp.add_hook('on_preloaded', 40, file_loaded)
+mp.observe_property('filtered-metadata', 'string', update_metadata)
diff --git a/mpv/scripts/user-input.lua b/mpv/scripts/user-input.lua
new file mode 100644
index 0000000..689e073
--- /dev/null
+++ b/mpv/scripts/user-input.lua
@@ -0,0 +1,664 @@
+local mp = require 'mp'
+local msg = require 'mp.msg'
+local utils = require 'mp.utils'
+local options = require 'mp.options'
+
+-- Default options
+local opts = {
+ -- All drawing is scaled by this value, including the text borders and the
+ -- cursor. Change it if you have a high-DPI display.
+ scale = 1,
+ -- Set the font used for the REPL and the console. This probably doesn't
+ -- have to be a monospaced font.
+ font = "",
+ -- Set the font size used for the REPL and the console. This will be
+ -- multiplied by "scale."
+ font_size = 16,
+}
+
+options.read_options(opts, "user_input")
+
+local queue = {
+ queue = {},
+ active_ids = {}
+}
+local histories = {}
+local request = nil
+
+local line = ''
+
+--[[
+ sends a response to the original script in the form of a json string
+ it is expected that all requests get a response, if the input is nil then err should say why
+ current error codes are:
+ exitted the user closed the input instead of pressing Enter
+ already_queued a request with the specified id was already in the queue
+ replaced the request was replaced with a newer request
+ cancelled a script cancelled the request
+]]--
+local function send_response(send_line, err, override_response)
+ mp.commandv("script-message", override_response or request.response, send_line and line or "", err or "")
+end
+
+
+--[[
+ The below code is a modified implementation of text input from mpv's console.lua:
+ https://github.com/mpv-player/mpv/blob/7ca14d646c7e405f3fb1e44600e2a67fc4607238/player/lua/console.lua
+
+ Modifications:
+ removed support for log messages, sending commands, tab complete, help commands
+ removed update timer
+ Changed esc key to call handle_esc function
+ handle_esc and handle_enter now call the send_response() function
+ all functions that send responses now call queue:pop()
+ made history specific to request ids
+ localised all functions - reordered some to fit
+ keybindings use new names
+]]--
+
+------------------------------START ORIGINAL MPV CODE-----------------------------------
+----------------------------------------------------------------------------------------
+----------------------------------------------------------------------------------------
+----------------------------------------------------------------------------------------
+----------------------------------------------------------------------------------------
+
+-- Copyright (C) 2019 the mpv developers
+--
+-- Permission to use, copy, modify, and/or distribute this software for any
+-- purpose with or without fee is hereby granted, provided that the above
+-- copyright notice and this permission notice appear in all copies.
+--
+-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+-- WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+-- MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+-- SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+-- WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
+-- OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
+-- CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+local assdraw = require 'mp.assdraw'
+
+local function detect_platform()
+ local o = {}
+ -- Kind of a dumb way of detecting the platform but whatever
+ if mp.get_property_native('options/vo-mmcss-profile', o) ~= o then
+ return 'windows'
+ elseif mp.get_property_native('options/macos-force-dedicated-gpu', o) ~= o then
+ return 'macos'
+ elseif os.getenv('WAYLAND_DISPLAY') then
+ return 'wayland'
+ end
+ return 'x11'
+end
+
+-- Pick a better default font for Windows and macOS
+local platform = detect_platform()
+if platform == 'windows' then
+ opts.font = 'Consolas'
+elseif platform == 'macos' then
+ opts.font = 'Menlo'
+else
+ opts.font = 'monospace'
+end
+
+local repl_active = false
+local insert_mode = false
+local cursor = 1
+local key_bindings = {}
+local global_margin_y = 0
+
+-- Escape a string for verbatim display on the OSD
+local function ass_escape(str)
+ -- There is no escape for '\' in ASS (I think?) but '\' is used verbatim if
+ -- it isn't followed by a recognised character, so add a zero-width
+ -- non-breaking space
+ str = str:gsub('\\', '\\\239\187\191')
+ str = str:gsub('{', '\\{')
+ str = str:gsub('}', '\\}')
+ -- Precede newlines with a ZWNBSP to prevent ASS's weird collapsing of
+ -- consecutive newlines
+ str = str:gsub('\n', '\239\187\191\\N')
+ -- Turn leading spaces into hard spaces to prevent ASS from stripping them
+ str = str:gsub('\\N ', '\\N\\h')
+ str = str:gsub('^ ', '\\h')
+ return str
+end
+
+-- Render the REPL and console as an ASS OSD
+local function update()
+ local dpi_scale = mp.get_property_native("display-hidpi-scale", 1.0)
+
+ dpi_scale = dpi_scale * opts.scale
+
+ local screenx, screeny, aspect = mp.get_osd_size()
+ screenx = screenx / dpi_scale
+ screeny = screeny / dpi_scale
+
+ -- Clear the OSD if the REPL is not active
+ if not repl_active then
+ mp.set_osd_ass(screenx, screeny, '')
+ return
+ end
+
+ local ass = assdraw.ass_new()
+ local style = '{\\r' ..
+ '\\1a&H00&\\3a&H00&\\4a&H99&' ..
+ '\\1c&Heeeeee&\\3c&H111111&\\4c&H000000&' ..
+ '\\fn' .. opts.font .. '\\fs' .. opts.font_size ..
+ '\\bord1\\xshad0\\yshad1\\fsp0\\q1}'
+ -- Create the cursor glyph as an ASS drawing. ASS will draw the cursor
+ -- inline with the surrounding text, but it sets the advance to the width
+ -- of the drawing. So the cursor doesn't affect layout too much, make it as
+ -- thin as possible and make it appear to be 1px wide by giving it 0.5px
+ -- horizontal borders.
+ local cheight = opts.font_size * 8
+ local cglyph = '{\\r' ..
+ '\\1a&H44&\\3a&H44&\\4a&H99&' ..
+ '\\1c&Heeeeee&\\3c&Heeeeee&\\4c&H000000&' ..
+ '\\xbord0.5\\ybord0\\xshad0\\yshad1\\p4\\pbo24}' ..
+ 'm 0 0 l 1 0 l 1 ' .. cheight .. ' l 0 ' .. cheight ..
+ '{\\p0}'
+ local before_cur = ass_escape(line:sub(1, cursor - 1))
+ local after_cur = ass_escape(line:sub(cursor))
+
+ ass:new_event()
+ ass:an(1)
+ ass:pos(2, screeny - 2 - global_margin_y * screeny)
+ ass:append(style .. request.text .. '\\N')
+ ass:append('> ' .. before_cur)
+ ass:append(cglyph)
+ ass:append(style .. after_cur)
+
+ -- Redraw the cursor with the REPL text invisible. This will make the
+ -- cursor appear in front of the text.
+ ass:new_event()
+ ass:an(1)
+ ass:pos(2, screeny - 2)
+ ass:append(style .. '{\\alpha&HFF&}> ' .. before_cur)
+ ass:append(cglyph)
+ ass:append(style .. '{\\alpha&HFF&}' .. after_cur)
+
+ mp.set_osd_ass(screenx, screeny, ass.text)
+end
+
+-- Naive helper function to find the next UTF-8 character in 'str' after 'pos'
+-- by skipping continuation bytes. Assumes 'str' contains valid UTF-8.
+local function next_utf8(str, pos)
+ if pos > str:len() then return pos end
+ repeat
+ pos = pos + 1
+ until pos > str:len() or str:byte(pos) < 0x80 or str:byte(pos) > 0xbf
+ return pos
+end
+
+-- As above, but finds the previous UTF-8 charcter in 'str' before 'pos'
+local function prev_utf8(str, pos)
+ if pos <= 1 then return pos end
+ repeat
+ pos = pos - 1
+ until pos <= 1 or str:byte(pos) < 0x80 or str:byte(pos) > 0xbf
+ return pos
+end
+
+-- Insert a character at the current cursor position (any_unicode, Shift+Enter)
+local function handle_char_input(c)
+ if insert_mode then
+ line = line:sub(1, cursor - 1) .. c .. line:sub(next_utf8(line, cursor))
+ else
+ line = line:sub(1, cursor - 1) .. c .. line:sub(cursor)
+ end
+ cursor = cursor + #c
+ update()
+end
+
+-- Remove the character behind the cursor (Backspace)
+local function handle_backspace()
+ if cursor <= 1 then return end
+ local prev = prev_utf8(line, cursor)
+ line = line:sub(1, prev - 1) .. line:sub(cursor)
+ cursor = prev
+ update()
+end
+
+-- Remove the character in front of the cursor (Del)
+local function handle_del()
+ if cursor > line:len() then return end
+ line = line:sub(1, cursor - 1) .. line:sub(next_utf8(line, cursor))
+ update()
+end
+
+-- Toggle insert mode (Ins)
+local function handle_ins()
+ insert_mode = not insert_mode
+end
+
+-- Move the cursor to the next character (Right)
+local function next_char(amount)
+ cursor = next_utf8(line, cursor)
+ update()
+end
+
+-- Move the cursor to the previous character (Left)
+local function prev_char(amount)
+ cursor = prev_utf8(line, cursor)
+ update()
+end
+
+-- Clear the current line (Ctrl+C)
+local function clear()
+ line = ''
+ cursor = 1
+ insert_mode = false
+ request.history.pos = #request.history.list + 1
+ update()
+end
+
+-- Close the REPL if the current line is empty, otherwise do nothing (Ctrl+D)
+local function maybe_exit()
+ if line == '' then
+ send_response(false, "exitted")
+ queue:pop()
+ end
+end
+
+local function handle_esc()
+ send_response(false, "exitted")
+ queue:pop()
+end
+
+-- Run the current command and clear the line (Enter)
+local function handle_enter()
+ if request.history.list[#request.history.list] ~= line and line ~= "" then
+ request.history.list[#request.history.list + 1] = line
+ end
+ send_response(true)
+ queue:pop()
+end
+
+-- Go to the specified position in the command history
+local function go_history(new_pos)
+ local old_pos = request.history.pos
+ request.history.pos = new_pos
+
+ -- Restrict the position to a legal value
+ if request.history.pos > #request.history.list + 1 then
+ request.history.pos = #request.history.list + 1
+ elseif request.history.pos < 1 then
+ request.history.pos = 1
+ end
+
+ -- Do nothing if the history position didn't actually change
+ if request.history.pos == old_pos then
+ return
+ end
+
+ -- If the user was editing a non-history line, save it as the last history
+ -- entry. This makes it much less frustrating to accidentally hit Up/Down
+ -- while editing a line.
+ if old_pos == #request.history.list + 1 and line ~= '' and request.history.list[#request.history.list] ~= line then
+ request.history.list[#request.history.list + 1] = line
+ end
+
+ -- Now show the history line (or a blank line for #history + 1)
+ if request.history.pos <= #request.history.list then
+ line = request.history.list[request.history.pos]
+ else
+ line = ''
+ end
+ cursor = line:len() + 1
+ insert_mode = false
+ update()
+end
+
+-- Go to the specified relative position in the command history (Up, Down)
+local function move_history(amount)
+ go_history(request.history.pos + amount)
+end
+
+-- Go to the first command in the command history (PgUp)
+local function handle_pgup()
+ go_history(1)
+end
+
+-- Stop browsing history and start editing a blank line (PgDown)
+local function handle_pgdown()
+ go_history(#request.history.list + 1)
+end
+
+-- Move to the start of the current word, or if already at the start, the start
+-- of the previous word. (Ctrl+Left)
+local function prev_word()
+ -- This is basically the same as next_word() but backwards, so reverse the
+ -- string in order to do a "backwards" find. This wouldn't be as annoying
+ -- to do if Lua didn't insist on 1-based indexing.
+ cursor = line:len() - select(2, line:reverse():find('%s*[^%s]*', line:len() - cursor + 2)) + 1
+ update()
+end
+
+-- Move to the end of the current word, or if already at the end, the end of
+-- the next word. (Ctrl+Right)
+local function next_word()
+ cursor = select(2, line:find('%s*[^%s]*', cursor)) + 1
+ update()
+end
+
+-- Move the cursor to the beginning of the line (HOME)
+local function go_home()
+ cursor = 1
+ update()
+end
+
+-- Move the cursor to the end of the line (END)
+local function go_end()
+ cursor = line:len() + 1
+ update()
+end
+
+-- Delete from the cursor to the end of the word (Ctrl+W)
+local function del_word()
+ local before_cur = line:sub(1, cursor - 1)
+ local after_cur = line:sub(cursor)
+
+ before_cur = before_cur:gsub('[^%s]+%s*$', '', 1)
+ line = before_cur .. after_cur
+ cursor = before_cur:len() + 1
+ update()
+end
+
+-- Delete from the cursor to the end of the line (Ctrl+K)
+local function del_to_eol()
+ line = line:sub(1, cursor - 1)
+ update()
+end
+
+-- Delete from the cursor back to the start of the line (Ctrl+U)
+local function del_to_start()
+ line = line:sub(cursor)
+ cursor = 1
+ update()
+end
+
+-- Returns a string of UTF-8 text from the clipboard (or the primary selection)
+local function get_clipboard(clip)
+ if platform == 'x11' then
+ local res = utils.subprocess({
+ args = { 'xclip', '-selection', clip and 'clipboard' or 'primary', '-out' },
+ playback_only = false,
+ })
+ if not res.error then
+ return res.stdout
+ end
+ elseif platform == 'wayland' then
+ local res = utils.subprocess({
+ args = { 'wl-paste', clip and '-n' or '-np' },
+ playback_only = false,
+ })
+ if not res.error then
+ return res.stdout
+ end
+ elseif platform == 'windows' then
+ local res = utils.subprocess({
+ args = { 'powershell', '-NoProfile', '-Command', [[& {
+ Trap {
+ Write-Error -ErrorRecord $_
+ Exit 1
+ }
+
+ $clip = ""
+ if (Get-Command "Get-Clipboard" -errorAction SilentlyContinue) {
+ $clip = Get-Clipboard -Raw -Format Text -TextFormatType UnicodeText
+ } else {
+ Add-Type -AssemblyName PresentationCore
+ $clip = [Windows.Clipboard]::GetText()
+ }
+
+ $clip = $clip -Replace "`r",""
+ $u8clip = [System.Text.Encoding]::UTF8.GetBytes($clip)
+ [Console]::OpenStandardOutput().Write($u8clip, 0, $u8clip.Length)
+ }]] },
+ playback_only = false,
+ })
+ if not res.error then
+ return res.stdout
+ end
+ elseif platform == 'macos' then
+ local res = utils.subprocess({
+ args = { 'pbpaste' },
+ playback_only = false,
+ })
+ if not res.error then
+ return res.stdout
+ end
+ end
+ return ''
+end
+
+-- Paste text from the window-system's clipboard. 'clip' determines whether the
+-- clipboard or the primary selection buffer is used (on X11 and Wayland only.)
+local function paste(clip)
+ local text = get_clipboard(clip)
+ local before_cur = line:sub(1, cursor - 1)
+ local after_cur = line:sub(cursor)
+ line = before_cur .. text .. after_cur
+ cursor = cursor + text:len()
+ update()
+end
+
+-- List of input bindings. This is a weird mashup between common GUI text-input
+-- bindings and readline bindings.
+local function get_bindings()
+ local bindings = {
+ { 'esc', handle_esc },
+ { 'enter', handle_enter },
+ { 'kp_enter', handle_enter },
+ { 'shift+enter', function() handle_char_input('\n') end },
+ { 'bs', handle_backspace },
+ { 'shift+bs', handle_backspace },
+ { 'del', handle_del },
+ { 'shift+del', handle_del },
+ { 'ins', handle_ins },
+ { 'shift+ins', function() paste(false) end },
+ { 'mbtn_mid', function() paste(false) end },
+ { 'left', function() prev_char() end },
+ { 'right', function() next_char() end },
+ { 'up', function() move_history(-1) end },
+ { 'wheel_up', function() move_history(-1) end },
+ { 'down', function() move_history(1) end },
+ { 'wheel_down', function() move_history(1) end },
+ { 'wheel_left', function() end },
+ { 'wheel_right', function() end },
+ { 'ctrl+left', prev_word },
+ { 'ctrl+right', next_word },
+ { 'home', go_home },
+ { 'end', go_end },
+ { 'pgup', handle_pgup },
+ { 'pgdwn', handle_pgdown },
+ { 'ctrl+c', clear },
+ { 'ctrl+d', maybe_exit },
+ { 'ctrl+k', del_to_eol },
+ { 'ctrl+u', del_to_start },
+ { 'ctrl+v', function() paste(true) end },
+ { 'meta+v', function() paste(true) end },
+ { 'ctrl+w', del_word },
+ { 'kp_dec', function() handle_char_input('.') end },
+ }
+
+ for i = 0, 9 do
+ bindings[#bindings + 1] =
+ {'kp' .. i, function() handle_char_input('' .. i) end}
+ end
+
+ return bindings
+end
+
+local function text_input(info)
+ if info.key_text and (info.event == "press" or info.event == "down"
+ or info.event == "repeat")
+ then
+ handle_char_input(info.key_text)
+ end
+end
+
+local function define_key_bindings()
+ if #key_bindings > 0 then
+ return
+ end
+ for _, bind in ipairs(get_bindings()) do
+ -- Generate arbitrary name for removing the bindings later.
+ local name = "_userinput_" .. bind[1]
+ key_bindings[#key_bindings + 1] = name
+ mp.add_forced_key_binding(bind[1], name, bind[2], {repeatable = true})
+ end
+ mp.add_forced_key_binding("any_unicode", "_userinput_text", text_input,
+ {repeatable = true, complex = true})
+ key_bindings[#key_bindings + 1] = "_userinput_text"
+end
+
+local function undefine_key_bindings()
+ for _, name in ipairs(key_bindings) do
+ mp.remove_key_binding(name)
+ end
+ key_bindings = {}
+end
+
+-- Set the REPL visibility ("enable", Esc)
+local function set_active(active)
+ if active == repl_active then return end
+ if active then
+ repl_active = true
+ insert_mode = false
+ define_key_bindings()
+ else
+ clear()
+ repl_active = false
+ undefine_key_bindings()
+ collectgarbage()
+ end
+ update()
+end
+
+
+utils.shared_script_property_observe("osc-margins", function(_, val)
+ if val then
+ -- formatted as "%f,%f,%f,%f" with left, right, top, bottom, each
+ -- value being the border size as ratio of the window size (0.0-1.0)
+ local vals = {}
+ for v in string.gmatch(val, "[^,]+") do
+ vals[#vals + 1] = tonumber(v)
+ end
+ global_margin_y = vals[4] -- bottom
+ else
+ global_margin_y = 0
+ end
+ update()
+end)
+
+-- Redraw the REPL when the OSD size changes. This is needed because the
+-- PlayRes of the OSD will need to be adjusted.
+mp.observe_property('osd-width', 'native', update)
+mp.observe_property('osd-height', 'native', update)
+mp.observe_property('display-hidpi-scale', 'native', update)
+
+----------------------------------------------------------------------------------------
+----------------------------------------------------------------------------------------
+----------------------------------------------------------------------------------------
+-------------------------------END ORIGINAL MPV CODE------------------------------------
+
+-- push new request onto the queue
+-- if a request with the same id already exists and the queueable flag is not enabled then
+-- a nil result will be returned to the function
+function queue:push(req)
+ if self.active_ids[req.id] then
+
+ -- replace an existing request with the new one
+ if req.replace then
+ for i = 1, #self.queue do
+ if self.queue[i].id == req.id then
+ send_response(false, "replaced", self.queue[i].response)
+ self.queue[i] = req
+ if i == 1 then request = req ; update() end
+ return
+ end
+ end
+ end
+
+ --cancel the new request if it is not queueable
+ if not req.queueable then send_response(false, "already_queued", req.response) ; return end
+ end
+
+ table.insert(self.queue, req)
+ self.active_ids[req.id] = (self.active_ids[req.id] or 0) + 1
+ if #self.queue == 1 then return self:start_queue() end
+end
+
+-- removes the first item in the queue and either continues or stops the queue
+function queue:pop()
+ self:remove(1)
+ clear()
+
+ if #self.queue < 1 then return self:stop_queue()
+ else return self:continue_queue() end
+end
+
+-- safely removes an item from the queue and updates the set of active requests
+function queue:remove(index)
+ local req = table.remove(self.queue, index)
+ self.active_ids[req.id] = self.active_ids[req.id] ~= 1 and self.active_ids[req.id] - 1 or nil
+end
+
+function queue:start_queue()
+ request = self.queue[1]
+ line = request.default_input
+ set_active(true)
+end
+
+function queue:continue_queue()
+ request = self.queue[1]
+ line = request.default_input
+ update()
+end
+
+function queue:stop_queue()
+ set_active(false)
+end
+
+-- removes all requests with the specified id from the queue
+mp.register_script_message("cancel-user-input", function(id)
+ local i = 2
+ while i <= #queue.queue do
+ if queue.queue[i].id == id then
+ send_response(false, "cancelled", queue.queue[i].response)
+ queue:remove(i)
+ else
+ i = i + 1
+ end
+ end
+
+ if queue.queue[1] and queue.queue[1].id == id then
+ send_response(false, "cancelled")
+ queue:pop()
+ end
+end)
+
+-- script message to recieve input requests, get-user-input.lua acts as an interface to call this script message
+-- requests are recieved as json objects
+mp.register_script_message("request-user-input", function(response, id, request_text, default_input, queueable, replace)
+ local req = {}
+
+ if not response then msg.error("input requests require a response string") ; return end
+ if not id then msg.error("input requests require an id string") ; return end
+ req.response = response
+ req.text = ass_escape(request_text or "")
+ req.default_input = default_input
+ req.id = id or "mpv"
+ req.queueable = (queueable == "1")
+ req.replace = (replace == "1")
+
+ if not histories[id] then histories[id] = {pos = 1, list = {}} end
+ req.history = histories[id]
+
+ queue:push(req)
+end)
+
+--temporary keybind for debugging purposes
+mp.add_key_binding("Ctrl+i", "user-input", function() set_active(true) end)
diff --git a/mpv/scripts/youtube-quality.lua b/mpv/scripts/youtube-quality.lua
new file mode 100644
index 0000000..b587f37
--- /dev/null
+++ b/mpv/scripts/youtube-quality.lua
@@ -0,0 +1,275 @@
+-- youtube-quality.lua
+--
+-- Change youtube video quality on the fly.
+--
+-- Diplays a menu that lets you switch to different ytdl-format settings while
+-- you're in the middle of a video (just like you were using the web player).
+--
+-- Bound to ctrl-f by default.
+
+local mp = require 'mp'
+local utils = require 'mp.utils'
+local msg = require 'mp.msg'
+local assdraw = require 'mp.assdraw'
+
+local opts = {
+ --key bindings
+ toggle_menu_binding = "ctrl+f",
+ up_binding = "UP",
+ down_binding = "DOWN",
+ select_binding = "ENTER",
+
+ --formatting / cursors
+ selected_and_active = "▶ - ",
+ selected_and_inactive = "● - ",
+ unselected_and_active = "▷ - ",
+ unselected_and_inactive = "○ - ",
+
+ --font size scales by window, if false requires larger font and padding sizes
+ scale_playlist_by_window=false,
+
+ --playlist ass style overrides inside curly brackets, \keyvalue is one field, extra \ for escape in lua
+ --example {\\fnUbuntu\\fs10\\b0\\bord1} equals: font=Ubuntu, size=10, bold=no, border=1
+ --read http://docs.aegisub.org/3.2/ASS_Tags/ for reference of tags
+ --undeclared tags will use default osd settings
+ --these styles will be used for the whole playlist. More specific styling will need to be hacked in
+ --
+ --(a monospaced font is recommended but not required)
+ style_ass_tags = "{\\fnmonospace}",
+
+ --paddings for top left corner
+ text_padding_x = 5,
+ text_padding_y = 5,
+
+ --other
+ menu_timeout = 10,
+
+ --use youtube-dl to fetch a list of available formats (overrides quality_strings)
+ fetch_formats = true,
+
+ --default menu entries
+ quality_strings=[[
+ [
+ {"4320p" : "bestvideo[height<=?4320p]+bestaudio/best"},
+ {"2160p" : "bestvideo[height<=?2160]+bestaudio/best"},
+ {"1440p" : "bestvideo[height<=?1440]+bestaudio/best"},
+ {"1080p" : "bestvideo[height<=?1080]+bestaudio/best"},
+ {"720p" : "bestvideo[height<=?720]+bestaudio/best"},
+ {"480p" : "bestvideo[height<=?480]+bestaudio/best"},
+ {"360p" : "bestvideo[height<=?360]+bestaudio/best"},
+ {"240p" : "bestvideo[height<=?240]+bestaudio/best"},
+ {"144p" : "bestvideo[height<=?144]+bestaudio/best"}
+ ]
+ ]],
+}
+(require 'mp.options').read_options(opts, "youtube-quality")
+opts.quality_strings = utils.parse_json(opts.quality_strings)
+
+local destroyer = nil
+
+
+function show_menu()
+ local selected = 1
+ local active = 0
+ local current_ytdl_format = mp.get_property("ytdl-format")
+ msg.verbose("current ytdl-format: "..current_ytdl_format)
+ local num_options = 0
+ local options = {}
+
+
+ if opts.fetch_formats then
+ options, num_options = download_formats()
+ end
+
+ if next(options) == nil then
+ for i,v in ipairs(opts.quality_strings) do
+ num_options = num_options + 1
+ for k,v2 in pairs(v) do
+ options[i] = {label = k, format=v2}
+ if v2 == current_ytdl_format then
+ active = i
+ selected = active
+ end
+ end
+ end
+ end
+
+ --set the cursor to the currently format
+ for i,v in ipairs(options) do
+ if v.format == current_ytdl_format then
+ active = i
+ selected = active
+ break
+ end
+ end
+
+ function selected_move(amt)
+ selected = selected + amt
+ if selected < 1 then selected = num_options
+ elseif selected > num_options then selected = 1 end
+ timeout:kill()
+ timeout:resume()
+ draw_menu()
+ end
+ function choose_prefix(i)
+ if i == selected and i == active then return opts.selected_and_active
+ elseif i == selected then return opts.selected_and_inactive end
+
+ if i ~= selected and i == active then return opts.unselected_and_active
+ elseif i ~= selected then return opts.unselected_and_inactive end
+ return "> " --shouldn't get here.
+ end
+
+ function draw_menu()
+ local ass = assdraw.ass_new()
+
+ ass:pos(opts.text_padding_x, opts.text_padding_y)
+ ass:append(opts.style_ass_tags)
+
+ for i,v in ipairs(options) do
+ ass:append(choose_prefix(i)..v.label.."\\N")
+ end
+
+ local w, h = mp.get_osd_size()
+ if opts.scale_playlist_by_window then w,h = 0, 0 end
+ mp.set_osd_ass(w, h, ass.text)
+ end
+
+ function destroy()
+ timeout:kill()
+ mp.set_osd_ass(0,0,"")
+ mp.remove_key_binding("move_up")
+ mp.remove_key_binding("move_down")
+ mp.remove_key_binding("select")
+ mp.remove_key_binding("escape")
+ destroyer = nil
+ end
+ timeout = mp.add_periodic_timer(opts.menu_timeout, destroy)
+ destroyer = destroy
+
+ mp.add_forced_key_binding(opts.up_binding, "move_up", function() selected_move(-1) end, {repeatable=true})
+ mp.add_forced_key_binding(opts.down_binding, "move_down", function() selected_move(1) end, {repeatable=true})
+ mp.add_forced_key_binding(opts.select_binding, "select", function()
+ destroy()
+ mp.set_property("ytdl-format", options[selected].format)
+ reload_resume()
+ end)
+ mp.add_forced_key_binding(opts.toggle_menu_binding, "escape", destroy)
+
+ draw_menu()
+ return
+end
+
+local ytdl = {
+ path = "youtube-dl",
+ searched = false,
+ blacklisted = {}
+}
+
+format_cache={}
+function download_formats()
+ local function exec(args)
+ local ret = utils.subprocess({args = args})
+ return ret.status, ret.stdout, ret
+ end
+
+ local function table_size(t)
+ s = 0
+ for i,v in ipairs(t) do
+ s = s+1
+ end
+ return s
+ end
+
+ local url = mp.get_property("path")
+
+ url = string.gsub(url, "ytdl://", "") -- Strip possible ytdl:// prefix.
+
+ -- don't fetch the format list if we already have it
+ if format_cache[url] ~= nil then
+ local res = format_cache[url]
+ return res, table_size(res)
+ end
+ mp.osd_message("fetching available formats with youtube-dl...", 60)
+
+ if not (ytdl.searched) then
+ local ytdl_mcd = mp.find_config_file("youtube-dl")
+ if not (ytdl_mcd == nil) then
+ msg.verbose("found youtube-dl at: " .. ytdl_mcd)
+ ytdl.path = ytdl_mcd
+ end
+ ytdl.searched = true
+ end
+
+ local command = {ytdl.path, "--no-warnings", "--no-playlist", "-J"}
+ table.insert(command, url)
+ local es, json, result = exec(command)
+
+ if (es < 0) or (json == nil) or (json == "") then
+ mp.osd_message("fetching formats failed...", 1)
+ msg.error("failed to get format list: " .. err)
+ return {}, 0
+ end
+
+ local json, err = utils.parse_json(json)
+
+ if (json == nil) then
+ mp.osd_message("fetching formats failed...", 1)
+ msg.error("failed to parse JSON data: " .. err)
+ return {}, 0
+ end
+
+ res = {}
+ msg.verbose("youtube-dl succeeded!")
+ for i,v in ipairs(json.formats) do
+ if v.vcodec ~= "none" then
+ local fps = v.fps and v.fps.."fps" or ""
+ local resolution = string.format("%sx%s", v.width, v.height)
+ local l = string.format("%-9s %-5s (%-4s / %s)", resolution, fps, v.ext, v.vcodec)
+ local f = string.format("%s+bestaudio/best", v.format_id)
+ table.insert(res, {label=l, format=f, width=v.width })
+ end
+ end
+
+ table.sort(res, function(a, b) return a.width > b.width end)
+
+ mp.osd_message("", 0)
+ format_cache[url] = res
+ return res, table_size(res)
+end
+
+
+-- register script message to show menu
+mp.register_script_message("toggle-quality-menu",
+function()
+ if destroyer ~= nil then
+ destroyer()
+ else
+ show_menu()
+ end
+end)
+
+-- keybind to launch menu
+mp.add_key_binding(opts.toggle_menu_binding, "quality-menu", show_menu)
+
+-- special thanks to reload.lua (https://github.com/4e6/mpv-reload/)
+function reload_resume()
+ local playlist_pos = mp.get_property_number("playlist-pos")
+ local reload_duration = mp.get_property_native("duration")
+ local time_pos = mp.get_property("time-pos")
+
+ mp.set_property_number("playlist-pos", playlist_pos)
+
+ -- Tries to determine live stream vs. pre-recordered VOD. VOD has non-zero
+ -- duration property. When reloading VOD, to keep the current time position
+ -- we should provide offset from the start. Stream doesn't have fixed start.
+ -- Decent choice would be to reload stream from it's current 'live' positon.
+ -- That's the reason we don't pass the offset when reloading streams.
+ if reload_duration and reload_duration > 0 then
+ local function seeker()
+ mp.commandv("seek", time_pos, "absolute")
+ mp.unregister_event(seeker)
+ end
+ mp.register_event("file-loaded", seeker)
+ end
+end
diff --git a/mpv/scripts/youtube-serarch.lua b/mpv/scripts/youtube-serarch.lua
new file mode 100644
index 0000000..dfe04c6
--- /dev/null
+++ b/mpv/scripts/youtube-serarch.lua
@@ -0,0 +1,149 @@
+--[[
+ This script allows users to search and open youtube results from within mpv.
+ Available at: https://github.com/CogentRedTester/mpv-scripts
+ Users can open the search page with Y, and use Y again to open a search.
+ Alternatively, Ctrl+y can be used at any time to open a search.
+ Esc can be used to close the page.
+ Enter will open the selected item, Shift+Enter will append the item to the playlist.
+ This script requires that my other scripts `scroll-list` and `user-input` be installed.
+ scroll-list.lua and user-input-module.lua must be in the ~~/script-modules/ directory,
+ while user-input.lua should be loaded by mpv normally.
+ https://github.com/CogentRedTester/mpv-scroll-list
+ https://github.com/CogentRedTester/mpv-user-input
+ This script also requires a youtube API key to be entered.
+ The API key must be passed to the `API_key` script-opt.
+ A personal API key is free and can be created from:
+ https://console.developers.google.com/apis/api/youtube.googleapis.com/
+ The script also requires that curl be in the system path.
+]]--
+
+local mp = require "mp"
+local msg = require "mp.msg"
+local utils = require "mp.utils"
+local opts = require "mp.options"
+
+package.path = mp.command_native({"expand-path", "~~/lua-modules/?.lua;"}) .. package.path
+local ui = require "user-input-module"
+local list = require "scroll-list"
+
+list.header = "Youtube Search Results\\N-----------------------------"
+list.num_entries = 18
+list.list_style = [[{\fs10}\N{\q2\fs25\c&Hffffff&}]]
+
+local o = {
+ API_key = "AIzaSyB3IuyFp8C5SYEgJ0BSGgKZZjfUtuVGl44",
+ num_results = 40
+}
+
+opts.read_options(o)
+
+local ass_escape = list.ass_escape
+
+--taken from: https://gist.github.com/liukun/f9ce7d6d14fa45fe9b924a3eed5c3d99
+local function urlencode(url)
+ if type(url) ~= "string" then return url end
+ url = url:gsub("\n", "\r\n")
+ url = url:gsub("([^%w ])", function (c) string.format("%%%02X", string.byte(c)) end)
+ url = url:gsub(" ", "+")
+ return url
+end
+
+--sends an API request
+local function send_request(type, queries)
+ local url = "https://www.googleapis.com/youtube/v3/"..type
+
+ url = url.."?key="..o.API_key
+
+ for key, value in pairs(queries) do
+ url = url.."&"..key.."="..urlencode(value)
+ end
+
+
+ local request = mp.command_native({
+ name = "subprocess",
+ capture_stdout = true,
+ capture_stderr = true,
+ playback_only = false,
+ args = {"curl", url}
+ })
+
+ local response = utils.parse_json(request.stdout)
+ if request.status ~= 0 then msg.error(request.stderr) ; return nil end
+ return response
+end
+
+local function insert_video(item)
+ list:insert({
+ ass = ("%s {\\c&aaaaaa&}%s"):format(ass_escape(item.snippet.title), ass_escape(item.snippet.channelTitle)),
+ url = "https://www.youtube.com/watch?v="..item.id.videoId
+ })
+end
+
+local function insert_playlist(item)
+ list:insert({
+ ass = ("🖿 %s {\\c&aaaaaa&}%s"):format(ass_escape(item.snippet.title), ass_escape(item.snippet.channelTitle)),
+ url = "https://www.youtube.com/playlist?list="..item.id.playlistId
+ })
+end
+
+local function insert_channel(item)
+ list:insert({
+ ass = ("👤 %s"):format(ass_escape(item.snippet.title)),
+ url = "https://www.youtube.com/channel/"..item.id.channelId
+ })
+end
+
+local function reset_list()
+ list.selected = 1
+ list:clear()
+end
+
+local function search(query)
+ local response = send_request("search", {
+ q = query,
+ part = "id,snippet",
+ maxResults = o.num_results
+ })
+
+ if not response then return end
+ reset_list()
+
+ for _, item in ipairs(response.items) do
+ if item.id.kind == "youtube#video" then
+ insert_video(item)
+ elseif item.id.kind == "youtube#playlist" then
+ insert_playlist(item)
+ elseif item.id.kind == "youtube#channel" then
+ insert_channel(item)
+ end
+ end
+ list.header = "Youtube Search: "..ass_escape(query).."\\N-------------------------------------------------"
+ list:update()
+ list:open()
+end
+
+local function play_result(flag)
+ if not list[list.selected] then return end
+ if flag == "new_window" then mp.commandv("run", "mpv", list[list.selected].url) ; return end
+
+ mp.commandv("loadfile", list[list.selected].url, flag)
+ if flag == "replace" then list:close() end
+end
+
+table.insert(list.keybinds, {"ENTER", "play", function() play_result("replace") end, {}})
+table.insert(list.keybinds, {"Shift+ENTER", "play_append", function() play_result("append-play") end, {}})
+table.insert(list.keybinds, {"Ctrl+ENTER", "play_new_window", function() play_result("new_window") end, {}})
+
+local function open_search_input()
+ ui.get_user_input(function(input)
+ if not input then return end
+ search( input )
+ end)
+end
+
+mp.add_key_binding("Ctrl+y", "yt", open_search_input)
+
+mp.add_key_binding("Y", "youtube-search", function()
+ if not list.hidden then open_search_input()
+ else list:open() end
+end)