aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md2
-rw-r--r--include/Config.hpp2
-rw-r--r--include/Downloader.hpp3
-rw-r--r--include/QuickMedia.hpp2
-rwxr-xr-xinstall.sh1
-rw-r--r--mpv/scripts/ytdl_hook.lua1057
-rw-r--r--plugins/Youtube.hpp2
-rw-r--r--src/Downloader.cpp54
-rw-r--r--src/QuickMedia.cpp124
-rw-r--r--src/VideoPlayer.cpp13
-rw-r--r--src/plugins/MediaGeneric.cpp2
-rw-r--r--src/plugins/Youtube.cpp38
12 files changed, 1158 insertions, 142 deletions
diff --git a/README.md b/README.md
index 5a54af4..29bdaa9 100644
--- a/README.md
+++ b/README.md
@@ -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;
};
}
diff --git a/install.sh b/install.sh
index a305ace..64ee28f 100755
--- a/install.sh
+++ b/install.sh
@@ -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