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