1local utils = require 'mp.utils'
2local msg = require 'mp.msg'
3local options = require 'mp.options'
4
5local o = {
6 exclude = "",
7 try_ytdl_first = false,
8 use_manifests = false,
9 all_formats = false,
10 force_all_formats = true,
11 ytdl_path = "youtube-dl",
12}
13
14local ytdl = {
15 path = nil,
16 searched = false,
17 blacklisted = {}
18}
19
20options.read_options(o, nil, function()
21 ytdl.blacklisted = {} -- reparse o.exclude next time
22 ytdl.searched = false
23end)
24
25local chapter_list = {}
26
27function Set (t)
28 local set = {}
29 for _, v in pairs(t) do set[v] = true end
30 return set
31end
32
33-- ?: surrogate (keep in mind that there is no lazy evaluation)
34function iif(cond, if_true, if_false)
35 if cond then
36 return if_true
37 end
38 return if_false
39end
40
41local safe_protos = Set {
42 "http", "https", "ftp", "ftps",
43 "rtmp", "rtmps", "rtmpe", "rtmpt", "rtmpts", "rtmpte",
44 "data"
45}
46
47-- For some sites, youtube-dl returns the audio codec (?) only in the "ext" field.
48local ext_map = {
49 ["mp3"] = "mp3",
50 ["opus"] = "opus",
51}
52
53local codec_map = {
54 -- src pattern = mpv codec
55 ["vtt"] = "webvtt",
56 ["opus"] = "opus",
57 ["vp9"] = "vp9",
58 ["avc1%..*"] = "h264",
59 ["av01%..*"] = "av1",
60 ["mp4a%..*"] = "aac",
61}
62
63-- Codec name as reported by youtube-dl mapped to mpv internal codec names.
64-- Fun fact: mpv will not really use the codec, but will still try to initialize
65-- the codec on track selection (just to scrap it), meaning it's only a hint,
66-- but one that may make initialization fail. On the other hand, if the codec
67-- is valid but completely different from the actual media, nothing bad happens.
68local function map_codec_to_mpv(codec)
69 if codec == nil then
70 return nil
71 end
72 for k, v in pairs(codec_map) do
73 local s, e = codec:find(k)
74 if s == 1 and e == #codec then
75 return v
76 end
77 end
78 return nil
79end
80
81local function exec(args)
82 local ret = mp.command_native({name = "subprocess",
83 args = args,
84 capture_stdout = true,
85 capture_stderr = true})
86 return ret.status, ret.stdout, ret, ret.killed_by_us
87end
88
89-- return true if it was explicitly set on the command line
90local function option_was_set(name)
91 return mp.get_property_bool("option-info/" ..name.. "/set-from-commandline",
92 false)
93end
94
95-- return true if the option was set locally
96local function option_was_set_locally(name)
97 return mp.get_property_bool("option-info/" ..name.. "/set-locally", false)
98end
99
100-- youtube-dl may set special http headers for some sites (user-agent, cookies)
101local function set_http_headers(http_headers)
102 if not http_headers then
103 return
104 end
105 local headers = {}
106 local useragent = http_headers["User-Agent"]
107 if useragent and not option_was_set("user-agent") then
108 mp.set_property("file-local-options/user-agent", useragent)
109 end
110 local additional_fields = {"Cookie", "Referer", "X-Forwarded-For"}
111 for idx, item in pairs(additional_fields) do
112 local field_value = http_headers[item]
113 if field_value then
114 headers[#headers + 1] = item .. ": " .. field_value
115 end
116 end
117 if #headers > 0 and not option_was_set("http-header-fields") then
118 mp.set_property_native("file-local-options/http-header-fields", headers)
119 end
120end
121
122local function append_libav_opt(props, name, value)
123 if not props then
124 props = {}
125 end
126
127 if name and value and not props[name] then
128 props[name] = value
129 end
130
131 return props
132end
133
134local function edl_escape(url)
135 return "%" .. string.len(url) .. "%" .. url
136end
137
138local function url_is_safe(url)
139 local proto = type(url) == "string" and url:match("^(.+)://") or nil
140 local safe = proto and safe_protos[proto]
141 if not safe then
142 msg.error(("Ignoring potentially unsafe url: '%s'"):format(url))
143 end
144 return safe
145end
146
147local function time_to_secs(time_string)
148 local ret
149
150 local a, b, c = time_string:match("(%d+):(%d%d?):(%d%d)")
151 if a ~= nil then
152 ret = (a*3600 + b*60 + c)
153 else
154 a, b = time_string:match("(%d%d?):(%d%d)")
155 if a ~= nil then
156 ret = (a*60 + b)
157 end
158 end
159
160 return ret
161end
162
163local function extract_chapters(data, video_length)
164 local ret = {}
165
166 for line in data:gmatch("[^\r\n]+") do
167 local time = time_to_secs(line)
168 if time and (time < video_length) then
169 table.insert(ret, {time = time, title = line})
170 end
171 end
172 table.sort(ret, function(a, b) return a.time < b.time end)
173 return ret
174end
175
176local function is_blacklisted(url)
177 if o.exclude == "" then return false end
178 if #ytdl.blacklisted == 0 then
179 local joined = o.exclude
180 while joined:match('%|?[^|]+') do
181 local _, e, substring = joined:find('%|?([^|]+)')
182 table.insert(ytdl.blacklisted, substring)
183 joined = joined:sub(e+1)
184 end
185 end
186 if #ytdl.blacklisted > 0 then
187 url = url:match('https?://(.+)')
188 for _, exclude in ipairs(ytdl.blacklisted) do
189 if url:match(exclude) then
190 msg.verbose('URL matches excluded substring. Skipping.')
191 return true
192 end
193 end
194 end
195 return false
196end
197
198local function parse_yt_playlist(url, json)
199 -- return 0-based index to use with --playlist-start
200
201 if not json.extractor or json.extractor ~= "youtube:playlist" then
202 return nil
203 end
204
205 local query = url:match("%?.+")
206 if not query then return nil end
207
208 local args = {}
209 for arg, param in query:gmatch("(%a+)=([^&?]+)") do
210 if arg and param then
211 args[arg] = param
212 end
213 end
214
215 local maybe_idx = tonumber(args["index"])
216
217 -- if index matches v param it's probably the requested item
218 if maybe_idx and #json.entries >= maybe_idx and
219 json.entries[maybe_idx].id == args["v"] then
220 msg.debug("index matches requested video")
221 return maybe_idx - 1
222 end
223
224 -- if there's no index or it doesn't match, look for video
225 for i = 1, #json.entries do
226 if json.entries[i] == args["v"] then
227 msg.debug("found requested video in index " .. (i - 1))
228 return i - 1
229 end
230 end
231
232 msg.debug("requested video not found in playlist")
233 -- if item isn't on the playlist, give up
234 return nil
235end
236
237local function make_absolute_url(base_url, url)
238 if url:find("https?://") == 1 then return url end
239
240 local proto, domain, rest =
241 base_url:match("(https?://)([^/]+/)(.*)/?")
242 local segs = {}
243 rest:gsub("([^/]+)", function(c) table.insert(segs, c) end)
244 url:gsub("([^/]+)", function(c) table.insert(segs, c) end)
245 local resolved_url = {}
246 for i, v in ipairs(segs) do
247 if v == ".." then
248 table.remove(resolved_url)
249 elseif v ~= "." then
250 table.insert(resolved_url, v)
251 end
252 end
253 return proto .. domain ..
254 table.concat(resolved_url, "/")
255end
256
257local function join_url(base_url, fragment)
258 local res = ""
259 if base_url and fragment.path then
260 res = make_absolute_url(base_url, fragment.path)
261 elseif fragment.url then
262 res = fragment.url
263 end
264 return res
265end
266
267local function edl_track_joined(fragments, protocol, is_live, base)
268 if not (type(fragments) == "table") or not fragments[1] then
269 msg.debug("No fragments to join into EDL")
270 return nil
271 end
272
273 local edl = "edl://"
274 local offset = 1
275 local parts = {}
276
277 if (protocol == "http_dash_segments") and not is_live then
278 msg.debug("Using dash")
279 local args = ""
280
281 -- assume MP4 DASH initialization segment
282 if not fragments[1].duration then
283 msg.debug("Using init segment")
284 args = args .. ",init=" .. edl_escape(join_url(base, fragments[1]))
285 offset = 2
286 end
287
288 table.insert(parts, "!mp4_dash" .. args)
289
290 -- Check remaining fragments for duration;
291 -- if not available in all, give up.
292 for i = offset, #fragments do
293 if not fragments[i].duration then
294 msg.error("EDL doesn't support fragments" ..
295 "without duration with MP4 DASH")
296 return nil
297 end
298 end
299 end
300
301 for i = offset, #fragments do
302 local fragment = fragments[i]
303 if not url_is_safe(join_url(base, fragment)) then
304 return nil
305 end
306 table.insert(parts, edl_escape(join_url(base, fragment)))
307 if fragment.duration then
308 parts[#parts] =
309 parts[#parts] .. ",length="..fragment.duration
310 end
311 end
312 return edl .. table.concat(parts, ";") .. ";"
313end
314
315local function has_native_dash_demuxer()
316 local demuxers = mp.get_property_native("demuxer-lavf-list", {})
317 for _, v in ipairs(demuxers) do
318 if v == "dash" then
319 return true
320 end
321 end
322 return false
323end
324
325local function valid_manifest(json)
326 local reqfmt = json["requested_formats"] and json["requested_formats"][1] or {}
327 if not reqfmt["manifest_url"] and not json["manifest_url"] then
328 return false
329 end
330 local proto = reqfmt["protocol"] or json["protocol"] or ""
331 return (proto == "http_dash_segments" and has_native_dash_demuxer()) or
332 proto:find("^m3u8")
333end
334
335local function as_integer(v, def)
336 def = def or 0
337 local num = math.floor(tonumber(v) or def)
338 if num > -math.huge and num < math.huge then
339 return num
340 end
341 return def
342end
343
344-- Convert a format list from youtube-dl to an EDL URL, or plain URL.
345-- json: full json blob by youtube-dl
346-- formats: format list by youtube-dl
347-- use_all_formats: if=true, then formats is the full format list, and the
348-- function will attempt to return them as delay-loaded tracks
349-- See res table initialization in the function for result type.
350local function formats_to_edl(json, formats, use_all_formats)
351 local res = {
352 -- the media URL, which may be EDL
353 url = nil,
354 -- for use_all_formats=true: whether any muxed formats are present, and
355 -- at the same time the separate EDL parts don't have both audio/video
356 muxed_needed = false,
357 }
358
359 local default_formats = {}
360 local requested_formats = json["requested_formats"]
361 if use_all_formats and requested_formats then
362 for _, track in ipairs(requested_formats) do
363 local id = track["format_id"]
364 if id then
365 default_formats[id] = true
366 end
367 end
368 end
369
370 local duration = as_integer(json["duration"])
371 local single_url = nil
372 local streams = {}
373
374 local tbr_only = true
375 for index, track in ipairs(formats) do
376 tbr_only = tbr_only and track["tbr"] and
377 (not track["abr"]) and (not track["vbr"])
378 end
379
380 for index, track in ipairs(formats) do
381 local edl_track = nil
382 edl_track = edl_track_joined(track.fragments,
383 track.protocol, json.is_live,
384 track.fragment_base_url)
385 if not edl_track and not url_is_safe(track.url) then
386 return nil
387 end
388
389 local tracks = {}
390 if track.vcodec and track.vcodec ~= "none" then
391 tracks[#tracks + 1] = {
392 media_type = "video",
393 codec = map_codec_to_mpv(track.vcodec),
394 }
395 end
396 -- Tries to follow the strange logic that vcodec unset means it's
397 -- an audio stream, even if acodec is sometimes unset.
398 if (#tracks == 0) or (track.acodec and track.acodec ~= "none") then
399 tracks[#tracks + 1] = {
400 media_type = "audio",
401 codec = map_codec_to_mpv(track.acodec) or
402 ext_map[track.ext],
403 }
404 end
405 if #tracks == 0 then
406 return nil
407 end
408
409 local url = edl_track or track.url
410 local hdr = {"!new_stream", "!no_clip", "!no_chapters"}
411 local skip = false
412 local params = ""
413
414 if use_all_formats then
415 for _, sub in ipairs(tracks) do
416 -- A single track that is either audio or video. Delay load it.
417 local props = ""
418 if sub.media_type == "video" then
419 props = props .. ",w=" .. as_integer(track.width)
420 .. ",h=" .. as_integer(track.height)
421 .. ",fps=" .. as_integer(track.fps)
422 elseif sub.media_type == "audio" then
423 props = props .. ",samplerate=" .. as_integer(track.asr)
424 end
425 hdr[#hdr + 1] = "!delay_open,media_type=" .. sub.media_type ..
426 ",codec=" .. (sub.codec or "null") .. props
427
428 -- Add bitrate information etc. for better user selection.
429 local byterate = 0
430 local rates = {"tbr", "vbr", "abr"}
431 if #tracks > 1 then
432 rates = {({video = "vbr", audio = "abr"})[sub.media_type]}
433 end
434 if tbr_only then
435 rates = {"tbr"}
436 end
437 for _, f in ipairs(rates) do
438 local br = as_integer(track[f])
439 if br > 0 then
440 byterate = math.floor(br * 1000 / 8)
441 break
442 end
443 end
444 local title = track.format or track.format_note or ""
445 if #tracks > 1 then
446 if #title > 0 then
447 title = title .. " "
448 end
449 title = title .. "muxed-" .. index
450 end
451 local flags = {}
452 if default_formats[track["format_id"]] then
453 flags[#flags + 1] = "default"
454 end
455 hdr[#hdr + 1] = "!track_meta,title=" ..
456 edl_escape(title) .. ",byterate=" .. byterate ..
457 iif(#flags > 0, ",flags=" .. table.concat(flags, "+"), "")
458 end
459
460 if duration > 0 then
461 params = params .. ",length=" .. duration
462 end
463 end
464
465 hdr[#hdr + 1] = edl_escape(url) .. params
466
467 streams[#streams + 1] = table.concat(hdr, ";")
468 -- In case there is only 1 of these streams.
469 -- Note: assumes it has no important EDL headers
470 single_url = url
471 end
472
473 -- Merge all tracks into a single virtual file, but avoid EDL if it's
474 -- only a single track (i.e. redundant).
475 if #streams == 1 and single_url then
476 res.url = single_url
477 elseif #streams > 0 then
478 res.url = "edl://" .. table.concat(streams, ";")
479 else
480 return nil
481 end
482
483 return res
484end
485
486local function add_single_video(json)
487 local streamurl = ""
488 local format_info = ""
489 local max_bitrate = 0
490 local requested_formats = json["requested_formats"]
491 local all_formats = json["formats"]
492
493 if o.use_manifests and valid_manifest(json) then
494 -- prefer manifest_url if present
495 format_info = "manifest"
496
497 local mpd_url = requested_formats and
498 requested_formats[1]["manifest_url"] or json["manifest_url"]
499 if not mpd_url then
500 msg.error("No manifest URL found in JSON data.")
501 return
502 elseif not url_is_safe(mpd_url) then
503 return
504 end
505
506 streamurl = mpd_url
507
508 if requested_formats then
509 for _, track in pairs(requested_formats) do
510 max_bitrate = (track.tbr and track.tbr > max_bitrate) and
511 track.tbr or max_bitrate
512 end
513 elseif json.tbr then
514 max_bitrate = json.tbr > max_bitrate and json.tbr or max_bitrate
515 end
516 end
517
518 if streamurl == "" then
519 -- possibly DASH/split tracks
520 local res = nil
521 local has_requested_formats = requested_formats and #requested_formats > 0
522
523 -- Not having requested_formats usually hints to HLS master playlist
524 -- usage, which we don't want to split off, at least not yet.
525 if (all_formats and o.all_formats) and
526 (has_requested_formats or o.force_all_formats)
527 then
528 format_info = "all_formats (separate)"
529 res = formats_to_edl(json, all_formats, true)
530 -- Note: since we don't delay-load muxed streams, use normal stream
531 -- selection if we have to use muxed streams.
532 if res and res.muxed_needed then
533 res = nil
534 end
535 end
536
537 if (not res) and has_requested_formats then
538 format_info = "youtube-dl (separate)"
539 res = formats_to_edl(json, requested_formats, false)
540 end
541
542 if res then
543 streamurl = res.url
544 end
545 end
546
547 if streamurl == "" and json.url then
548 format_info = "youtube-dl (single)"
549 local edl_track = nil
550 edl_track = edl_track_joined(json.fragments, json.protocol,
551 json.is_live, json.fragment_base_url)
552
553 if not edl_track and not url_is_safe(json.url) then
554 return
555 end
556 -- normal video or single track
557 streamurl = edl_track or json.url
558 set_http_headers(json.http_headers)
559 end
560
561 if streamurl == "" then
562 msg.error("No URL found in JSON data.")
563 return
564 end
565
566 msg.verbose("format selection: " .. format_info)
567 msg.debug("streamurl: " .. streamurl)
568
569 mp.set_property("stream-open-filename", streamurl:gsub("^data:", "data://", 1))
570
571 mp.set_property("file-local-options/force-media-title", json.title)
572
573 -- set hls-bitrate for dash track selection
574 if max_bitrate > 0 and
575 not option_was_set("hls-bitrate") and
576 not option_was_set_locally("hls-bitrate") then
577 mp.set_property_native('file-local-options/hls-bitrate', max_bitrate*1000)
578 end
579
580 -- add subtitles
581 if not (json.requested_subtitles == nil) then
582 local subs = {}
583 for lang, info in pairs(json.requested_subtitles) do
584 subs[#subs + 1] = {lang = lang or "-", info = info}
585 end
586 table.sort(subs, function(a, b) return a.lang < b.lang end)
587 for _, e in ipairs(subs) do
588 local lang, sub_info = e.lang, e.info
589 msg.verbose("adding subtitle ["..lang.."]")
590
591 local sub = nil
592
593 if not (sub_info.data == nil) then
594 sub = "memory://"..sub_info.data
595 elseif not (sub_info.url == nil) and
596 url_is_safe(sub_info.url) then
597 sub = sub_info.url
598 end
599
600 if not (sub == nil) then
601 local edl = "edl://!no_clip;!delay_open,media_type=sub"
602 local codec = map_codec_to_mpv(sub_info.ext)
603 if codec then
604 edl = edl .. ",codec=" .. codec
605 end
606 edl = edl .. ";" .. edl_escape(sub)
607 mp.commandv("sub-add", edl, "auto", sub_info.ext, lang)
608 else
609 msg.verbose("No subtitle data/url for ["..lang.."]")
610 end
611 end
612 end
613
614 -- add chapters
615 if json.chapters then
616 msg.debug("Adding pre-parsed chapters")
617 for i = 1, #json.chapters do
618 local chapter = json.chapters[i]
619 local title = chapter.title or ""
620 if title == "" then
621 title = string.format('Chapter %02d', i)
622 end
623 table.insert(chapter_list, {time=chapter.start_time, title=title})
624 end
625 elseif not (json.description == nil) and not (json.duration == nil) then
626 chapter_list = extract_chapters(json.description, json.duration)
627 end
628
629 -- set start time
630 if not (json.start_time == nil) and
631 not option_was_set("start") and
632 not option_was_set_locally("start") then
633 msg.debug("Setting start to: " .. json.start_time .. " secs")
634 mp.set_property("file-local-options/start", json.start_time)
635 end
636
637 -- set aspect ratio for anamorphic video
638 if not (json.stretched_ratio == nil) and
639 not option_was_set("video-aspect-override") then
640 mp.set_property('file-local-options/video-aspect-override', json.stretched_ratio)
641 end
642
643 local stream_opts = mp.get_property_native("file-local-options/stream-lavf-o", {})
644
645 -- for rtmp
646 if (json.protocol == "rtmp") then
647 stream_opts = append_libav_opt(stream_opts,
648 "rtmp_tcurl", streamurl)
649 stream_opts = append_libav_opt(stream_opts,
650 "rtmp_pageurl", json.page_url)
651 stream_opts = append_libav_opt(stream_opts,
652 "rtmp_playpath", json.play_path)
653 stream_opts = append_libav_opt(stream_opts,
654 "rtmp_swfverify", json.player_url)
655 stream_opts = append_libav_opt(stream_opts,
656 "rtmp_swfurl", json.player_url)
657 stream_opts = append_libav_opt(stream_opts,
658 "rtmp_app", json.app)
659 end
660
661 if json.proxy and json.proxy ~= "" then
662 stream_opts = append_libav_opt(stream_opts,
663 "http_proxy", json.proxy)
664 end
665
666 mp.set_property_native("file-local-options/stream-lavf-o", stream_opts)
667end
668
669local function check_version(ytdl_path)
670 local command = {
671 name = "subprocess",
672 capture_stdout = true,
673 args = {ytdl_path, "--version"}
674 }
675 local version_string = mp.command_native(command).stdout
676 local year, month, day = string.match(version_string, "(%d+).(%d+).(%d+)")
677
678 -- sanity check
679 if (tonumber(year) < 2000) or (tonumber(month) > 12) or
680 (tonumber(day) > 31) then
681 return
682 end
683 local version_ts = os.time{year=year, month=month, day=day}
684 if (os.difftime(os.time(), version_ts) > 60*60*24*90) then
685 msg.warn("It appears that your youtube-dl version is severely out of date.")
686 end
687end
688
689function run_ytdl_hook(url)
690 local start_time = os.clock()
691
692 -- check for youtube-dl in mpv's config dir
693 if not (ytdl.searched) then
694 local exesuf = (package.config:sub(1,1) == '\\') and '.exe' or ''
695 local ytdl_mcd = mp.find_config_file(o.ytdl_path .. exesuf)
696 if ytdl_mcd == nil then
697 msg.verbose("No youtube-dl found with path "..o.ytdl_path..exesuf.." in config directories")
698 ytdl.path = o.ytdl_path
699 else
700 msg.verbose("found youtube-dl at: " .. ytdl_mcd)
701 ytdl.path = ytdl_mcd
702 end
703 ytdl.searched = true
704 end
705
706 -- strip ytdl://
707 if (url:find("ytdl://") == 1) then
708 url = url:sub(8)
709 end
710
711 local format = mp.get_property("options/ytdl-format")
712 local raw_options = mp.get_property_native("options/ytdl-raw-options")
713 local allsubs = true
714 local proxy = nil
715 local use_playlist = false
716
717 local command = {
718 ytdl.path, "--no-warnings", "-J", "--flat-playlist",
719 "--sub-format", "ass/srt/best"
720 }
721
722 -- Checks if video option is "no", change format accordingly,
723 -- but only if user didn't explicitly set one
724 if (mp.get_property("options/vid") == "no") and (#format == 0) then
725 format = "bestaudio/best"
726 msg.verbose("Video disabled. Only using audio")
727 end
728
729 if (format == "") then
730 format = "bestvideo+bestaudio/best"
731 end
732
733 if format ~= "ytdl" then
734 table.insert(command, "--format")
735 table.insert(command, format)
736 end
737
738 for param, arg in pairs(raw_options) do
739 table.insert(command, "--" .. param)
740 if (arg ~= "") then
741 table.insert(command, arg)
742 end
743 if (param == "sub-lang") and (arg ~= "") then
744 allsubs = false
745 elseif (param == "proxy") and (arg ~= "") then
746 proxy = arg
747 elseif (param == "yes-playlist") then
748 use_playlist = true
749 end
750 end
751
752 if (allsubs == true) then
753 table.insert(command, "--all-subs")
754 end
755 if not use_playlist then
756 table.insert(command, "--no-playlist")
757 end
758 table.insert(command, "--")
759 table.insert(command, url)
760 msg.debug("Running: " .. table.concat(command,' '))
761 local es, json, result, aborted = exec(command)
762
763 if aborted then
764 return
765 end
766
767 if (es < 0) or (json == nil) or (json == "") then
768 -- trim our stderr to avoid spurious newlines
769 ytdl_err = result.stderr:gsub("^%s*(.-)%s*$", "%1")
770 msg.error(ytdl_err)
771 local err = "youtube-dl failed: "
772 if result.error_string and result.error_string == "init" then
773 err = err .. "not found or not enough permissions"
774 elseif not result.killed_by_us then
775 err = err .. "unexpected error occurred"
776 else
777 err = string.format("%s returned '%d'", err, es)
778 end
779 msg.error(err)
780 if string.find(ytdl_err, "yt%-dl%.org/bug") then
781 check_version(ytdl.path)
782 end
783 return
784 end
785
786 local json, err = utils.parse_json(json)
787
788 if (json == nil) then
789 msg.error("failed to parse JSON data: " .. err)
790 check_version(ytdl.path)
791 return
792 end
793
794 msg.verbose("youtube-dl succeeded!")
795 msg.debug('ytdl parsing took '..os.clock()-start_time..' seconds')
796
797 json["proxy"] = json["proxy"] or proxy
798
799 -- what did we get?
800 if json["direct"] then
801 -- direct URL, nothing to do
802 msg.verbose("Got direct URL")
803 return
804 elseif (json["_type"] == "playlist")
805 or (json["_type"] == "multi_video") then
806 -- a playlist
807
808 if (#json.entries == 0) then
809 msg.warn("Got empty playlist, nothing to play.")
810 return
811 end
812
813 local self_redirecting_url =
814 json.entries[1]["_type"] ~= "url_transparent" and
815 json.entries[1]["webpage_url"] and
816 json.entries[1]["webpage_url"] == json["webpage_url"]
817
818
819 -- some funky guessing to detect multi-arc videos
820 if self_redirecting_url and #json.entries > 1
821 and json.entries[1].protocol == "m3u8_native"
822 and json.entries[1].url then
823 msg.verbose("multi-arc video detected, building EDL")
824
825 local playlist = edl_track_joined(json.entries)
826
827 msg.debug("EDL: " .. playlist)
828
829 if not playlist then
830 return
831 end
832
833 -- can't change the http headers for each entry, so use the 1st
834 set_http_headers(json.entries[1].http_headers)
835
836 mp.set_property("stream-open-filename", playlist)
837 if not (json.title == nil) then
838 mp.set_property("file-local-options/force-media-title",
839 json.title)
840 end
841
842 -- there might not be subs for the first segment
843 local entry_wsubs = nil
844 for i, entry in pairs(json.entries) do
845 if not (entry.requested_subtitles == nil) then
846 entry_wsubs = i
847 break
848 end
849 end
850
851 if not (entry_wsubs == nil) and
852 not (json.entries[entry_wsubs].duration == nil) then
853 for j, req in pairs(json.entries[entry_wsubs].requested_subtitles) do
854 local subfile = "edl://"
855 for i, entry in pairs(json.entries) do
856 if not (entry.requested_subtitles == nil) and
857 not (entry.requested_subtitles[j] == nil) and
858 url_is_safe(entry.requested_subtitles[j].url) then
859 subfile = subfile..edl_escape(entry.requested_subtitles[j].url)
860 else
861 subfile = subfile..edl_escape("memory://WEBVTT")
862 end
863 subfile = subfile..",length="..entry.duration..";"
864 end
865 msg.debug(j.." sub EDL: "..subfile)
866 mp.commandv("sub-add", subfile, "auto", req.ext, j)
867 end
868 end
869
870 elseif self_redirecting_url and #json.entries == 1 then
871 msg.verbose("Playlist with single entry detected.")
872 add_single_video(json.entries[1])
873 else
874 local playlist_index = parse_yt_playlist(url, json)
875 local playlist = {"#EXTM3U"}
876 for i, entry in pairs(json.entries) do
877 local site = entry.url
878 local title = entry.title
879
880 if not (title == nil) then
881 title = string.gsub(title, '%s+', ' ')
882 table.insert(playlist, "#EXTINF:0," .. title)
883 end
884
885 --[[ some extractors will still return the full info for
886 all clips in the playlist and the URL will point
887 directly to the file in that case, which we don't
888 want so get the webpage URL instead, which is what
889 we want, but only if we aren't going to trigger an
890 infinite loop
891 --]]
892 if entry["webpage_url"] and not self_redirecting_url then
893 site = entry["webpage_url"]
894 end
895
896 -- links without protocol as returned by --flat-playlist
897 if not site:find("://") then
898 -- youtube extractor provides only IDs,
899 -- others come prefixed with the extractor name and ":"
900 local prefix = site:find(":") and "ytdl://" or
901 "https://youtu.be/"
902 table.insert(playlist, prefix .. site)
903 elseif url_is_safe(site) then
904 table.insert(playlist, site)
905 end
906
907 end
908
909 if use_playlist and
910 not option_was_set("playlist-start") and playlist_index then
911 mp.set_property_number("playlist-start", playlist_index)
912 end
913
914 mp.set_property("stream-open-filename", "memory://" .. table.concat(playlist, "\n"))
915 end
916
917 else -- probably a video
918 add_single_video(json)
919 end
920 msg.debug('script running time: '..os.clock()-start_time..' seconds')
921end
922
923if (not o.try_ytdl_first) then
924 mp.add_hook("on_load", 10, function ()
925 msg.verbose('ytdl:// hook')
926 local url = mp.get_property("stream-open-filename", "")
927 if not (url:find("ytdl://") == 1) then
928 msg.verbose('not a ytdl:// url')
929 return
930 end
931 run_ytdl_hook(url)
932 end)
933end
934
935mp.add_hook(o.try_ytdl_first and "on_load" or "on_load_fail", 10, function()
936 msg.verbose('full hook')
937 local url = mp.get_property("stream-open-filename", "")
938 if not (url:find("ytdl://") == 1) and
939 not ((url:find("https?://") == 1) and not is_blacklisted(url)) then
940 return
941 end
942 run_ytdl_hook(url)
943end)
944
945mp.add_hook("on_preloaded", 10, function ()
946 if next(chapter_list) ~= nil then
947 msg.verbose("Setting chapters")
948
949 mp.set_property_native("chapter-list", chapter_list)
950 chapter_list = {}
951 end
952end)