dotfiles

My personal shell configs and stuff
git clone git://git.alex.balgavy.eu/dotfiles.git
Log | Files | Refs | Submodules | README | LICENSE

youtube-quality.lua (8902B)


      1 -- youtube-quality.lua
      2 -- https://github.com/jgreco/mpv-youtube-quality
      3 -- Change youtube video quality on the fly.
      4 --
      5 -- Diplays a menu that lets you switch to different ytdl-format settings while
      6 -- you're in the middle of a video (just like you were using the web player).
      7 --
      8 -- Bound to ctrl-f by default.
      9 
     10 local mp = require 'mp'
     11 local utils = require 'mp.utils'
     12 local msg = require 'mp.msg'
     13 local assdraw = require 'mp.assdraw'
     14 
     15 local opts = {
     16     --key bindings
     17     toggle_menu_binding = "ctrl+f",
     18     up_binding = "UP",
     19     down_binding = "DOWN",
     20     select_binding = "ENTER",
     21     ytdl_format = "bestvideo+bestaudio/best",
     22 
     23     --formatting / cursors
     24     selected_and_active     = "▶ - ",
     25     selected_and_inactive   = "● - ",
     26     unselected_and_active   = "▷ - ",
     27     unselected_and_inactive = "○ - ",
     28 
     29 	--font size scales by window, if false requires larger font and padding sizes
     30 	scale_playlist_by_window=false,
     31 
     32     --playlist ass style overrides inside curly brackets, \keyvalue is one field, extra \ for escape in lua
     33     --example {\\fnUbuntu\\fs10\\b0\\bord1} equals: font=Ubuntu, size=10, bold=no, border=1
     34     --read http://docs.aegisub.org/3.2/ASS_Tags/ for reference of tags
     35     --undeclared tags will use default osd settings
     36     --these styles will be used for the whole playlist. More specific styling will need to be hacked in
     37     --
     38     --(a monospaced font is recommended but not required)
     39     style_ass_tags = "{\\fnmonospace}",
     40 
     41     --paddings for top left corner
     42     text_padding_x = 5,
     43     text_padding_y = 5,
     44 
     45     --other
     46     menu_timeout = 10,
     47 
     48     --use youtube-dl to fetch a list of available formats (overrides quality_strings)
     49     fetch_formats = true,
     50 
     51     --default menu entries
     52     quality_strings=[[
     53     [
     54     {"4320p" : "bestvideo[height<=?4320p]+bestaudio/best"},
     55     {"2160p" : "bestvideo[height<=?2160]+bestaudio/best"},
     56     {"1440p" : "bestvideo[height<=?1440]+bestaudio/best"},
     57     {"1080p" : "bestvideo[height<=?1080]+bestaudio/best"},
     58     {"720p" : "bestvideo[height<=?720]+bestaudio/best"},
     59     {"480p" : "bestvideo[height<=?480]+bestaudio/best"},
     60     {"360p" : "bestvideo[height<=?360]+bestaudio/best"},
     61     {"240p" : "bestvideo[height<=?240]+bestaudio/best"},
     62     {"144p" : "bestvideo[height<=?144]+bestaudio/best"}
     63     ]
     64     ]],
     65 }
     66 (require 'mp.options').read_options(opts, "youtube-quality")
     67 opts.quality_strings = utils.parse_json(opts.quality_strings)
     68 
     69 local destroyer = nil
     70 
     71 mp.set_property("ytdl-format",opts.ytdl_format)
     72 
     73 function show_menu()
     74     local selected = 1
     75     local active = 0
     76     local current_ytdl_format = mp.get_property("ytdl-format")
     77     msg.verbose("current ytdl-format: "..current_ytdl_format)
     78     local num_options = 0
     79     local options = {}
     80 
     81 
     82     if opts.fetch_formats then
     83         options, num_options = download_formats()
     84     end
     85 
     86     if next(options) == nil then
     87         for i,v in ipairs(opts.quality_strings) do
     88             num_options = num_options + 1
     89             for k,v2 in pairs(v) do
     90                 options[i] = {label = k, format=v2}
     91                 if v2 == current_ytdl_format then
     92                     active = i
     93                     selected = active
     94                 end
     95             end
     96         end
     97     end
     98 
     99     --set the cursor to the currently format
    100     for i,v in ipairs(options) do
    101         if v.format == current_ytdl_format then
    102             active = i
    103             selected = active
    104             break
    105         end
    106     end
    107 
    108     function selected_move(amt)
    109         selected = selected + amt
    110         if selected < 1 then selected = num_options
    111         elseif selected > num_options then selected = 1 end
    112         timeout:kill()
    113         timeout:resume()
    114         draw_menu()
    115     end
    116     function choose_prefix(i)
    117         if     i == selected and i == active then return opts.selected_and_active
    118         elseif i == selected then return opts.selected_and_inactive end
    119 
    120         if     i ~= selected and i == active then return opts.unselected_and_active
    121         elseif i ~= selected then return opts.unselected_and_inactive end
    122         return "> " --shouldn't get here.
    123     end
    124 
    125     function draw_menu()
    126         local ass = assdraw.ass_new()
    127 
    128         ass:pos(opts.text_padding_x, opts.text_padding_y)
    129         ass:append(opts.style_ass_tags)
    130 
    131         for i,v in ipairs(options) do
    132             ass:append(choose_prefix(i)..v.label.."\\N")
    133         end
    134 
    135 		local w, h = mp.get_osd_size()
    136 		if opts.scale_playlist_by_window then w,h = 0, 0 end
    137 		mp.set_osd_ass(w, h, ass.text)
    138     end
    139 
    140     function destroy()
    141         timeout:kill()
    142         mp.set_osd_ass(0,0,"")
    143         mp.remove_key_binding("move_up")
    144         mp.remove_key_binding("move_down")
    145         mp.remove_key_binding("select")
    146         mp.remove_key_binding("escape")
    147         destroyer = nil
    148     end
    149     timeout = mp.add_periodic_timer(opts.menu_timeout, destroy)
    150     destroyer = destroy
    151 
    152     mp.add_forced_key_binding(opts.up_binding,     "move_up",   function() selected_move(-1) end, {repeatable=true})
    153     mp.add_forced_key_binding(opts.down_binding,   "move_down", function() selected_move(1)  end, {repeatable=true})
    154     mp.add_forced_key_binding(opts.select_binding, "select",    function()
    155         destroy()
    156         mp.set_property("ytdl-format", options[selected].format)
    157         reload_resume()
    158     end)
    159     mp.add_forced_key_binding(opts.toggle_menu_binding, "escape", destroy)
    160 
    161     draw_menu()
    162     return
    163 end
    164 
    165 local ytdl = {
    166     path = "youtube-dl",
    167     searched = false,
    168     blacklisted = {}
    169 }
    170 
    171 format_cache={}
    172 function download_formats()
    173     local function exec(args)
    174         local ret = utils.subprocess({args = args})
    175         return ret.status, ret.stdout, ret
    176     end
    177 
    178     local function table_size(t)
    179         s = 0
    180         for i,v in ipairs(t) do
    181             s = s+1
    182         end
    183         return s
    184     end
    185 
    186     local url = mp.get_property("path")
    187 
    188     url = string.gsub(url, "ytdl://", "") -- Strip possible ytdl:// prefix.
    189 
    190     -- don't fetch the format list if we already have it
    191     if format_cache[url] ~= nil then
    192         local res = format_cache[url]
    193         return res, table_size(res)
    194     end
    195     mp.osd_message("fetching available formats with youtube-dl...", 60)
    196 
    197     if not (ytdl.searched) then
    198         local ytdl_mcd = mp.find_config_file("youtube-dl")
    199         if not (ytdl_mcd == nil) then
    200             msg.verbose("found youtube-dl at: " .. ytdl_mcd)
    201             ytdl.path = ytdl_mcd
    202         end
    203         ytdl.searched = true
    204     end
    205 
    206     local command = {ytdl.path, "--no-warnings", "--no-playlist", "-J"}
    207     table.insert(command, url)
    208     local es, json, result = exec(command)
    209 
    210     if (es < 0) or (json == nil) or (json == "") then
    211         mp.osd_message("fetching formats failed...", 1)
    212         msg.error("failed to get format list: " .. err)
    213         return {}, 0
    214     end
    215 
    216     local json, err = utils.parse_json(json)
    217 
    218     if (json == nil) then
    219         mp.osd_message("fetching formats failed...", 1)
    220         msg.error("failed to parse JSON data: " .. err)
    221         return {}, 0
    222     end
    223 
    224     res = {}
    225     msg.verbose("youtube-dl succeeded!")
    226     for i,v in ipairs(json.formats) do
    227         if v.vcodec ~= "none" then
    228             local fps = v.fps and v.fps.."fps" or ""
    229             local resolution = string.format("%sx%s", v.width, v.height)
    230             local l = string.format("%-9s %-5s (%-4s / %s)", resolution, fps, v.ext, v.vcodec)
    231             local f = string.format("%s+bestaudio/best", v.format_id)
    232             table.insert(res, {label=l, format=f, width=v.width })
    233         end
    234     end
    235 
    236     table.sort(res, function(a, b) return a.width > b.width end)
    237 
    238     mp.osd_message("", 0)
    239     format_cache[url] = res
    240     return res, table_size(res)
    241 end
    242 
    243 
    244 -- register script message to show menu
    245 mp.register_script_message("toggle-quality-menu",
    246 function()
    247     if destroyer ~= nil then
    248         destroyer()
    249     else
    250         show_menu()
    251     end
    252 end)
    253 
    254 -- keybind to launch menu
    255 mp.add_key_binding(opts.toggle_menu_binding, "quality-menu", show_menu)
    256 
    257 -- special thanks to reload.lua (https://github.com/4e6/mpv-reload/)
    258 function reload_resume()
    259     local playlist_pos = mp.get_property_number("playlist-pos")
    260     local reload_duration = mp.get_property_native("duration")
    261     local time_pos = mp.get_property("time-pos")
    262 
    263     mp.set_property_number("playlist-pos", playlist_pos)
    264 
    265     -- Tries to determine live stream vs. pre-recordered VOD. VOD has non-zero
    266     -- duration property. When reloading VOD, to keep the current time position
    267     -- we should provide offset from the start. Stream doesn't have fixed start.
    268     -- Decent choice would be to reload stream from it's current 'live' positon.
    269     -- That's the reason we don't pass the offset when reloading streams.
    270     if reload_duration and reload_duration > 0 then
    271         local function seeker()
    272             mp.commandv("seek", time_pos, "absolute")
    273             mp.unregister_event(seeker)
    274         end
    275         mp.register_event("file-loaded", seeker)
    276     end
    277 end