---------------------- -- #example ytdl_preload.conf -- # make sure lines do not have trailing whitespace -- # ytdl_opt has no sanity check and should be formatted exactly how it would appear in yt-dlp CLI, they are split into a key/value pair on whitespace -- # at least on Windows, do not escape '\' in temp, just us a single one for each divider -- #temp=R:\ytdltest -- #ytdl_opt1=-r 50k -- #ytdl_opt2=-N 5 -- #ytdl_opt#=etc ---------------------- local nextIndex local caught = true -- local pop = false local ytdl = "yt-dlp" local utils = require("mp.utils") local options = require("mp.options") local cache = os.getenv("XDG_CACHE_HOME") local opts = { temp = cache .. "/mpv", ytdl_opt1 = "", ytdl_opt2 = "", ytdl_opt3 = "", ytdl_opt4 = "", ytdl_opt5 = "", ytdl_opt6 = "", ytdl_opt7 = "", ytdl_opt8 = "", ytdl_opt9 = "", } options.read_options(opts, "ytdl_preload") local additionalOpts = {} for k, v in pairs(opts) do if k:find("ytdl_opt%d") and v ~= "" then additionalOpts[k] = v -- print("entry") -- print(k .. v) end end local cachePath = opts.temp local chapter_list = {} local json = "" local filesToDelete = {} local function exists(file) local ok, err, code = os.rename(file, file) if not ok then if code == 13 then -- Permission denied, but it exists return true end end return ok, err end --from ytdl_hook 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 chapters() if json.chapters then 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 end --end ytdl_hook local title = "" local fVideo = "" local fAudio = "" local function load_files(dtitle, destination, audio, wait) if wait then if exists(destination .. ".mka") then print("---wait success: found mka---") audio = "audio-file=" .. destination .. ".mka," else print("---could not find mka after wait, audio may be missing---") end end -- if audio ~= "" then -- table.insert(filesToDelete, destination .. ".mka") -- end -- table.insert(filesToDelete, destination .. ".mkv") dtitle = dtitle:gsub("-" .. ("[%w_-]"):rep(11) .. "$", "") dtitle = dtitle:gsub("^" .. ("%d"):rep(10) .. "%-", "") mp.commandv( "loadfile", destination .. ".mkv", "append", audio .. 'force-media-title="' .. dtitle .. '",demuxer-max-back-bytes=1MiB,demuxer-max-bytes=3MiB,ytdl=no' ) --,sub-file="..destination..".en.vtt") --in case they are not set up to autoload mp.commandv("playlist_move", mp.get_property("playlist-count") - 1, nextIndex) mp.commandv("playlist_remove", nextIndex + 1) caught = true title = "" -- pop = true end local listenID = "" local function listener(event) if not caught and event.prefix == mp.get_script_name() and string.find(event.text, listenID) then local destination = string.match(event.text, "%[download%] Destination: (.+).mkv") or string.match(event.text, "%[download%] (.+).mkv has already been downloaded") -- if destination then print("---"..cachePath) end; if destination and string.find(destination, string.gsub(cachePath, "~/", "")) then -- print(listenID) mp.unregister_event(listener) _, title = utils.split_path(destination) local audio = "" if fAudio == "" then load_files(title, destination, audio, false) else if exists(destination .. ".mka") then audio = "audio-file=" .. destination .. ".mka," load_files(title, destination, audio, false) else print("---expected mka but could not find it, waiting for 2 seconds---") mp.add_timeout(2, function() load_files(title, destination, audio, true) end) end end end end end --from ytdl_hook mp.add_hook("on_preloaded", 10, function() if string.find(mp.get_property("path"), cachePath) then chapters() if next(chapter_list) ~= nil then mp.set_property_native("chapter-list", chapter_list) chapter_list = {} json = "" end end end) --end ytdl_hook function dump(o) if type(o) == "table" then local s = "{ " for k, v in pairs(o) do if type(k) ~= "number" then k = '"' .. k .. '"' end s = s .. "[" .. k .. "] = " .. dump(v) .. "," end return s .. "} " else return tostring(o) end end local function addOPTS(old) for k, v in pairs(additionalOpts) do -- print(k) if string.find(v, "%s") then for l, w in string.gmatch(v, "([-%w]+) (.+)") do table.insert(old, l) table.insert(old, w) end else table.insert(old, v) end end -- print(dump(old)) return old end local AudioDownloadHandle = {} local VideoDownloadHandle = {} local JsonDownloadHandle = {} local function download_files(id, success, result, error) if result.killed_by_us then return end local jfile = cachePath .. "/" .. id .. ".json" local jfileIO = io.open(jfile, "w") jfileIO:write(result.stdout) jfileIO:close() json = utils.parse_json(result.stdout) -- print(dump(json)) if json.requested_downloads[1].requested_formats ~= nil then local args = { ytdl, "--no-continue", "-q", "-f", fAudio, "--restrict-filenames", "--no-playlist", "--no-part", "-o", cachePath .. "/" .. id .. "-%(title)s-%(id)s.mka", "--load-info-json", jfile, } args = addOPTS(args) AudioDownloadHandle = mp.command_native_async({ name = "subprocess", args = args, playback_only = false, }, function() end) else fAudio = "" fVideo = fVideo:gsub("bestvideo", "best") fVideo = fVideo:gsub("bv", "best") end local args = { ytdl, "--no-continue", "-f", fVideo .. "/best", "--restrict-filenames", "--no-playlist", "--no-part", "-o", cachePath .. "/" .. id .. "-%(title)s-%(id)s.mkv", "--load-info-json", jfile, } args = addOPTS(args) VideoDownloadHandle = mp.command_native_async({ name = "subprocess", args = args, playback_only = false, }, function() end) end local function DL() local index = tonumber(mp.get_property("playlist-pos")) if mp.get_property("playlist/" .. index .. "/filename"):find("/videos$") and mp.get_property("playlist/" .. index + 1 .. "/filename"):find("/shorts$") then return end if tonumber(mp.get_property("playlist-pos-1")) > 0 and mp.get_property("playlist-pos-1") ~= mp.get_property("playlist-count") then nextIndex = index + 1 local nextFile = mp.get_property("playlist/" .. nextIndex .. "/filename") if nextFile and caught and nextFile:find("://", 0, false) then caught = false mp.enable_messages("info") mp.register_event("log-message", listener) local ytFormat = mp.get_property("ytdl-format") fVideo = string.match(ytFormat, "(.+)%+.+//?") or "bestvideo" fAudio = string.match(ytFormat, ".+%+(.+)//?") or "bestaudio" -- print("start"..nextFile) listenID = tostring(os.time()) local args = { ytdl, "--dump-single-json", "--no-simulate", "--skip-download", "--restrict-filenames", "--no-playlist", "--sub-lang", "en", "--write-sub", "--no-part", "-o", cachePath .. "/" .. listenID .. "-%(title)s-%(id)s.%(ext)s", nextFile, } args = addOPTS(args) -- print(dump(args)) table.insert(filesToDelete, listenID) JsonDownloadHandle = mp.command_native_async({ name = "subprocess", args = args, capture_stdout = true, capture_stderr = true, playback_only = false, }, function(...) download_files(listenID, ...) end) end end end local function clearCache() -- print(pop) --if pop == true then mp.abort_async_command(AudioDownloadHandle) mp.abort_async_command(VideoDownloadHandle) mp.abort_async_command(JsonDownloadHandle) -- for k, v in pairs(filesToDelete) do -- print("remove: " .. v) -- os.remove(v) -- end local ftd = io.open(cachePath .. "/temp.files", "a") for k, v in pairs(filesToDelete) do ftd:write(v .. "\n") if package.config:sub(1, 1) ~= "/" then os.execute('del /Q /F "' .. cachePath .. "\\" .. v .. '*"') else os.execute("rm -f " .. cachePath .. "/" .. v .. "*") end end ftd:close() print("clear") mp.command("quit") --end end mp.add_hook("on_unload", 50, function() -- mp.abort_async_command(AudioDownloadHandle) -- mp.abort_async_command(VideoDownloadHandle) mp.abort_async_command(JsonDownloadHandle) mp.unregister_event(listener) caught = true listenID = "resetYtdlPreloadListener" -- print(listenID) end) local skipInitial mp.observe_property("playlist-count", "number", function() if skipInitial then DL() else skipInitial = true end end) --from ytdl_hook local platform_is_windows = (package.config:sub(1, 1) == "\\") local o = { exclude = "", try_ytdl_first = false, use_manifests = false, all_formats = false, force_all_formats = true, ytdl_path = "", } local paths_to_search = { "yt-dlp", "yt-dlp_x86", "youtube-dl" } --local options = require 'mp.options' options.read_options(o, "ytdl_hook") local separator = platform_is_windows and ";" or ":" if o.ytdl_path:match("[^" .. separator .. "]") then paths_to_search = {} for path in o.ytdl_path:gmatch("[^" .. separator .. "]+") do table.insert(paths_to_search, path) end end local function exec(args) local ret = mp.command_native({ name = "subprocess", args = args, capture_stdout = true, capture_stderr = true, }) return ret.status, ret.stdout, ret, ret.killed_by_us end local msg = require("mp.msg") local command = {} for _, path in pairs(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 = ytdl_cmd break else msg.verbose("No youtube-dl found with path " .. path .. exesuf .. " in config directories") --search in PATH command[1] = path es, json, result, aborted = 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 break end end end --end ytdl_hook mp.register_event("start-file", DL) mp.register_event("shutdown", clearCache) local ftd = io.open(cachePath .. "/temp.files", "r") while ftd ~= nil do local line = ftd:read() if line == nil or line == "" then ftd:close() io.open(cachePath .. "/temp.files", "w"):close() break end -- print("DEL::"..line) if package.config:sub(1, 1) ~= "/" then os.execute('del /Q /F "' .. cachePath .. "\\" .. line .. '*" >nul 2>nul') else os.execute("rm -f " .. cachePath .. "/" .. line .. "* &> /dev/null") end end