1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057 |
- local utils = require 'mp.utils'
- local msg = require 'mp.msg'
- local options = require 'mp.options'
- local o = {
- exclude = "",
- try_ytdl_first = false,
- use_manifests = false,
- all_formats = false,
- force_all_formats = true,
- ytdl_path = "",
- }
- local ytdl = {
- path = "",
- paths_to_search = {"yt-dlp", "yt-dlp_x86", "youtube-dl"},
- searched = false,
- blacklisted = {}
- }
- options.read_options(o, nil, function()
- ytdl.blacklisted = {} -- reparse o.exclude next time
- ytdl.searched = false
- end)
- local chapter_list = {}
- function Set (t)
- local set = {}
- for _, v in pairs(t) do set[v] = true end
- return set
- end
- -- ?: surrogate (keep in mind that there is no lazy evaluation)
- function iif(cond, if_true, if_false)
- if cond then
- return if_true
- end
- return if_false
- end
- -- youtube-dl JSON name to mpv tag name
- local tag_list = {
- ["uploader"] = "uploader",
- ["channel_url"] = "channel_url",
- -- these titles tend to be a bit too long, so hide them on the terminal
- -- (default --display-tags does not include this name)
- ["description"] = "ytdl_description",
- -- "title" is handled by force-media-title
- -- tags don't work with all_formats=yes
- }
- local safe_protos = Set {
- "http", "https", "ftp", "ftps",
- "rtmp", "rtmps", "rtmpe", "rtmpt", "rtmpts", "rtmpte",
- "data"
- }
- -- For some sites, youtube-dl returns the audio codec (?) only in the "ext" field.
- local ext_map = {
- ["mp3"] = "mp3",
- ["opus"] = "opus",
- }
- local codec_map = {
- -- src pattern = mpv codec
- ["vtt"] = "webvtt",
- ["opus"] = "opus",
- ["vp9"] = "vp9",
- ["avc1%..*"] = "h264",
- ["av01%..*"] = "av1",
- ["mp4a%..*"] = "aac",
- }
- -- Codec name as reported by youtube-dl mapped to mpv internal codec names.
- -- Fun fact: mpv will not really use the codec, but will still try to initialize
- -- the codec on track selection (just to scrap it), meaning it's only a hint,
- -- but one that may make initialization fail. On the other hand, if the codec
- -- is valid but completely different from the actual media, nothing bad happens.
- local function map_codec_to_mpv(codec)
- if codec == nil then
- return nil
- end
- for k, v in pairs(codec_map) do
- local s, e = codec:find(k)
- if s == 1 and e == #codec then
- return v
- end
- end
- return nil
- end
- local function platform_is_windows()
- return mp.get_property_native("platform") == "windows"
- end
- local function exec(args)
- msg.debug("Running: " .. table.concat(args, " "))
- return mp.command_native({
- name = "subprocess",
- args = args,
- capture_stdout = true,
- capture_stderr = true,
- })
- end
- -- return true if it was explicitly set on the command line
- local function option_was_set(name)
- return mp.get_property_bool("option-info/" ..name.. "/set-from-commandline",
- false)
- end
- -- return true if the option was set locally
- local function option_was_set_locally(name)
- return mp.get_property_bool("option-info/" ..name.. "/set-locally", false)
- end
- -- youtube-dl may set special http headers for some sites (user-agent, cookies)
- local function set_http_headers(http_headers)
- if not http_headers then
- return
- end
- local headers = {}
- local useragent = http_headers["User-Agent"]
- if useragent and not option_was_set("user-agent") then
- mp.set_property("file-local-options/user-agent", useragent)
- end
- local additional_fields = {"Cookie", "Referer", "X-Forwarded-For"}
- for idx, item in pairs(additional_fields) do
- local field_value = http_headers[item]
- if field_value then
- headers[#headers + 1] = item .. ": " .. field_value
- end
- end
- if #headers > 0 and not option_was_set("http-header-fields") then
- mp.set_property_native("file-local-options/http-header-fields", headers)
- end
- end
- local function append_libav_opt(props, name, value)
- if not props then
- props = {}
- end
- if name and value and not props[name] then
- props[name] = value
- end
- return props
- end
- local function edl_escape(url)
- return "%" .. string.len(url) .. "%" .. url
- end
- local function url_is_safe(url)
- local proto = type(url) == "string" and url:match("^(%a[%w+.-]*):") or nil
- local safe = proto and safe_protos[proto]
- if not safe then
- msg.error(("Ignoring potentially unsafe url: '%s'"):format(url))
- end
- return safe
- end
- local function time_to_secs(time_string)
- local ret
- local a, b, c = time_string:match("(%d+):(%d%d?):(%d%d)")
- if a ~= nil then
- ret = (a*3600 + b*60 + c)
- else
- a, b = time_string:match("(%d%d?):(%d%d)")
- if a ~= nil then
- ret = (a*60 + b)
- end
- end
- return ret
- end
- local function extract_chapters(data, video_length)
- local ret = {}
- for line in data:gmatch("[^\r\n]+") do
- local time = time_to_secs(line)
- if time and (time < video_length) then
- table.insert(ret, {time = time, title = line})
- end
- end
- table.sort(ret, function(a, b) return a.time < b.time end)
- return ret
- end
- local function is_blacklisted(url)
- if o.exclude == "" then return false end
- if #ytdl.blacklisted == 0 then
- for match in o.exclude:gmatch('%|?([^|]+)') do
- ytdl.blacklisted[#ytdl.blacklisted + 1] = match
- end
- end
- if #ytdl.blacklisted > 0 then
- url = url:match('https?://(.+)')
- for _, exclude in ipairs(ytdl.blacklisted) do
- if url:match(exclude) then
- msg.verbose('URL matches excluded substring. Skipping.')
- return true
- end
- end
- end
- return false
- end
- local function parse_yt_playlist(url, json)
- -- return 0-based index to use with --playlist-start
- if not json.extractor or
- (json.extractor ~= "youtube:tab" and
- json.extractor ~= "youtube:playlist") then
- return nil
- end
- local query = url:match("%?.+")
- if not query then return nil end
- local args = {}
- for arg, param in query:gmatch("(%a+)=([^&?]+)") do
- if arg and param then
- args[arg] = param
- end
- end
- local maybe_idx = tonumber(args["index"])
- -- if index matches v param it's probably the requested item
- if maybe_idx and #json.entries >= maybe_idx and
- json.entries[maybe_idx].id == args["v"] then
- msg.debug("index matches requested video")
- return maybe_idx - 1
- end
- -- if there's no index or it doesn't match, look for video
- for i = 1, #json.entries do
- if json.entries[i].id == args["v"] then
- msg.debug("found requested video in index " .. (i - 1))
- return i - 1
- end
- end
- msg.debug("requested video not found in playlist")
- -- if item isn't on the playlist, give up
- return nil
- end
- local function make_absolute_url(base_url, url)
- if url:find("https?://") == 1 then return url end
- local proto, domain, rest =
- base_url:match("(https?://)([^/]+/)(.*)/?")
- local segs = {}
- rest:gsub("([^/]+)", function(c) table.insert(segs, c) end)
- url:gsub("([^/]+)", function(c) table.insert(segs, c) end)
- local resolved_url = {}
- for i, v in ipairs(segs) do
- if v == ".." then
- table.remove(resolved_url)
- elseif v ~= "." then
- table.insert(resolved_url, v)
- end
- end
- return proto .. domain ..
- table.concat(resolved_url, "/")
- end
- local function join_url(base_url, fragment)
- local res = ""
- if base_url and fragment.path then
- res = make_absolute_url(base_url, fragment.path)
- elseif fragment.url then
- res = fragment.url
- end
- return res
- end
- local function edl_track_joined(fragments, protocol, is_live, base)
- if not (type(fragments) == "table") or not fragments[1] then
- msg.debug("No fragments to join into EDL")
- return nil
- end
- local edl = "edl://"
- local offset = 1
- local parts = {}
- if (protocol == "http_dash_segments") and not is_live then
- msg.debug("Using dash")
- local args = ""
- -- assume MP4 DASH initialization segment
- if not fragments[1].duration and #fragments > 1 then
- msg.debug("Using init segment")
- args = args .. ",init=" .. edl_escape(join_url(base, fragments[1]))
- offset = 2
- end
- table.insert(parts, "!mp4_dash" .. args)
- -- Check remaining fragments for duration;
- -- if not available in all, give up.
- for i = offset, #fragments do
- if not fragments[i].duration then
- msg.verbose("EDL doesn't support fragments " ..
- "without duration with MP4 DASH")
- return nil
- end
- end
- end
- for i = offset, #fragments do
- local fragment = fragments[i]
- if not url_is_safe(join_url(base, fragment)) then
- return nil
- end
- table.insert(parts, edl_escape(join_url(base, fragment)))
- if fragment.duration then
- parts[#parts] =
- parts[#parts] .. ",length="..fragment.duration
- end
- end
- return edl .. table.concat(parts, ";") .. ";"
- end
- local function has_native_dash_demuxer()
- local demuxers = mp.get_property_native("demuxer-lavf-list", {})
- for _, v in ipairs(demuxers) do
- if v == "dash" then
- return true
- end
- end
- return false
- end
- local function valid_manifest(json)
- local reqfmt = json["requested_formats"] and json["requested_formats"][1] or {}
- if not reqfmt["manifest_url"] and not json["manifest_url"] then
- return false
- end
- local proto = reqfmt["protocol"] or json["protocol"] or ""
- return (proto == "http_dash_segments" and has_native_dash_demuxer()) or
- proto:find("^m3u8")
- end
- local function as_integer(v, def)
- def = def or 0
- local num = math.floor(tonumber(v) or def)
- if num > -math.huge and num < math.huge then
- return num
- end
- return def
- end
- local function tags_to_edl(json)
- local tags = {}
- for json_name, mp_name in pairs(tag_list) do
- local v = json[json_name]
- if v then
- tags[#tags + 1] = mp_name .. "=" .. edl_escape(tostring(v))
- end
- end
- if #tags == 0 then
- return nil
- end
- return "!global_tags," .. table.concat(tags, ",")
- end
- -- Convert a format list from youtube-dl to an EDL URL, or plain URL.
- -- json: full json blob by youtube-dl
- -- formats: format list by youtube-dl
- -- use_all_formats: if=true, then formats is the full format list, and the
- -- function will attempt to return them as delay-loaded tracks
- -- See res table initialization in the function for result type.
- local function formats_to_edl(json, formats, use_all_formats)
- local res = {
- -- the media URL, which may be EDL
- url = nil,
- -- for use_all_formats=true: whether any muxed formats are present, and
- -- at the same time the separate EDL parts don't have both audio/video
- muxed_needed = false,
- }
- local default_formats = {}
- local requested_formats = json["requested_formats"] or json["requested_downloads"]
- if use_all_formats and requested_formats then
- for _, track in ipairs(requested_formats) do
- local id = track["format_id"]
- if id then
- default_formats[id] = true
- end
- end
- end
- local duration = as_integer(json["duration"])
- local single_url = nil
- local streams = {}
- local tbr_only = true
- for index, track in ipairs(formats) do
- tbr_only = tbr_only and track["tbr"] and
- (not track["abr"]) and (not track["vbr"])
- end
- local has_requested_video = false
- local has_requested_audio = false
- -- Web players with quality selection always show the highest quality
- -- option at the top. Since tracks are usually listed with the first
- -- track at the top, that should also be the highest quality track.
- -- yt-dlp/youtube-dl sorts it's formats from worst to best.
- -- Iterate in reverse to get best track first.
- for index = #formats, 1, -1 do
- local track = formats[index]
- local edl_track = nil
- edl_track = edl_track_joined(track.fragments,
- track.protocol, json.is_live,
- track.fragment_base_url)
- if not edl_track and not url_is_safe(track.url) then
- msg.error("No safe URL or supported fragmented stream available")
- return nil
- end
- local is_default = default_formats[track["format_id"]]
- local tracks = {}
- -- "none" means it is not a video
- -- nil means it is unknown
- if (o.force_all_formats or track.vcodec) and track.vcodec ~= "none" then
- tracks[#tracks + 1] = {
- media_type = "video",
- codec = map_codec_to_mpv(track.vcodec),
- }
- if is_default then
- has_requested_video = true
- end
- end
- if (o.force_all_formats or track.acodec) and track.acodec ~= "none" then
- tracks[#tracks + 1] = {
- media_type = "audio",
- codec = map_codec_to_mpv(track.acodec) or
- ext_map[track.ext],
- }
- if is_default then
- has_requested_audio = true
- end
- end
- local url = edl_track or track.url
- local hdr = {"!new_stream", "!no_clip", "!no_chapters"}
- local skip = #tracks == 0
- local params = ""
- if use_all_formats then
- for _, sub in ipairs(tracks) do
- -- A single track that is either audio or video. Delay load it.
- local props = ""
- if sub.media_type == "video" then
- props = props .. ",w=" .. as_integer(track.width)
- .. ",h=" .. as_integer(track.height)
- .. ",fps=" .. as_integer(track.fps)
- elseif sub.media_type == "audio" then
- props = props .. ",samplerate=" .. as_integer(track.asr)
- end
- hdr[#hdr + 1] = "!delay_open,media_type=" .. sub.media_type ..
- ",codec=" .. (sub.codec or "null") .. props
- -- Add bitrate information etc. for better user selection.
- local byterate = 0
- local rates = {"tbr", "vbr", "abr"}
- if #tracks > 1 then
- rates = {({video = "vbr", audio = "abr"})[sub.media_type]}
- end
- if tbr_only then
- rates = {"tbr"}
- end
- for _, f in ipairs(rates) do
- local br = as_integer(track[f])
- if br > 0 then
- byterate = math.floor(br * 1000 / 8)
- break
- end
- end
- local title = track.format or track.format_note or ""
- if #tracks > 1 then
- if #title > 0 then
- title = title .. " "
- end
- title = title .. "muxed-" .. index
- end
- local flags = {}
- if is_default then
- flags[#flags + 1] = "default"
- end
- hdr[#hdr + 1] = "!track_meta,title=" ..
- edl_escape(title) .. ",byterate=" .. byterate ..
- iif(#flags > 0, ",flags=" .. table.concat(flags, "+"), "")
- end
- if duration > 0 then
- params = params .. ",length=" .. duration
- end
- end
- if not skip then
- hdr[#hdr + 1] = edl_escape(url) .. params
- streams[#streams + 1] = table.concat(hdr, ";")
- -- In case there is only 1 of these streams.
- -- Note: assumes it has no important EDL headers
- single_url = url
- end
- end
- -- Merge all tracks into a single virtual file, but avoid EDL if it's
- -- only a single track (i.e. redundant).
- if #streams == 1 and single_url then
- res.url = single_url
- elseif #streams > 0 then
- local tags = tags_to_edl(json)
- if tags then
- -- not a stream; just for the sake of concatenating the EDL string
- streams[#streams + 1] = tags
- end
- res.url = "edl://" .. table.concat(streams, ";")
- else
- return nil
- end
- if has_requested_audio ~= has_requested_video then
- local not_req_prop = has_requested_video and "aid" or "vid"
- if mp.get_property(not_req_prop) == "auto" then
- mp.set_property("file-local-options/" .. not_req_prop, "no")
- end
- end
- return res
- end
- local function add_single_video(json)
- local streamurl = ""
- local format_info = ""
- local max_bitrate = 0
- local requested_formats = json["requested_formats"] or json["requested_downloads"]
- local all_formats = json["formats"]
- local has_requested_formats = requested_formats and #requested_formats > 0
- local http_headers = has_requested_formats
- and requested_formats[1].http_headers
- or json.http_headers
- if o.use_manifests and valid_manifest(json) then
- -- prefer manifest_url if present
- format_info = "manifest"
- local mpd_url = requested_formats and
- requested_formats[1]["manifest_url"] or json["manifest_url"]
- if not mpd_url then
- msg.error("No manifest URL found in JSON data.")
- return
- elseif not url_is_safe(mpd_url) then
- return
- end
- streamurl = mpd_url
- if requested_formats then
- for _, track in pairs(requested_formats) do
- max_bitrate = (track.tbr and track.tbr > max_bitrate) and
- track.tbr or max_bitrate
- end
- elseif json.tbr then
- max_bitrate = json.tbr > max_bitrate and json.tbr or max_bitrate
- end
- end
- if streamurl == "" then
- -- possibly DASH/split tracks
- local res = nil
- -- Not having requested_formats usually hints to HLS master playlist
- -- usage, which we don't want to split off, at least not yet.
- if (all_formats and o.all_formats) and
- (has_requested_formats or o.force_all_formats)
- then
- format_info = "all_formats (separate)"
- res = formats_to_edl(json, all_formats, true)
- -- Note: since we don't delay-load muxed streams, use normal stream
- -- selection if we have to use muxed streams.
- if res and res.muxed_needed then
- res = nil
- end
- end
- if (not res) and has_requested_formats then
- format_info = "youtube-dl (separate)"
- res = formats_to_edl(json, requested_formats, false)
- end
- if res then
- streamurl = res.url
- end
- end
- if streamurl == "" and json.url then
- format_info = "youtube-dl (single)"
- local edl_track = nil
- edl_track = edl_track_joined(json.fragments, json.protocol,
- json.is_live, json.fragment_base_url)
- if not edl_track and not url_is_safe(json.url) then
- return
- end
- -- normal video or single track
- streamurl = edl_track or json.url
- end
- if streamurl == "" then
- msg.error("No URL found in JSON data.")
- return
- end
- set_http_headers(http_headers)
- msg.verbose("format selection: " .. format_info)
- msg.debug("streamurl: " .. streamurl)
- mp.set_property("stream-open-filename", streamurl:gsub("^data:", "data://", 1))
- if mp.get_property("force-media-title", "") == "" then
- mp.set_property("file-local-options/force-media-title", json.title)
- end
- -- set hls-bitrate for dash track selection
- if max_bitrate > 0 and
- not option_was_set("hls-bitrate") and
- not option_was_set_locally("hls-bitrate") then
- mp.set_property_native('file-local-options/hls-bitrate', max_bitrate*1000)
- end
- -- add subtitles
- if not (json.requested_subtitles == nil) then
- local subs = {}
- for lang, info in pairs(json.requested_subtitles) do
- subs[#subs + 1] = {lang = lang or "-", info = info}
- end
- table.sort(subs, function(a, b) return a.lang < b.lang end)
- for _, e in ipairs(subs) do
- local lang, sub_info = e.lang, e.info
- msg.verbose("adding subtitle ["..lang.."]")
- local sub = nil
- if not (sub_info.data == nil) then
- sub = "memory://"..sub_info.data
- elseif not (sub_info.url == nil) and
- url_is_safe(sub_info.url) then
- sub = sub_info.url
- end
- if not (sub == nil) then
- local edl = "edl://!no_clip;!delay_open,media_type=sub"
- local codec = map_codec_to_mpv(sub_info.ext)
- if codec then
- edl = edl .. ",codec=" .. codec
- end
- edl = edl .. ";" .. edl_escape(sub)
- local title = sub_info.name or sub_info.ext
- mp.commandv("sub-add", edl, "auto", title, lang)
- else
- msg.verbose("No subtitle data/url for ["..lang.."]")
- end
- end
- end
- -- add chapters
- if json.chapters then
- msg.debug("Adding pre-parsed chapters")
- for i = 1, #json.chapters do
- local chapter = json.chapters[i]
- local title = chapter.title or ""
- if title == "" then
- title = string.format('Chapter %02d', i)
- end
- table.insert(chapter_list, {time=chapter.start_time, title=title})
- end
- elseif not (json.description == nil) and not (json.duration == nil) then
- chapter_list = extract_chapters(json.description, json.duration)
- end
- -- set start time
- if (json.start_time or json.section_start) and
- not option_was_set("start") and
- not option_was_set_locally("start") then
- local start_time = json.start_time or json.section_start
- msg.debug("Setting start to: " .. start_time .. " secs")
- mp.set_property("file-local-options/start", start_time)
- end
- -- set end time
- if (json.end_time or json.section_end) and
- not option_was_set("end") and
- not option_was_set_locally("end") then
- local end_time = json.end_time or json.section_end
- msg.debug("Setting end to: " .. end_time .. " secs")
- mp.set_property("file-local-options/end", end_time)
- end
- -- set aspect ratio for anamorphic video
- if not (json.stretched_ratio == nil) and
- not option_was_set("video-aspect-override") then
- mp.set_property('file-local-options/video-aspect-override', json.stretched_ratio)
- end
- local stream_opts = mp.get_property_native("file-local-options/stream-lavf-o", {})
- -- for rtmp
- if (json.protocol == "rtmp") then
- stream_opts = append_libav_opt(stream_opts,
- "rtmp_tcurl", streamurl)
- stream_opts = append_libav_opt(stream_opts,
- "rtmp_pageurl", json.page_url)
- stream_opts = append_libav_opt(stream_opts,
- "rtmp_playpath", json.play_path)
- stream_opts = append_libav_opt(stream_opts,
- "rtmp_swfverify", json.player_url)
- stream_opts = append_libav_opt(stream_opts,
- "rtmp_swfurl", json.player_url)
- stream_opts = append_libav_opt(stream_opts,
- "rtmp_app", json.app)
- end
- if json.proxy and json.proxy ~= "" then
- stream_opts = append_libav_opt(stream_opts,
- "http_proxy", json.proxy)
- end
- mp.set_property_native("file-local-options/stream-lavf-o", stream_opts)
- end
- local function check_version(ytdl_path)
- local command = {
- name = "subprocess",
- capture_stdout = true,
- args = {ytdl_path, "--version"}
- }
- local version_string = mp.command_native(command).stdout
- local year, month, day = string.match(version_string, "(%d+).(%d+).(%d+)")
- -- sanity check
- if (tonumber(year) < 2000) or (tonumber(month) > 12) or
- (tonumber(day) > 31) then
- return
- end
- local version_ts = os.time{year=year, month=month, day=day}
- if (os.difftime(os.time(), version_ts) > 60*60*24*90) then
- msg.warn("It appears that your youtube-dl version is severely out of date.")
- end
- end
- function run_ytdl_hook(url)
- local start_time = os.clock()
- -- strip ytdl://
- if (url:find("ytdl://") == 1) then
- url = url:sub(8)
- end
- local format = mp.get_property("options/ytdl-format")
- local raw_options = mp.get_property_native("options/ytdl-raw-options")
- local allsubs = true
- local proxy = nil
- local use_playlist = false
- local command = {
- ytdl.path, "--no-warnings", "-J", "--flat-playlist",
- "--sub-format", "ass/srt/best"
- }
- -- Checks if video option is "no", change format accordingly,
- -- but only if user didn't explicitly set one
- if (mp.get_property("options/vid") == "no") and (#format == 0) then
- format = "bestaudio/best"
- msg.verbose("Video disabled. Only using audio")
- end
- if (format == "") then
- format = "bestvideo+bestaudio/best"
- end
- if format ~= "ytdl" then
- table.insert(command, "--format")
- table.insert(command, format)
- end
- for param, arg in pairs(raw_options) do
- table.insert(command, "--" .. param)
- if (arg ~= "") then
- table.insert(command, arg)
- end
- if (param == "sub-lang" or param == "sub-langs" or param == "srt-lang") and (arg ~= "") then
- allsubs = false
- elseif (param == "proxy") and (arg ~= "") then
- proxy = arg
- elseif (param == "yes-playlist") then
- use_playlist = true
- end
- end
- if (allsubs == true) then
- table.insert(command, "--all-subs")
- end
- if not use_playlist then
- table.insert(command, "--no-playlist")
- end
- table.insert(command, "--")
- table.insert(command, url)
- local result
- if ytdl.searched then
- result = exec(command)
- else
- local separator = platform_is_windows() and ";" or ":"
- if o.ytdl_path:match("[^" .. separator .. "]") then
- ytdl.paths_to_search = {}
- for path in o.ytdl_path:gmatch("[^" .. separator .. "]+") do
- table.insert(ytdl.paths_to_search, path)
- end
- end
- for _, path in pairs(ytdl.paths_to_search) do
- -- search for youtube-dl in mpv's config dir
- local exesuf = platform_is_windows() and ".exe" or ""
- local ytdl_cmd = mp.find_config_file(path .. exesuf)
- if ytdl_cmd then
- msg.verbose("Found youtube-dl at: " .. ytdl_cmd)
- ytdl.path = ytdl_cmd
- command[1] = ytdl.path
- result = exec(command)
- break
- else
- msg.verbose("No youtube-dl found with path " .. path .. exesuf .. " in config directories")
- command[1] = path
- result = exec(command)
- if result.error_string == "init" then
- msg.verbose("youtube-dl with path " .. path .. exesuf .. " not found in PATH or not enough permissions")
- else
- msg.verbose("Found youtube-dl with path " .. path .. exesuf .. " in PATH")
- ytdl.path = path
- break
- end
- end
- end
- ytdl.searched = true
- end
- if result.killed_by_us then
- return
- end
- local json = result.stdout
- local parse_err = nil
- if result.status ~= 0 or json == "" then
- json = nil
- elseif json then
- json, parse_err = utils.parse_json(json)
- end
- if (json == nil) then
- msg.verbose("status:", result.status)
- msg.verbose("reason:", result.error_string)
- msg.verbose("stdout:", result.stdout)
- msg.verbose("stderr:", result.stderr)
- -- trim our stderr to avoid spurious newlines
- ytdl_err = result.stderr:gsub("^%s*(.-)%s*$", "%1")
- msg.error(ytdl_err)
- local err = "youtube-dl failed: "
- if result.error_string and result.error_string == "init" then
- err = err .. "not found or not enough permissions"
- elseif parse_err then
- err = err .. "failed to parse JSON data: " .. parse_err
- else
- err = err .. "unexpected error occurred"
- end
- msg.error(err)
- if parse_err or string.find(ytdl_err, "yt%-dl%.org/bug") then
- check_version(ytdl.path)
- end
- return
- end
- msg.verbose("youtube-dl succeeded!")
- msg.debug('ytdl parsing took '..os.clock()-start_time..' seconds')
- json["proxy"] = json["proxy"] or proxy
- -- what did we get?
- if json["direct"] then
- -- direct URL, nothing to do
- msg.verbose("Got direct URL")
- return
- elseif (json["_type"] == "playlist")
- or (json["_type"] == "multi_video") then
- -- a playlist
- if (#json.entries == 0) then
- msg.warn("Got empty playlist, nothing to play.")
- return
- end
- local self_redirecting_url =
- json.entries[1]["_type"] ~= "url_transparent" and
- json.entries[1]["webpage_url"] and
- json.entries[1]["webpage_url"] == json["webpage_url"]
- -- some funky guessing to detect multi-arc videos
- if self_redirecting_url and #json.entries > 1
- and json.entries[1].protocol == "m3u8_native"
- and json.entries[1].url then
- msg.verbose("multi-arc video detected, building EDL")
- local playlist = edl_track_joined(json.entries)
- msg.debug("EDL: " .. playlist)
- if not playlist then
- return
- end
- -- can't change the http headers for each entry, so use the 1st
- set_http_headers(json.entries[1].http_headers)
- mp.set_property("stream-open-filename", playlist)
- if json.title and mp.get_property("force-media-title", "") == "" then
- mp.set_property("file-local-options/force-media-title",
- json.title)
- end
- -- there might not be subs for the first segment
- local entry_wsubs = nil
- for i, entry in pairs(json.entries) do
- if not (entry.requested_subtitles == nil) then
- entry_wsubs = i
- break
- end
- end
- if not (entry_wsubs == nil) and
- not (json.entries[entry_wsubs].duration == nil) then
- for j, req in pairs(json.entries[entry_wsubs].requested_subtitles) do
- local subfile = "edl://"
- for i, entry in pairs(json.entries) do
- if not (entry.requested_subtitles == nil) and
- not (entry.requested_subtitles[j] == nil) and
- url_is_safe(entry.requested_subtitles[j].url) then
- subfile = subfile..edl_escape(entry.requested_subtitles[j].url)
- else
- subfile = subfile..edl_escape("memory://WEBVTT")
- end
- subfile = subfile..",length="..entry.duration..";"
- end
- msg.debug(j.." sub EDL: "..subfile)
- mp.commandv("sub-add", subfile, "auto", req.ext, j)
- end
- end
- elseif self_redirecting_url and #json.entries == 1 then
- msg.verbose("Playlist with single entry detected.")
- add_single_video(json.entries[1])
- else
- local playlist_index = parse_yt_playlist(url, json)
- local playlist = {"#EXTM3U"}
- for i, entry in pairs(json.entries) do
- local site = entry.url
- local title = entry.title
- if not (title == nil) then
- title = string.gsub(title, '%s+', ' ')
- table.insert(playlist, "#EXTINF:0," .. title)
- end
- --[[ some extractors will still return the full info for
- all clips in the playlist and the URL will point
- directly to the file in that case, which we don't
- want so get the webpage URL instead, which is what
- we want, but only if we aren't going to trigger an
- infinite loop
- --]]
- if entry["webpage_url"] and not self_redirecting_url then
- site = entry["webpage_url"]
- end
- -- links without protocol as returned by --flat-playlist
- if not site:find("://") then
- -- youtube extractor provides only IDs,
- -- others come prefixed with the extractor name and ":"
- local prefix = site:find(":") and "ytdl://" or
- "https://youtu.be/"
- table.insert(playlist, prefix .. site)
- elseif url_is_safe(site) then
- table.insert(playlist, site)
- end
- end
- if use_playlist and
- not option_was_set("playlist-start") and playlist_index then
- mp.set_property_number("playlist-start", playlist_index)
- end
- mp.set_property("stream-open-filename", "memory://" .. table.concat(playlist, "\n"))
- end
- else -- probably a video
- add_single_video(json)
- end
- msg.debug('script running time: '..os.clock()-start_time..' seconds')
- end
- if (not o.try_ytdl_first) then
- mp.add_hook("on_load", 10, function ()
- msg.verbose('ytdl:// hook')
- local url = mp.get_property("stream-open-filename", "")
- if not (url:find("ytdl://") == 1) then
- msg.verbose('not a ytdl:// url')
- return
- end
- run_ytdl_hook(url)
- end)
- end
- mp.add_hook(o.try_ytdl_first and "on_load" or "on_load_fail", 10, function()
- msg.verbose('full hook')
- local url = mp.get_property("stream-open-filename", "")
- if not (url:find("ytdl://") == 1) and
- not ((url:find("https?://") == 1) and not is_blacklisted(url)) then
- return
- end
- run_ytdl_hook(url)
- end)
- mp.add_hook("on_preloaded", 10, function ()
- if next(chapter_list) ~= nil then
- msg.verbose("Setting chapters")
- mp.set_property_native("chapter-list", chapter_list)
- chapter_list = {}
- end
- end)
|