mirror of https://github.com/Flinner/dots.git
parent
e94a61b04f
commit
a13773dc70
@ -0,0 +1,275 @@ |
||||
-- youtube-quality.lua |
||||
-- Source: https://github.com/jgreco/mpv-youtube-quality/blob/master/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 = "yt-dlp", |
||||
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 |
Loading…
Reference in new issue