-
-
Save bitingsock/bb4964a3ecd7afd21b0ce39d54f67d56 to your computer and use it in GitHub Desktop.
experimental version
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
---------------------- | |
-- #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 opts = { | |
temp = "R:\\ytdl", | |
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) | |
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 | |
@Zylsjsp On windows (at least), the abort signal sent to yt-dlp was not robust enough to allow the script to delete the file that was still locked for download before mpv exits. I have added a list of files that should be deleted on next runtime.
Tks for notice, but the script failed with a nil value of local ftd
.
logs as follows:
[ytdl_preload]
[ytdl_preload] stack traceback:
[ytdl_preload] [C]: at 0x563492990780
[ytdl_preload] [C]: at 0x563492990f30
[ytdl_preload] Lua error: /home/zentino/.config/mpv/scripts/ytdl-preload.lua:376: attempt to index local 'ftd' (a nil value)
Sending hook command failed. Removing hook.
my temp dir is /home/zentino/tmp/ytdl
oops sorry, try now
Works perfectly on my Arch🥳🥳
why temp=$home/tmp/ytdl
temp=~/tmp/ytdl
or temp=~~/ytdl
not working?
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
ya I used the "force" flag for the delete now. what that means in each OS and while the file is locked by yt-dlp, I don't fully know. Only other way around it is to either halt mpv's shutdown or make a list of things to delete for the next time it runs.