diff options
Diffstat (limited to 'mac/.config/mpv/scripts/ytdl-preload.lua')
| -rw-r--r-- | mac/.config/mpv/scripts/ytdl-preload.lua | 433 |
1 files changed, 433 insertions, 0 deletions
diff --git a/mac/.config/mpv/scripts/ytdl-preload.lua b/mac/.config/mpv/scripts/ytdl-preload.lua new file mode 100644 index 0000000..835b25c --- /dev/null +++ b/mac/.config/mpv/scripts/ytdl-preload.lua @@ -0,0 +1,433 @@ +---------------------- +-- #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 |
