diff options
-rw-r--r-- | README.md | 2 | ||||
-rw-r--r-- | include/Config.hpp | 2 | ||||
-rw-r--r-- | include/Downloader.hpp | 3 | ||||
-rw-r--r-- | include/QuickMedia.hpp | 2 | ||||
-rwxr-xr-x | install.sh | 1 | ||||
-rw-r--r-- | mpv/scripts/ytdl_hook.lua | 1057 | ||||
-rw-r--r-- | plugins/Youtube.hpp | 2 | ||||
-rw-r--r-- | src/Downloader.cpp | 54 | ||||
-rw-r--r-- | src/QuickMedia.cpp | 124 | ||||
-rw-r--r-- | src/VideoPlayer.cpp | 13 | ||||
-rw-r--r-- | src/plugins/MediaGeneric.cpp | 2 | ||||
-rw-r--r-- | src/plugins/Youtube.cpp | 38 |
12 files changed, 1158 insertions, 142 deletions
@@ -36,7 +36,7 @@ Installing `lld` (the LLVM linker) can improve compile times. `noto-fonts` (when `use_system_fonts` config is not set to `true`) ### Optional `noto-fonts-cjk` needs to be installed to view chinese, japanese and korean characters (when `use_system_fonts` config is not set to `true`).\ -`youtube-dl` needs to be installed to play/download xxx videos.\ +`youtube-dl/yt-dlp` needs to be installed to play/download xxx videos.\ `notify-send` (which is part of `libnotify`) needs to be installed to show notifications (on Linux and other systems that uses d-bus notification system).\ [automedia](https://git.dec05eba.com/AutoMedia/) needs to be installed when tracking anime/manga with `Ctrl + T`.\ `waifu2x-ncnn-vulkan` needs to be installed when using the `--upscale-images` or `--upscale-images-always` option.\ diff --git a/include/Config.hpp b/include/Config.hpp index 3a172d2..e0756cf 100644 --- a/include/Config.hpp +++ b/include/Config.hpp @@ -45,7 +45,7 @@ namespace QuickMedia { }; struct YoutubeConfig { - bool load_progress = true; + bool load_progress = false; }; struct MatrixConfig { diff --git a/include/Downloader.hpp b/include/Downloader.hpp index cb70f2e..1f926c3 100644 --- a/include/Downloader.hpp +++ b/include/Downloader.hpp @@ -55,7 +55,7 @@ namespace QuickMedia { class YoutubeDlDownloader : public Downloader { public: - YoutubeDlDownloader(const std::string &url, const std::string &output_filepath, bool no_video); + YoutubeDlDownloader(const char *yt_dl_name, const std::string &url, const std::string &output_filepath, bool no_video); bool start() override; bool stop(bool download_completed) override; DownloadUpdateStatus update() override; @@ -72,6 +72,7 @@ namespace QuickMedia { std::string download_speed_text; bool no_video; bool finished = false; + const char *yt_dl_name; }; struct MediaMetadata { diff --git a/include/QuickMedia.hpp b/include/QuickMedia.hpp index 4edea6e..0f8837d 100644 --- a/include/QuickMedia.hpp +++ b/include/QuickMedia.hpp @@ -119,6 +119,7 @@ namespace QuickMedia { void set_clipboard(const std::string &str); private: void init(mgl::WindowHandle parent_window, std::string &program_path, bool no_dialog); + void check_youtube_dl_installed(const std::string &plugin_name); void load_plugin_by_name(std::vector<Tab> &tabs, int &start_tab_index, FileManagerMimeType fm_mime_type, FileSelectionHandler file_selection_handler, std::string instance); void common_event_handler(mgl::Event &event); void handle_x11_events(); @@ -240,5 +241,6 @@ namespace QuickMedia { bool use_youtube_dl = false; int video_max_height = 0; std::mutex login_inputs_mutex; + const char *yt_dl_name = nullptr; }; } @@ -23,6 +23,7 @@ install -Dm644 README.md "/usr/share/quickmedia/README.md" install -Dm644 mpv/fonts/Material-Design-Iconic-Font.ttf "/usr/share/quickmedia/mpv/fonts/Material-Design-Iconic-Font.ttf" install -Dm644 mpv/scripts/mordenx.lua "/usr/share/quickmedia/mpv/scripts/mordenx.lua" +install -Dm644 mpv/scripts/ytdl_hook.lua "/usr/share/quickmedia/mpv/scripts/ytdl_hook.lua" install -Dm644 mpv/input.conf "/usr/share/quickmedia/mpv/input.conf" install -Dm644 mpv/mpv.conf "/usr/share/quickmedia/mpv/mpv.conf" diff --git a/mpv/scripts/ytdl_hook.lua b/mpv/scripts/ytdl_hook.lua new file mode 100644 index 0000000..0d31bb1 --- /dev/null +++ b/mpv/scripts/ytdl_hook.lua @@ -0,0 +1,1057 @@ +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)
\ No newline at end of file diff --git a/plugins/Youtube.hpp b/plugins/Youtube.hpp index 1088df9..3c6732c 100644 --- a/plugins/Youtube.hpp +++ b/plugins/Youtube.hpp @@ -163,8 +163,8 @@ namespace QuickMedia { int get_related_pages_first_tab() override { return 1; } void set_url(std::string new_url) override; std::string get_url_timestamp() override; + std::string get_download_url(int max_height) override; std::string get_video_url(int max_height, bool &has_embedded_audio, std::string &ext) override; - std::string get_audio_url(std::string &ext) override; PluginResult load(const SubmitArgs &args, VideoInfo &video_info, std::string &err_str) override; void mark_watched() override; void get_subtitles(SubtitleData &subtitle_data) override; diff --git a/src/Downloader.cpp b/src/Downloader.cpp index 57a0349..75cef1c 100644 --- a/src/Downloader.cpp +++ b/src/Downloader.cpp @@ -2,6 +2,7 @@ #include "../include/Storage.hpp" #include "../include/NetUtils.hpp" #include "../include/Notification.hpp" +#include "../include/StringUtils.hpp" #include <unistd.h> #include <signal.h> #include <sys/wait.h> @@ -147,7 +148,44 @@ namespace QuickMedia { return download_speed_text; } - YoutubeDlDownloader::YoutubeDlDownloader(const std::string &url, const std::string &output_filepath, bool no_video) : Downloader(url, output_filepath), no_video(no_video) { + static bool parse_ytdl_download_line(const char *line, size_t size, std::string &progress, std::string &content_size, std::string &download_speed) { + progress.clear(); + content_size.clear(); + download_speed.clear(); + + const std::string_view line_v(line, size); + size_t index = line_v.find("[download]", 0); + if(index == std::string::npos) + return false; + + index += 10; + size_t end_index = line_v.find(" of ~ ", index); + if(end_index == std::string::npos) + return false; + + progress = line_v.substr(index, end_index - index); + index = end_index + 6; + + end_index = line_v.find(" at", index); + if(end_index == std::string::npos) + return false; + + content_size = line_v.substr(index, end_index - index); + index = end_index + 3; + + end_index = line_v.find(" ETA ", index); + if(end_index == std::string::npos) + return false; + + download_speed = line_v.substr(index, end_index - index); + + progress = strip(progress); + content_size = strip(content_size); + download_speed = strip(download_speed); + return true; + } + + YoutubeDlDownloader::YoutubeDlDownloader(const char *yt_dl_name, const std::string &url, const std::string &output_filepath, bool no_video) : Downloader(url, output_filepath), no_video(no_video), yt_dl_name(yt_dl_name) { // youtube-dl requires a file extension for the file if(this->output_filepath.find('.') == std::string::npos) this->output_filepath += ".mkv"; @@ -161,7 +199,7 @@ namespace QuickMedia { bool YoutubeDlDownloader::start() { remove(output_filepath.c_str()); - std::vector<const char*> args = { "youtube-dl", "--no-warnings", "--no-continue", "--output", output_filepath.c_str(), "--newline" }; + std::vector<const char*> args = { yt_dl_name, "--no-warnings", "--no-continue", "--output", output_filepath.c_str(), "--newline" }; if(no_video) { args.push_back("-f"); args.push_back("bestaudio/best"); @@ -185,9 +223,9 @@ namespace QuickMedia { // TODO: Remove this async task and make the fd non blocking instead youtube_dl_output_reader = AsyncTask<bool>([this]{ char line[128]; - char progress_c[11]; - char content_size_c[21]; - char download_speed_c[21]; + std::string progress_c; + std::string content_size_c; + std::string download_speed_c; while(true) { if(fgets(line, sizeof(line), read_program_file)) { @@ -197,10 +235,10 @@ namespace QuickMedia { --len; } - if(sscanf(line, "[download] %10s of %20s at %20s", progress_c, content_size_c, download_speed_c) == 3) { + if(parse_ytdl_download_line(line, len, progress_c, content_size_c, download_speed_c)) { std::lock_guard<std::mutex> lock(progress_update_mutex); - if(strcmp(progress_c, "Unknown") != 0 && strcmp(content_size_c, "Unknown") != 0) { + if(strcmp(progress_c.c_str(), "Unknown") != 0 && strcmp(content_size_c.c_str(), "Unknown") != 0) { std::string progress_str = progress_c; progress_text = progress_str + " of " + content_size_c; if(progress_str.back() == '%') { @@ -212,7 +250,7 @@ namespace QuickMedia { } } - if(strcmp(download_speed_c, "Unknown") == 0) + if(strcmp(download_speed_c.c_str(), "Unknown") == 0) download_speed_text = "Unknown/s"; else download_speed_text = download_speed_c; diff --git a/src/QuickMedia.cpp b/src/QuickMedia.cpp index f91ef7e..b1bbcde 100644 --- a/src/QuickMedia.cpp +++ b/src/QuickMedia.cpp @@ -1080,10 +1080,17 @@ namespace QuickMedia { .related_media_thumbnail_handler({{"//div[data-role='video-relations']//img", "src", "xhcdn"}}); } - static void check_youtube_dl_installed(const std::string &plugin_name) { - if(!is_program_executable_by_name("youtube-dl")) { - show_notification("QuickMedia", "youtube-dl needs to be installed to play " + plugin_name + " videos", Urgency::CRITICAL); - abort(); + void Program::check_youtube_dl_installed(const std::string &plugin_name) { + if(yt_dl_name) + return; + + if(is_program_executable_by_name("yt-dlp")) { + yt_dl_name = "yt-dlp"; + } else if(is_program_executable_by_name("youtube-dl")) { + yt_dl_name = "youtube-dl"; + } else { + show_notification("QuickMedia", "yt-dlp or youtube-dl needs to be installed to play " + plugin_name + " videos", Urgency::CRITICAL); + exit(10); } } @@ -1322,6 +1329,8 @@ namespace QuickMedia { pipe_body->set_items(std::move(body_items)); tabs.push_back(Tab{std::move(pipe_body), std::make_unique<PipePage>(this), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); } else if(strcmp(plugin_name, "youtube") == 0) { + check_youtube_dl_installed(plugin_name); + use_youtube_dl = true; if(launch_url_type == LaunchUrlType::YOUTUBE_CHANNEL) { YoutubeChannelPage::create_each_type(this, std::move(launch_url), "", "Channel", tabs); } else if(launch_url_type == LaunchUrlType::YOUTUBE_VIDEO) { @@ -3031,6 +3040,8 @@ namespace QuickMedia { return url.find("pornhub.com") != std::string::npos || url.find("xhamster.com") != std::string::npos || url.find("spankbang.com") != std::string::npos + || url.find("youtube.com") != std::string::npos + || url.find("youtu.be") != std::string::npos // TODO: Remove when youtube-dl is no longer required to download soundcloud music || is_soundcloud(url); } @@ -3215,9 +3226,11 @@ namespace QuickMedia { mgl::Clock update_window_focus_time; // HACK! std::string youtube_video_id_dummy; - const bool is_youtube = youtube_url_extract_id(video_page->get_url(), youtube_video_id_dummy); + //const bool is_youtube = youtube_url_extract_id(video_page->get_url(), youtube_video_id_dummy); const bool is_matrix = strcmp(plugin_name, "matrix") == 0; const bool is_youtube_plugin = strcmp(plugin_name, "youtube") == 0; + const bool is_youtube = false; + const bool is_youtube_rel = youtube_url_extract_id(video_page->get_url(), youtube_video_id_dummy); bool added_recommendations = false; mgl::Clock time_watched_timer; @@ -3739,7 +3752,7 @@ namespace QuickMedia { current_page = previous_page; go_to_previous_page = true; break; - } else if(update_err == VideoPlayer::Error::EXITED && video_player->exit_status == 0 && (!is_matrix || is_youtube)) { + } else if(update_err == VideoPlayer::Error::EXITED && video_player->exit_status == 0 && (!is_matrix || is_youtube_rel)) { std::string new_video_url; if(!video_page->should_autoplay()) { @@ -7960,7 +7973,8 @@ namespace QuickMedia { const bool download_use_youtube_dl = url_should_download_with_youtube_dl(url); std::string filename; std::string video_id; - const bool url_is_youtube = youtube_url_extract_id(url, video_id); + //const bool url_is_youtube = youtube_url_extract_id(url, video_id); + const bool url_is_youtube = false; std::unique_ptr<YoutubeVideoPage> youtube_video_page; std::string video_url; @@ -7970,14 +7984,18 @@ namespace QuickMedia { TaskResult task_result = TaskResult::TRUE; if(download_use_youtube_dl) { - if(!is_program_executable_by_name("youtube-dl")) { - show_notification("QuickMedia", "youtube-dl needs to be installed to download the video/music", Urgency::CRITICAL); - abort(); + if(is_program_executable_by_name("yt-dlp")) { + yt_dl_name = "yt-dlp"; + } else if(is_program_executable_by_name("youtube-dl")) { + yt_dl_name = "youtube-dl"; + } else { + show_notification("QuickMedia", "yt-dlp or youtube-dl needs to be installed to download the video/music", Urgency::CRITICAL); + exit(10); } task_result = run_task_with_loading_screen([this, url, &filename]{ std::string json_str; - std::vector<const char*> args = { "youtube-dl", "--skip-download", "--print-json", "--no-warnings" }; + std::vector<const char*> args = { yt_dl_name, "--skip-download", "--print-json", "--no-warnings" }; if(no_video) { args.push_back("-f"); args.push_back("bestaudio/best"); @@ -8012,78 +8030,6 @@ namespace QuickMedia { return !filename.empty(); }); - } else if(url_is_youtube) { - youtube_video_page = std::make_unique<YoutubeVideoPage>(this, url); - bool cancelled = false; - bool load_successful = false; - const int video_max_height = video_get_max_height(); - - std::string err_str; - for(int i = 0; i < 3; ++i) { - task_result = run_task_with_loading_screen([&]{ - VideoInfo video_info; - SubmitArgs submit_args; - if(youtube_video_page->load(submit_args, video_info, err_str) != PluginResult::OK) - return false; - - filename = video_info.title; - std::string ext; - bool has_embedded_audio = true; - video_url = no_video ? "" : youtube_video_page->get_video_url(video_max_height, has_embedded_audio, ext); - audio_url.clear(); - - if(!has_embedded_audio || no_video) - audio_url = youtube_video_page->get_audio_url(ext); - - if(video_url.empty() && audio_url.empty()) - return false; - - if(!youtube_url_is_live_stream(video_url) && !youtube_url_is_live_stream(audio_url)) { - video_content_length = 0; - audio_content_length = 0; - std::string new_video_url = video_url; - std::string new_audio_url = audio_url; - auto current_thread_id = std::this_thread::get_id(); - if(!youtube_custom_redirect(new_video_url, new_audio_url, video_content_length, audio_content_length, [current_thread_id]{ return !program_is_dead_in_thread(current_thread_id); })) { - if(program_is_dead_in_current_thread()) - cancelled = true; - return false; - } - - video_url = std::move(new_video_url); - audio_url = std::move(new_audio_url); - } - - if(!video_url.empty() && !audio_url.empty()) - filename += ".mkv"; - else - filename += ext; - - return true; - }); - - if(task_result == TaskResult::CANCEL || cancelled) { - exit_code = 1; - return; - } else if(task_result == TaskResult::FALSE) { - continue; - } - - load_successful = true; - break; - } - - if(!load_successful) { - show_notification("QuickMedia", "Download failed" + (err_str.empty() ? "" : ", error: " + err_str), Urgency::CRITICAL); - exit_code = 1; - return; - } - - if(youtube_url_is_live_stream(video_url) || youtube_url_is_live_stream(audio_url)) { - show_notification("QuickMedia", "Downloading youtube live streams is currently not supported", Urgency::CRITICAL); - exit_code = 1; - return; - } } else { if(download_filename.empty()) { task_result = run_task_with_loading_screen([url, &filename]{ @@ -8173,17 +8119,7 @@ namespace QuickMedia { std::unique_ptr<Downloader> downloader; if(download_use_youtube_dl) { - downloader = std::make_unique<YoutubeDlDownloader>(url, output_filepath, no_video); - } else if(url_is_youtube) { - MediaMetadata video_metadata; - video_metadata.url = std::move(video_url); - video_metadata.content_length = video_content_length; - - MediaMetadata audio_metadata; - audio_metadata.url = std::move(audio_url); - audio_metadata.content_length = audio_content_length; - - downloader = std::make_unique<YoutubeDownloader>(video_metadata, audio_metadata, output_filepath); + downloader = std::make_unique<YoutubeDlDownloader>(yt_dl_name, url, output_filepath, no_video); } else { downloader = std::make_unique<CurlDownloader>(url, output_filepath); } diff --git a/src/VideoPlayer.cpp b/src/VideoPlayer.cpp index 8614e94..32eda39 100644 --- a/src/VideoPlayer.cpp +++ b/src/VideoPlayer.cpp @@ -169,8 +169,9 @@ namespace QuickMedia { if(get_file_type(video_player_filepath.c_str()) != FileType::REGULAR) video_player_filepath = "/usr/bin/quickmedia-video-player"; - std::string config_dir = "--config-dir=" + startup_args.resource_root + "mpv"; - std::string input_conf_file = "--input-conf=" + startup_args.resource_root + "mpv/input.conf"; + const std::string config_dir = "--config-dir=" + startup_args.resource_root + "mpv"; + const std::string input_conf_file = "--input-conf=" + startup_args.resource_root + "mpv/input.conf"; + const std::string ytdl_hook_file = "--scripts=" + startup_args.resource_root + "mpv/scripts/ytdl_hook.lua"; std::vector<const char*> args; // TODO: Resume playback if the last video played matches the first video played next time QuickMedia is launched @@ -228,10 +229,7 @@ namespace QuickMedia { else ytdl_format = "--ytdl-format=bestvideo[height<=?" + std::to_string(startup_args.monitor_height) + "]+bestaudio/best"; - if(!startup_args.use_youtube_dl) - args.push_back("--ytdl=no"); - else - args.push_back(ytdl_format.c_str()); + args.push_back("--ytdl=no"); // TODO: Properly escape referer quotes std::string referer_arg = "--http-header-fields=Referer: " + startup_args.referer; @@ -248,6 +246,9 @@ namespace QuickMedia { args.push_back("--load-scripts=yes"); args.push_back("--osc=yes"); args.push_back(input_conf_file.c_str()); + + if(startup_args.use_youtube_dl) + args.push_back(ytdl_hook_file.c_str()); } else { args.insert(args.end(), { config_dir.c_str(), diff --git a/src/plugins/MediaGeneric.cpp b/src/plugins/MediaGeneric.cpp index cc869ef..b2d2538 100644 --- a/src/plugins/MediaGeneric.cpp +++ b/src/plugins/MediaGeneric.cpp @@ -220,7 +220,7 @@ namespace QuickMedia { // TODO: Use max_height, if possible (void)max_height; has_embedded_audio = true; - ext = "m3u8"; + ext = ".m3u8"; return video_url; } diff --git a/src/plugins/Youtube.cpp b/src/plugins/Youtube.cpp index 9b64054..33e4cea 100644 --- a/src/plugins/Youtube.cpp +++ b/src/plugins/Youtube.cpp @@ -1568,18 +1568,6 @@ namespace QuickMedia { return fetch_comments(this, video_url, continuation_token, result_items); } - static const char* youtube_channel_page_type_to_api_params(YoutubeChannelPage::Type type) { - switch(type) { - case YoutubeChannelPage::Type::VIDEOS: - return "EgZ2aWRlb3PyBgQKAjoA"; - case YoutubeChannelPage::Type::SHORTS: - return "EgZzaG9ydHPyBgUKA5oBAA%3D%3D"; - case YoutubeChannelPage::Type::LIVE: - return "EgdzdHJlYW1z8gYECgJ6AA%3D%3D"; - } - return ""; - } - static const char* youtube_channel_page_type_get_endpoint(YoutubeChannelPage::Type type) { switch(type) { case YoutubeChannelPage::Type::VIDEOS: @@ -2359,7 +2347,15 @@ namespace QuickMedia { return nullptr; } + std::string YoutubeVideoPage::get_download_url(int max_height) { + return url; + } + std::string YoutubeVideoPage::get_video_url(int max_height, bool &has_embedded_audio, std::string &ext) { + has_embedded_audio = true; + ext = ".mp4"; + return url; +#if 0 if(!livestream_url.empty()) { has_embedded_audio = true; return livestream_url; @@ -2392,23 +2388,7 @@ namespace QuickMedia { ext = ".webm"; return chosen_video_format->base.url; - } - - std::string YoutubeVideoPage::get_audio_url(std::string &ext) { - if(audio_formats.empty()) - return ""; - - const YoutubeAudioFormat *chosen_audio_format = &audio_formats.front(); - fprintf(stderr, "Choosing youtube audio format: bitrate: %d, mime type: %s\n", chosen_audio_format->base.bitrate, chosen_audio_format->base.mime_type.c_str()); - - if(chosen_audio_format->base.mime_type.find("mp4") != std::string::npos) - ext = ".m4a"; - else if(chosen_audio_format->base.mime_type.find("webm") != std::string::npos) - ext = ".opus"; // TODO: Detect if vorbis (.ogg) or opus (.opus) - else if(chosen_audio_format->base.mime_type.find("opus") != std::string::npos) - ext = ".opus"; - - return chosen_audio_format->base.url; +#endif } // Returns -1 if timestamp is in an invalid format |