diff options
| -rw-r--r-- | .gitignore | 2 | ||||
| -rw-r--r-- | README.md | 2 | ||||
| -rw-r--r-- | build-server.hxml | 1 | ||||
| -rw-r--r-- | build/server.js | 539 | ||||
| -rw-r--r-- | package-lock.json | 15 | ||||
| -rw-r--r-- | package.json | 2 | ||||
| -rw-r--r-- | res/client.js | 10 | ||||
| -rw-r--r-- | src/client/Player.hx | 5 | ||||
| -rw-r--r-- | src/server/HttpServer.hx | 11 | ||||
| -rw-r--r-- | src/server/Main.hx | 8 | ||||
| -rw-r--r-- | src/server/cache/Cache.hx | 28 | ||||
| -rw-r--r-- | src/server/cache/RawCache.hx | 68 | ||||
| -rw-r--r-- | src/server/cache/YoutubeCache.hx | 266 | ||||
| -rw-r--r-- | src/utils/YoutubeUtils.hx | 60 | ||||
| -rw-r--r-- | tests.hxml | 1 |
15 files changed, 542 insertions, 476 deletions
@@ -6,5 +6,5 @@ /user/state.json
/user/config.json
/user/users.json
-/user/cookies.json
+/user/cookies.txt
/user/res/
@@ -54,7 +54,7 @@ or ## Optional dependencies
If you want to enable `Cache on server` feature for Youtube player, you can also run:
```shell
-npm i @distube/ytdl-core@latest
+npm i https://github.com/RblSb/ytdlp-nodejs
```
And install `ffmpeg` on your server system, it's only used to build single mp4 from downloaded audio/video tracks. Default cache size is 3.0 GiB.
diff --git a/build-server.hxml b/build-server.hxml index 7bc5e6b..8d4f8b4 100644 --- a/build-server.hxml +++ b/build-server.hxml @@ -1,6 +1,7 @@ --library hxnodejs
--library hxnodejs-ws
--library json2object:git:https://github.com/RblSb/json2object.git#nightly_safe_macros
+--library ytdlp-nodejs:git:https://github.com/RblSb/ytdlp-nodejs-externs.git
# Client libs for completion
--library youtubeIFramePlayer:git:https://github.com/okawa-h/youtubeIFramePlayer-externs.git
--library hls.js-extern:git:https://github.com/zoldesi-andor/hls.js-haxe-extern.git
diff --git a/build/server.js b/build/server.js index 2592fe8..8dd9077 100644 --- a/build/server.js +++ b/build/server.js @@ -1,4 +1,4 @@ -// Generated by Haxe 4.3.6 +// Generated by Haxe 4.3.7 (function ($global) { "use strict"; var $estr = function() { return js_Boot.__string_rec(this,''); },$hxEnums = $hxEnums || {},$_; function $extend(from, fields) { @@ -4089,7 +4089,7 @@ server_HttpServer.prototype = { } }); stream.on("error",function(err) { - haxe_Log.trace(err,{ fileName : "src/server/HttpServer.hx", lineNumber : 225, className : "server.HttpServer", methodName : "uploadFile"}); + haxe_Log.trace(err,{ fileName : "src/server/HttpServer.hx", lineNumber : 227, className : "server.HttpServer", methodName : "uploadFile"}); tools_HttpServerTools.json(tools_HttpServerTools.status(res,500),{ info : "File write stream error."}); var _this = _gthis.uploadingFilesSizes; if(Object.prototype.hasOwnProperty.call(_this.h,filePath)) { @@ -4102,7 +4102,7 @@ server_HttpServer.prototype = { _gthis.cache.remove(name); }); req.on("error",function(err) { - haxe_Log.trace("Request Error:",{ fileName : "src/server/HttpServer.hx", lineNumber : 232, className : "server.HttpServer", methodName : "uploadFile", customParams : [err]}); + haxe_Log.trace("Request Error:",{ fileName : "src/server/HttpServer.hx", lineNumber : 234, className : "server.HttpServer", methodName : "uploadFile", customParams : [err]}); stream.destroy(); tools_HttpServerTools.json(tools_HttpServerTools.status(res,500),{ info : "File request error."}); var _this = _gthis.uploadingFilesSizes; @@ -4131,7 +4131,7 @@ server_HttpServer.prototype = { var jsonParser = new JsonParser_$f3c29c0813c93ee49a61ccf072b8a177(); var jsonData = jsonParser.fromJson(body); if(jsonParser.errors.length > 0) { - haxe_Log.trace(json2object_ErrorUtils.convertErrorArray(jsonParser.errors),{ fileName : "src/server/HttpServer.hx", lineNumber : 258, className : "server.HttpServer", methodName : "finishSetup"}); + haxe_Log.trace(json2object_ErrorUtils.convertErrorArray(jsonParser.errors),{ fileName : "src/server/HttpServer.hx", lineNumber : 260, className : "server.HttpServer", methodName : "finishSetup"}); tools_HttpServerTools.json(tools_HttpServerTools.status(res,400),{ success : false, errors : []}); return; } @@ -4232,15 +4232,17 @@ server_HttpServer.prototype = { end = start + this.CHUNK_SIZE; } if(server_Utils.isOutOfRange(end,start,videoSize - 1)) { - end = videoSize - 1; + var a = videoSize - 1; + end = a < 0 ? 0 : a; } return { start : start, end : end}; } ,isMediaExtension: function(ext) { - if(!(ext == "mp4" || ext == "webm" || ext == "mp3")) { - return ext == "wav"; - } else { + switch(ext) { + case "mp3":case "mp4":case "ogg":case "wav":case "webm": return true; + default: + return false; } } ,localizeHtml: function(data,lang) { @@ -4490,7 +4492,7 @@ var server_Main = function(opts) { preparePort = function() { server_Utils.isPortFree(_gthis.port,function(isFree) { if(!isFree && attempts > 0) { - haxe_Log.trace("Warning: port " + _gthis.port + " is already in use. Changed to " + (_gthis.port + 1),{ fileName : "src/server/Main.hx", lineNumber : 140, className : "server.Main", methodName : "new"}); + haxe_Log.trace("Warning: port " + _gthis.port + " is already in use. Changed to " + (_gthis.port + 1),{ fileName : "src/server/Main.hx", lineNumber : 142, className : "server.Main", methodName : "new"}); attempts -= 1; _gthis.port++; preparePort(); @@ -4517,16 +4519,16 @@ server_Main.jsonFilterNulls = function(key,value) { server_Main.prototype = { runServer: function() { var _gthis = this; - haxe_Log.trace("Local: http://" + this.localIp + ":" + this.port,{ fileName : "src/server/Main.hx", lineNumber : 153, className : "server.Main", methodName : "runServer"}); + haxe_Log.trace("Local: http://" + this.localIp + ":" + this.port,{ fileName : "src/server/Main.hx", lineNumber : 155, className : "server.Main", methodName : "runServer"}); if(this.config.localNetworkOnly) { - haxe_Log.trace("Global network is disabled in config",{ fileName : "src/server/Main.hx", lineNumber : 155, className : "server.Main", methodName : "runServer"}); + haxe_Log.trace("Global network is disabled in config",{ fileName : "src/server/Main.hx", lineNumber : 157, className : "server.Main", methodName : "runServer"}); } else if(!this.isNoState) { server_Utils.getGlobalIp(function(ip) { if(ip.indexOf(":") != -1) { ip = "[" + ip + "]"; } _gthis.globalIp = ip; - haxe_Log.trace("Global: http://" + _gthis.globalIp + ":" + _gthis.port,{ fileName : "src/server/Main.hx", lineNumber : 161, className : "server.Main", methodName : "runServer"}); + haxe_Log.trace("Global: http://" + _gthis.globalIp + ":" + _gthis.port,{ fileName : "src/server/Main.hx", lineNumber : 163, className : "server.Main", methodName : "runServer"}); }); } var dir = "" + this.rootDir + "/res"; @@ -4611,7 +4613,7 @@ server_Main.prototype = { var field = _g1[_g]; ++_g; if(Reflect.field(config,field) == null) { - haxe_Log.trace("Warning: config field \"" + field + "\" is unknown",{ fileName : "src/server/Main.hx", lineNumber : 235, className : "server.Main", methodName : "getUserConfig"}); + haxe_Log.trace("Warning: config field \"" + field + "\" is unknown",{ fileName : "src/server/Main.hx", lineNumber : 237, className : "server.Main", methodName : "getUserConfig"}); } config[field] = Reflect.field(customConfig,field); } @@ -4622,14 +4624,14 @@ server_Main.prototype = { var emote = _g1[_g]; ++_g; if(emoteCopies_h[emote.name]) { - haxe_Log.trace("Warning: emote name \"" + emote.name + "\" has copy",{ fileName : "src/server/Main.hx", lineNumber : 241, className : "server.Main", methodName : "getUserConfig"}); + haxe_Log.trace("Warning: emote name \"" + emote.name + "\" has copy",{ fileName : "src/server/Main.hx", lineNumber : 243, className : "server.Main", methodName : "getUserConfig"}); } emoteCopies_h[emote.name] = true; if(!this.verbose) { continue; } if(emoteCopies_h[emote.image]) { - haxe_Log.trace("Warning: emote url of name \"" + emote.name + "\" has copy",{ fileName : "src/server/Main.hx", lineNumber : 245, className : "server.Main", methodName : "getUserConfig"}); + haxe_Log.trace("Warning: emote url of name \"" + emote.name + "\" has copy",{ fileName : "src/server/Main.hx", lineNumber : 247, className : "server.Main", methodName : "getUserConfig"}); } emoteCopies_h[emote.image] = true; } @@ -4666,7 +4668,7 @@ server_Main.prototype = { js_node_Fs.writeFileSync("" + this.userDir + "/users.json",JSON.stringify({ admins : users1, bans : _g, salt : users.salt},null,"\t")); } ,saveState: function() { - haxe_Log.trace("Saving state...",{ fileName : "src/server/Main.hx", lineNumber : 283, className : "server.Main", methodName : "saveState"}); + haxe_Log.trace("Saving state...",{ fileName : "src/server/Main.hx", lineNumber : 285, className : "server.Main", methodName : "saveState"}); var json = JSON.stringify(this.getCurrentState(),null,"\t"); js_node_Fs.writeFileSync(this.statePath,json); this.writeUsers(this.userList); @@ -4681,7 +4683,7 @@ server_Main.prototype = { if(!sys_FileSystem.exists(this.statePath)) { return; } - haxe_Log.trace("Loading state...",{ fileName : "src/server/Main.hx", lineNumber : 307, className : "server.Main", methodName : "loadState"}); + haxe_Log.trace("Loading state...",{ fileName : "src/server/Main.hx", lineNumber : 309, className : "server.Main", methodName : "loadState"}); var state = JSON.parse(js_node_Fs.readFileSync(this.statePath,{ encoding : "utf8"})); state.flashbacks = state.flashbacks != null ? state.flashbacks : []; state.cachedFiles = state.cachedFiles != null ? state.cachedFiles : []; @@ -4703,7 +4705,7 @@ server_Main.prototype = { } ,logError: function(type,data) { this.cache.removeOlderCache(1048576); - haxe_Log.trace(type,{ fileName : "src/server/Main.hx", lineNumber : 331, className : "server.Main", methodName : "logError", customParams : [data]}); + haxe_Log.trace(type,{ fileName : "src/server/Main.hx", lineNumber : 333, className : "server.Main", methodName : "logError", customParams : [data]}); var crashesFolder = "" + this.userDir + "/crashes"; server_Utils.ensureDir(crashesFolder); var name = DateTools.format(new Date(),"%Y-%m-%d_%H_%M_%S") + "-" + type; @@ -4725,7 +4727,7 @@ server_Main.prototype = { if(_gthis.clients.length == 0) { return; } - haxe_Log.trace("Ping " + url,{ fileName : "src/server/Main.hx", lineNumber : 344, className : "server.Main", methodName : "initIntergationHandlers"}); + haxe_Log.trace("Ping " + url,{ fileName : "src/server/Main.hx", lineNumber : 346, className : "server.Main", methodName : "initIntergationHandlers"}); js_node_Http.get(url,null,function(r) { }); }; @@ -4744,13 +4746,13 @@ server_Main.prototype = { password += this.config.salt; var hash = haxe_crypto_Sha256.encode(password); this.userList.admins.push({ name : name, hash : hash}); - haxe_Log.trace("Admin " + name + " added.",{ fileName : "src/server/Main.hx", lineNumber : 365, className : "server.Main", methodName : "addAdmin"}); + haxe_Log.trace("Admin " + name + " added.",{ fileName : "src/server/Main.hx", lineNumber : 367, className : "server.Main", methodName : "addAdmin"}); } ,removeAdmin: function(name) { HxOverrides.remove(this.userList.admins,Lambda.find(this.userList.admins,function(item) { return item.name == name; })); - haxe_Log.trace("Admin " + name + " removed.",{ fileName : "src/server/Main.hx", lineNumber : 372, className : "server.Main", methodName : "removeAdmin"}); + haxe_Log.trace("Admin " + name + " removed.",{ fileName : "src/server/Main.hx", lineNumber : 374, className : "server.Main", methodName : "removeAdmin"}); } ,hasAdmins: function() { return this.userList.admins.length > 0; @@ -4820,7 +4822,7 @@ server_Main.prototype = { var ip = this.clientIp(req); var id = this.freeIds.length > 0 ? this.freeIds.shift() : this.clients.length; var name = "Guest " + (id + 1); - haxe_Log.trace(HxOverrides.dateStr(new Date()),{ fileName : "src/server/Main.hx", lineNumber : 435, className : "server.Main", methodName : "onConnect", customParams : ["" + name + " connected (" + ip + ")"]}); + haxe_Log.trace(HxOverrides.dateStr(new Date()),{ fileName : "src/server/Main.hx", lineNumber : 437, className : "server.Main", methodName : "onConnect", customParams : ["" + name + " connected (" + ip + ")"]}); var isAdmin = this.config.localAdmins && req.socket.localAddress == ip; var client = new Client(ws,req,id,name,0); client.uuid = uuid; @@ -4834,7 +4836,7 @@ server_Main.prototype = { var obj = _gthis.wsEventParser.fromJson(data.toString()); if(_gthis.wsEventParser.errors.length > 0 || _gthis.noTypeObj(obj)) { var errors = "" + ("Wrong request for type \"" + obj.type + "\":") + "\n" + json2object_ErrorUtils.convertErrorArray(_gthis.wsEventParser.errors); - haxe_Log.trace(errors,{ fileName : "src/server/Main.hx", lineNumber : 452, className : "server.Main", methodName : "onConnect"}); + haxe_Log.trace(errors,{ fileName : "src/server/Main.hx", lineNumber : 454, className : "server.Main", methodName : "onConnect"}); _gthis.serverMessage(client,errors); return; } @@ -5029,7 +5031,7 @@ server_Main.prototype = { if(!internal) { return; } - haxe_Log.trace(HxOverrides.dateStr(new Date()),{ fileName : "src/server/Main.hx", lineNumber : 517, className : "server.Main", methodName : "onMessage", customParams : ["Client " + client.name + " disconnected"]}); + haxe_Log.trace(HxOverrides.dateStr(new Date()),{ fileName : "src/server/Main.hx", lineNumber : 519, className : "server.Main", methodName : "onMessage", customParams : ["Client " + client.name + " disconnected"]}); server_Utils.sortedPush(this.freeIds,client.id); HxOverrides.remove(this.clients,client); this.sendClientList(); @@ -5080,6 +5082,7 @@ server_Main.prototype = { } var json = server_Main.jsonStringify({ state : data1, clients : result, logs : this.logger.getLogs()},"\t"); this.serverMessage(client,"Free space: " + tools_MathTools.toFixed(this.cache.getFreeSpace() / 1024) + "KiB"); + this.serverMessage(client,"Memory usage: " + Std.string(process.memoryUsage())); this.send(client,{ type : "Dump", dump : { data : json}}); break; case "Flashback": @@ -5169,7 +5172,7 @@ server_Main.prototype = { this.send(client,{ type : "LoginError"}); return; } - haxe_Log.trace(HxOverrides.dateStr(new Date()),{ fileName : "src/server/Main.hx", lineNumber : 608, className : "server.Main", methodName : "onMessage", customParams : ["Client " + client.name + " logged as " + name]}); + haxe_Log.trace(HxOverrides.dateStr(new Date()),{ fileName : "src/server/Main.hx", lineNumber : 610, className : "server.Main", methodName : "onMessage", customParams : ["Client " + client.name + " logged as " + name]}); client.name = name; client.setGroupFlag(ClientGroup.User,true); this.checkBan(client); @@ -5182,7 +5185,7 @@ server_Main.prototype = { var oldName = client.name; client.name = "Guest " + (this.clients.indexOf(client) + 1); client.setGroupFlag(ClientGroup.User,false); - haxe_Log.trace(HxOverrides.dateStr(new Date()),{ fileName : "src/server/Main.hx", lineNumber : 629, className : "server.Main", methodName : "onMessage", customParams : ["Client " + oldName + " logout to " + client.name]}); + haxe_Log.trace(HxOverrides.dateStr(new Date()),{ fileName : "src/server/Main.hx", lineNumber : 631, className : "server.Main", methodName : "onMessage", customParams : ["Client " + oldName + " logout to " + client.name]}); this.send(client,{ type : data.type, logout : { oldClientName : oldName, clientName : client.name, clients : this.clientList()}}); this.sendClientListExcept(client); break; @@ -5445,6 +5448,13 @@ server_Main.prototype = { ,send: function(client,data) { client.ws.send(server_Main.jsonStringify(data),null); } + ,sendByName: function(clientName,data) { + var tmp = ClientTools.getByName(this.clients,clientName); + if(tmp == null) { + return; + } + tmp.ws.send(server_Main.jsonStringify(data),null); + } ,broadcast: function(data) { var json = server_Main.jsonStringify(data); var _g = 0; @@ -5512,7 +5522,7 @@ server_Main.prototype = { client.setGroupFlag(ClientGroup.Banned,!isOutdated); if(isOutdated) { HxOverrides.remove(this.userList.bans,ban); - haxe_Log.trace("" + client.name + " ban removed",{ fileName : "src/server/Main.hx", lineNumber : 1070, className : "server.Main", methodName : "checkBan"}); + haxe_Log.trace("" + client.name + " ban removed",{ fileName : "src/server/Main.hx", lineNumber : 1078, className : "server.Main", methodName : "checkBan"}); this.sendClientList(); } break; @@ -5851,6 +5861,9 @@ server_cache_Cache.prototype = { this.cachedFiles.length = 0; var _g = 0; while(_g < names.length) this.cachedFiles.push(names[_g++]); + this.removeUntrackedFiles(); + } + ,removeUntrackedFiles: function() { var names = js_node_Fs.readdirSync(this.cacheDir); var _g = 0; while(_g < names.length) { @@ -5865,13 +5878,42 @@ server_cache_Cache.prototype = { if(this.cachedFiles.indexOf(name) != -1) { continue; } - haxe_Log.trace("Remove non-tracked cache " + name,{ fileName : "src/server/cache/Cache.hx", lineNumber : 47, className : "server.cache.Cache", methodName : "setCachedFiles"}); + haxe_Log.trace("Remove untracked cache " + name,{ fileName : "src/server/cache/Cache.hx", lineNumber : 51, className : "server.cache.Cache", methodName : "removeUntrackedFiles"}); this.remove(name); } } ,log: function(client,msg) { + haxe_Log.trace(msg,{ fileName : "src/server/cache/Cache.hx", lineNumber : 57, className : "server.cache.Cache", methodName : "log"}); this.main.serverMessage(client,msg); - haxe_Log.trace(msg,{ fileName : "src/server/cache/Cache.hx", lineNumber : 54, className : "server.cache.Cache", methodName : "log"}); + } + ,logByName: function(clientName,msg) { + haxe_Log.trace(msg,{ fileName : "src/server/cache/Cache.hx", lineNumber : 62, className : "server.cache.Cache", methodName : "logByName"}); + var tmp = ClientTools.getByName(this.main.clients,clientName); + if(tmp == null) { + return; + } + this.main.serverMessage(tmp,msg); + } + ,logWithAdmins: function(client,msg) { + this.log(client,msg); + var _this = this.main.clients; + var _g = []; + var _g1 = 0; + while(_g1 < _this.length) { + var v = _this[_g1]; + ++_g1; + if((v.group & 8) != 0) { + _g.push(v); + } + } + var _g1 = 0; + while(_g1 < _g.length) { + var admin = _g[_g1]; + ++_g1; + if(client != admin) { + this.main.serverMessage(admin,msg); + } + } } ,cacheYoutubeVideo: function(client,url,callback) { this.youtubeCache.cacheYoutubeVideo(client,url,callback); @@ -5900,13 +5942,13 @@ server_cache_Cache.prototype = { var _gthis = this; var tmp = js_node_Fs.statfs; if(tmp == null) { - haxe_Log.trace("Warning: no fs.statfs support in current nodejs version (needs v18+)",{ fileName : "src/server/cache/Cache.hx", lineNumber : 83, className : "server.cache.Cache", methodName : "getFreeDiskSpace"}); + haxe_Log.trace("Warning: no fs.statfs support in current nodejs version (needs v18+)",{ fileName : "src/server/cache/Cache.hx", lineNumber : 101, className : "server.cache.Cache", methodName : "getFreeDiskSpace"}); callback(this.storageLimit); return; } tmp("/",function(err,stats) { if(err != null) { - haxe_Log.trace(err,{ fileName : "src/server/cache/Cache.hx", lineNumber : 89, className : "server.cache.Cache", methodName : "getFreeDiskSpace"}); + haxe_Log.trace(err,{ fileName : "src/server/cache/Cache.hx", lineNumber : 107, className : "server.cache.Cache", methodName : "getFreeDiskSpace"}); callback(_gthis.storageLimit); return; } @@ -5979,6 +6021,18 @@ server_cache_Cache.prototype = { ,isFileExists: function(name) { return sys_FileSystem.exists(this.getFilePath(name)); } + ,findFile: function(callback) { + var names = js_node_Fs.readdirSync(this.cacheDir); + var _g = 0; + while(_g < names.length) { + var name = names[_g]; + ++_g; + if(callback(name)) { + return name; + } + } + return null; + } ,getFreeSpace: function() { return this.storageLimit - this.getUsedSpace(); } @@ -6034,31 +6088,33 @@ server_cache_RawCache.prototype = { } ,handleMp4: function(client,url,outName,callback) { var _gthis = this; + var clientName = client.name; this.downloadFile(client,url,outName,function(downloaded,total) { var v = downloaded / total; - _gthis.main.send(client,{ type : "Progress", progress : { type : "Downloading", ratio : v < 0 ? 0 : v > 1 ? 1 : v}}); + _gthis.main.sendByName(clientName,{ type : "Progress", progress : { type : "Downloading", ratio : tools_MathTools.toFixed(v < 0 ? 0 : v > 1 ? 1 : v,4)}}); },function() { _gthis.cache.add(outName); callback(outName); },function(err) { - _gthis.log(client,"Mp4 download failed: " + err); - _gthis.cancelProgress(client); + _gthis.log(clientName,"Mp4 download failed: " + err); + _gthis.cancelProgress(clientName); }); } ,handleM3u8: function(client,url,outName,callback) { var _gthis = this; + var clientName = client.name; var useProxy = true; this.downloadM3u8Playlist(client,url,useProxy,function(playlist,totalSize,segments) { if(useProxy) { totalSize = playlist.length; } if(!_gthis.cache.removeOlderCache(totalSize + _gthis.cache.freeSpaceBlock)) { - _gthis.log(client,_gthis.cache.notEnoughSpaceErrorText); - _gthis.cancelProgress(client); + _gthis.log(clientName,_gthis.cache.notEnoughSpaceErrorText); + _gthis.cancelProgress(clientName); return; } if(useProxy) { - _gthis.main.send(client,{ type : "Progress", progress : { type : "Caching", ratio : 1, data : outName}}); + _gthis.main.sendByName(clientName,{ type : "Progress", progress : { type : "Caching", ratio : 1, data : outName}}); js_node_Fs.writeFileSync("" + _gthis.cache.cacheDir + "/" + outName,playlist); _gthis.cache.add(outName); callback(outName); @@ -6081,7 +6137,7 @@ server_cache_RawCache.prototype = { } segment[0].started = true; activeDownloads += 1; - haxe_Log.trace("download segment",{ fileName : "src/server/cache/RawCache.hx", lineNumber : 118, className : "server.cache.RawCache", methodName : "handleM3u8", customParams : [segment[0].i]}); + haxe_Log.trace("download segment",{ fileName : "src/server/cache/RawCache.hx", lineNumber : 120, className : "server.cache.RawCache", methodName : "handleM3u8", customParams : [segment[0].i]}); _gthis.downloadFile(client,segment[0].url,segment[0].name,(function() { return function(downloadedBytes,totalBytes) { }; @@ -6091,9 +6147,9 @@ server_cache_RawCache.prototype = { segment[0].completed = true; downloaded += 1; var progress = downloaded / segments.length; - _gthis.main.send(client,{ type : "Progress", progress : { type : "Downloading", ratio : progress < 0 ? 0 : progress > 1 ? 1 : progress}}); + _gthis.main.sendByName(clientName,{ type : "Progress", progress : { type : "Downloading", ratio : progress < 0 ? 0 : progress > 1 ? 1 : progress}}); if(downloaded == segments.length) { - haxe_Log.trace("All " + downloaded + "/" + segments.length + " segments downloaded",{ fileName : "src/server/cache/RawCache.hx", lineNumber : 138, className : "server.cache.RawCache", methodName : "handleM3u8"}); + haxe_Log.trace("All " + downloaded + "/" + segments.length + " segments downloaded",{ fileName : "src/server/cache/RawCache.hx", lineNumber : 140, className : "server.cache.RawCache", methodName : "handleM3u8"}); js_node_Fs.writeFileSync("" + _gthis.cache.cacheDir + "/" + outName,playlist); _gthis.cache.add(outName); callback(outName); @@ -6105,8 +6161,8 @@ server_cache_RawCache.prototype = { return function(err) { activeDownloads -= 1; downloaded += 1; - _gthis.log(client,"TS segment " + segment[0].i + " download failed: " + err); - _gthis.cancelProgress(client); + _gthis.log(clientName,"TS segment " + segment[0].i + " download failed: " + err); + _gthis.cancelProgress(clientName); var _gthis1 = _gthis; var result = new Array(segments.length); var _g = 0; @@ -6122,8 +6178,8 @@ server_cache_RawCache.prototype = { }; downloadNextBatch(); },function(err) { - _gthis.log(client,"M3U8 processing failed: " + err); - _gthis.cancelProgress(client); + _gthis.log(clientName,"M3U8 processing failed: " + err); + _gthis.cancelProgress(clientName); }); } ,request: function(url,options,callback) { @@ -6250,11 +6306,11 @@ server_cache_RawCache.prototype = { } } } - ,log: function(client,msg) { - this.cache.log(client,msg); + ,log: function(clientName,msg) { + this.cache.logByName(clientName,msg); } - ,cancelProgress: function(client) { - this.main.send(client,{ type : "Progress", progress : { type : "Canceled", ratio : 0}}); + ,cancelProgress: function(clientName) { + this.main.sendByName(clientName,{ type : "Progress", progress : { type : "Canceled", ratio : 0}}); } ,__class__: server_cache_RawCache }; @@ -6265,26 +6321,24 @@ var server_cache_YoutubeCache = function(main,cache) { server_cache_YoutubeCache.__name__ = true; server_cache_YoutubeCache.prototype = { checkYtDeps: function() { - var ytdl; - try { - ytdl = require("@distube/ytdl-core"); - } catch( _g ) { - return false; - } try { - js_node_ChildProcess.execSync("ffmpeg -version",{ stdio : "ignore", timeout : 3000}); + js_node_ChildProcess.execSync("ffmpeg -version",{ stdio : "ignore", timeout : 5000}); + this.ytDlp = new (require('ytdlp-nodejs')).YtDlp(); return true; } catch( _g ) { return false; } } - ,cleanYtInputFiles: function() { + ,cleanYtInputFiles: function(prefix) { + if(prefix == null) { + prefix = "__tmp"; + } var names = js_node_Fs.readdirSync(this.cache.cacheDir); var _g = 0; while(_g < names.length) { var name = names[_g]; ++_g; - if(!StringTools.startsWith(name,"__tmp")) { + if(!StringTools.startsWith(name,prefix)) { continue; } this.cache.remove(name); @@ -6293,12 +6347,13 @@ server_cache_YoutubeCache.prototype = { ,cacheYoutubeVideo: function(client,url,callback) { var _gthis = this; if(!this.cache.isYtReady) { - haxe_Log.trace("Do `npm i @distube/ytdl-core@latest` to use cache feature (you also need to install `ffmpeg` to build mp4 from downloaded audio/video tracks).",{ fileName : "src/server/cache/YoutubeCache.hx", lineNumber : 46, className : "server.cache.YoutubeCache", methodName : "cacheYoutubeVideo"}); + haxe_Log.trace("Do `npm i https://github.com/RblSb/ytdlp-nodejs` to use cache feature (you also need to install `ffmpeg` to build mp4 from downloaded audio/video tracks).",{ fileName : "src/server/cache/YoutubeCache.hx", lineNumber : 42, className : "server.cache.YoutubeCache", methodName : "cacheYoutubeVideo"}); return; } + var clientName = client.name; var videoId = utils_YoutubeUtils.extractVideoId(url); if(videoId == "") { - this.log(client,"Error: youtube video id not found in url: " + url); + this.log(clientName,"Error: youtube video id not found in url: " + url); return; } var outName = videoId + ".mp4"; @@ -6307,221 +6362,247 @@ server_cache_YoutubeCache.prototype = { return; } var inVideoName = "__tmp-video-" + videoId; - var inAudioName = "__tmp-audio-" + videoId; if(this.cache.isFileExists(inVideoName)) { - this.log(client,"Caching " + outName + " already in progress"); + this.log(clientName,"Caching " + outName + " already in progress"); return; } - var ytdl = require("@distube/ytdl-core"); - haxe_Log.trace("Caching " + url + " to " + outName + "...",{ fileName : "src/server/cache/YoutubeCache.hx", lineNumber : 80, className : "server.cache.YoutubeCache", methodName : "cacheYoutubeVideo"}); - this.main.send(client,{ type : "Progress", progress : { type : "Caching", ratio : 0, data : outName}}); - var agent = null; - var cookiesPath = "" + this.main.userDir + "/cookies.json"; - if(sys_FileSystem.exists(cookiesPath)) { - agent = ytdl.createAgent(JSON.parse(js_node_Fs.readFileSync(cookiesPath,{ encoding : "utf8"}))); - } - var promise = ytdl.getInfo(url,{ agent : agent}); - promise.then(function(info) { - haxe_Log.trace("Get info with " + info.formats.length + " formats",{ fileName : "src/server/cache/YoutubeCache.hx", lineNumber : 98, className : "server.cache.YoutubeCache", methodName : "cacheYoutubeVideo"}); - var audioFormat; - try { - var ytdl1 = ytdl.chooseFormat; + haxe_Log.trace("Caching " + url + " to " + outName + "...",{ fileName : "src/server/cache/YoutubeCache.hx", lineNumber : 74, className : "server.cache.YoutubeCache", methodName : "cacheYoutubeVideo"}); + this.main.sendByName(clientName,{ type : "Progress", progress : { type : "Caching", ratio : 0, data : outName}}); + var useCookies = false; + var onGetInfo = function(info) { + haxe_Log.trace("Get info with " + info.formats.length + " formats",{ fileName : "src/server/cache/YoutubeCache.hx", lineNumber : 87, className : "server.cache.YoutubeCache", methodName : "cacheYoutubeVideo"}); + var _this = info.formats; + var _g = []; + var _g1 = 0; + while(_g1 < _this.length) { + var v = _this[_g1]; + ++_g1; + if(v.vcodec == "none") { + _g.push(v); + } + } + var aformats = _g; + if(_g.length == 0) { + var _this = info.formats; var _g = []; var _g1 = 0; - var _g2 = info.formats; - while(_g1 < _g2.length) { - var v = _g2[_g1]; + while(_g1 < _this.length) { + var v = _this[_g1]; ++_g1; - var tmp = v.audioCodec; - if(tmp != null ? StringTools.startsWith(tmp,"mp4a") : null) { + if(v.acodec != "none") { _g.push(v); } } - audioFormat = ytdl1(_g,{ quality : "highestaudio"}); - } catch( _g ) { - var e = haxe_Exception.caught(_g); - _gthis.log(client,"Error: audio format not found"); - haxe_Log.trace(e,{ fileName : "src/server/cache/YoutubeCache.hx", lineNumber : 105, className : "server.cache.YoutubeCache", methodName : "cacheYoutubeVideo"}); - var _g1 = []; - var _g2 = 0; - var _g3 = info.formats; - while(_g2 < _g3.length) { - var v = _g3[_g2]; - ++_g2; - if(v.hasAudio) { - _g1.push(v); - } + aformats = _g; + } + aformats.sort(function(a,b) { + var tmp = a != null ? a.filesize : null; + var tmp1 = b != null ? b.filesize : null; + if((tmp != null ? tmp : 0) < (tmp1 != null ? tmp1 : 0)) { + return 1; + } else { + return -1; } - haxe_Log.trace(_g1,{ fileName : "src/server/cache/YoutubeCache.hx", lineNumber : 106, className : "server.cache.YoutubeCache", methodName : "cacheYoutubeVideo"}); + }); + var tmp = aformats[0]; + if(tmp == null) { + _gthis.log(clientName,"Error: format with audio not found"); + var _g = 0; + var _g1 = info.formats; + while(_g < _g1.length) haxe_Log.trace(_g1[_g++],{ fileName : "src/server/cache/YoutubeCache.hx", lineNumber : 95, className : "server.cache.YoutubeCache", methodName : "cacheYoutubeVideo"}); return; } + var _this = info.formats; + var _g = []; + var _g1 = 0; + while(_g1 < _this.length) { + var v = _this[_g1]; + ++_g1; + if(v.vcodec != "none") { + _g.push(v); + } + } + _g.sort(function(a,b) { + var tmp = a != null ? a.filesize : null; + var tmp1 = b != null ? b.filesize : null; + if((tmp != null ? tmp : 0) < (tmp1 != null ? tmp1 : 0)) { + return 1; + } else { + return -1; + } + }); var videoFormat; - var tmp = _gthis.getBestYoutubeVideoFormat(info.formats); - if(tmp != null) { - videoFormat = tmp; + var tmp1 = _gthis.getBestYoutubeVideoFormat(_g); + if(tmp1 != null) { + videoFormat = tmp1; } else { - _gthis.log(client,"Error: video format not found"); - var _g = []; + _gthis.log(clientName,"Error: video format not found"); var _g1 = 0; var _g2 = info.formats; - while(_g1 < _g2.length) { - var v = _g2[_g1]; - ++_g1; - if(v.hasVideo) { - _g.push(v); - } - } - haxe_Log.trace(_g,{ fileName : "src/server/cache/YoutubeCache.hx", lineNumber : 111, className : "server.cache.YoutubeCache", methodName : "cacheYoutubeVideo"}); + while(_g1 < _g2.length) haxe_Log.trace(_g2[_g1++],{ fileName : "src/server/cache/YoutubeCache.hx", lineNumber : 102, className : "server.cache.YoutubeCache", methodName : "cacheYoutubeVideo"}); return; } - var tmp = Std.parseInt(videoFormat.contentLength); - var videoSize = tmp != null ? tmp : 0; - var tmp = Std.parseInt(audioFormat.contentLength); - var audioSize = tmp != null ? tmp : 0; - var hasSpace = _gthis.cache.removeOlderCache((videoSize + audioSize) * 2 + _gthis.cache.freeSpaceBlock); - if(!hasSpace) { - videoFormat = _gthis.getBestYoutubeVideoFormat(info.formats,videoFormat.qualityLabel); - var tmp = Std.parseInt(videoFormat.contentLength); - var videoSize = tmp != null ? tmp : 0; - var tmp = Std.parseInt(audioFormat.contentLength); - var audioSize = tmp != null ? tmp : 0; - var hasSpace = _gthis.cache.removeOlderCache((videoSize + audioSize) * 2 + _gthis.cache.freeSpaceBlock); - if(!hasSpace) { - _gthis.cache.remove(inVideoName); - _gthis.cache.remove(inAudioName); - _gthis.cancelProgress(client); - _gthis.log(client,_gthis.cache.notEnoughSpaceErrorText); + var ignoreQualities = []; + var _g1 = 0; + while(_g1 < 3) { + ++_g1; + var tmp1 = videoFormat.filesize; + var tmp2 = tmp.filesize; + if(_gthis.cache.removeOlderCache(((tmp1 != null ? tmp1 : 0) + (tmp2 != null ? tmp2 : 0)) * 2 + _gthis.cache.freeSpaceBlock)) { + break; } - if(!hasSpace) { - return; + var tmp3 = videoFormat.height; + ignoreQualities.push((tmp3 != null ? tmp3 : 0) | 0); + var tmp4 = _gthis.getBestYoutubeVideoFormat(_g,ignoreQualities); + if(tmp4 != null) { + videoFormat = tmp4; + } else { + break; } } - var dlVideo = ytdl(url,{ format : videoFormat, agent : agent}); - dlVideo.pipe(js_node_Fs.createWriteStream("" + _gthis.cache.cacheDir + "/" + inVideoName)); - dlVideo.on("error",function(err) { - _gthis.log(client,"Error during video download: " + err); - _gthis.cache.remove(inVideoName); - _gthis.cache.remove(inAudioName); - _gthis.cancelProgress(client); - }); - var dlAudio = ytdl(url,{ format : audioFormat, agent : agent}); - dlAudio.pipe(js_node_Fs.createWriteStream("" + _gthis.cache.cacheDir + "/" + inAudioName)); - dlAudio.on("error",function(err) { - _gthis.log(client,"Error during audio download: " + err); - _gthis.cache.remove(inVideoName); - _gthis.cache.remove(inAudioName); - _gthis.cancelProgress(client); - }); - var count = 0; - var onComplete = function(type) { - count += 1; - haxe_Log.trace("" + type + " track downloaded (" + count + "/2)",{ fileName : "src/server/cache/YoutubeCache.hx", lineNumber : 153, className : "server.cache.YoutubeCache", methodName : "cacheYoutubeVideo"}); - if(count < 2) { - return; - } - if(!_gthis.cache.isFileExists(inVideoName) || !_gthis.cache.isFileExists(inAudioName)) { - _gthis.log(client,"Input files not found for making final video"); - _gthis.cache.remove(inVideoName); - _gthis.cache.remove(inAudioName); - _gthis.cancelProgress(client); - return; + var tmp1 = videoFormat.filesize; + var tmp2 = tmp.filesize; + var hasSpace = _gthis.cache.removeOlderCache(((tmp1 != null ? tmp1 : 0) + (tmp2 != null ? tmp2 : 0)) * 2 + _gthis.cache.freeSpaceBlock); + if(!hasSpace) { + _gthis.cleanYtInputFiles(inVideoName); + _gthis.cancelProgress(clientName); + _gthis.log(clientName,_gthis.cache.notEnoughSpaceErrorText); + } + if(!hasSpace) { + return; + } + var tmp1 = videoFormat.filesize; + var tmp2 = tmp.filesize; + var a = (tmp1 != null ? tmp1 : 0) + (tmp2 != null ? tmp2 : 0); + var totalSize = a < 10 ? 10 : a; + var tmp1 = videoFormat.filesize; + var a = tmp1 != null ? tmp1 : 0; + var videoSizeRatio = (a < 8 ? 8 : a) / totalSize; + var tmp1 = tmp.filesize; + var a = tmp1 != null ? tmp1 : 0; + var audioSizeRatio = (a < 2 ? 2 : a) / totalSize; + var isVideoFormatDownloading = true; + var dlVideo = _gthis.ytDlp.downloadAsync(url,{ format : videoFormat.format_id == tmp.format_id ? videoFormat.format_id : "" + videoFormat.format_id + "+" + tmp.format_id, output : "" + _gthis.cache.cacheDir + "/" + inVideoName, remuxVideo : "mp4", cookies : useCookies ? _gthis.getCookiesPathOrNull() : null, onProgress : function(p) { + var isFinished = p.status == "finished"; + var ratio; + if(isFinished) { + ratio = 1; + } else { + var v = p.downloaded / p.total; + ratio = v < 0 ? 0 : v > 1 ? 1 : v; } - var size = js_node_Fs.statSync("" + _gthis.cache.cacheDir + "/" + inVideoName).size; - size += js_node_Fs.statSync("" + _gthis.cache.cacheDir + "/" + inAudioName).size; - var hasSpace = _gthis.cache.removeOlderCache(size + _gthis.cache.freeSpaceBlock); - if(!hasSpace) { - _gthis.cache.remove(inVideoName); - _gthis.cache.remove(inAudioName); - _gthis.cancelProgress(client); - _gthis.log(client,_gthis.cache.notEnoughSpaceErrorText); + if(isVideoFormatDownloading) { + ratio *= videoSizeRatio; + } else { + ratio = videoSizeRatio + ratio * audioSizeRatio; } - if(!hasSpace) { - return; + if(isFinished) { + isVideoFormatDownloading = false; } - var args = ("-y -i ./" + inVideoName + " -i ./" + inAudioName + " -c copy -map 0:v -map 1:a ./" + outName).split(" "); - var $process = js_node_ChildProcess.spawn("ffmpeg",args,{ cwd : _gthis.cache.cacheDir}); - var outputData = []; - $process.stderr.on("data",function(data) { - return outputData.push(data); - }); - $process.on("close",function(code) { - _gthis.cache.remove(inVideoName); - _gthis.cache.remove(inAudioName); - if(code != 0) { - _gthis.cancelProgress(client); - var errCodeMsg = "Error: ffmpeg closed with code " + code; - var _g = []; - var _g1 = 0; - var _g2 = _gthis.main.clients; - while(_g1 < _g2.length) { - var v = _g2[_g1]; - ++_g1; - if((v.group & 1 << ClientGroup.Admin._hx_index) != 0) { - _g.push(v); - } - } - var admins = _g; - var _g = 0; - while(_g < admins.length) { - var client1 = admins[_g]; - ++_g; - _gthis.log(client1,js_node_buffer_Buffer.concat(outputData).toString()); - _gthis.log(client1,errCodeMsg); - } - if(admins.indexOf(client) == -1) { - _gthis.log(client,errCodeMsg); - } - return; + _gthis.main.sendByName(clientName,{ type : "Progress", progress : { type : "Downloading", ratio : tools_MathTools.toFixed(ratio,4)}}); + }}).catch(function(err) { + _gthis.cache.logWithAdmins(client,"Error during video download: " + err); + _gthis.cleanYtInputFiles(inVideoName); + _gthis.cancelProgress(clientName); + }); + dlVideo.then(function(v) { + var tmp = _gthis.cache.findFile(function(n) { + if(StringTools.startsWith(n,inVideoName)) { + return StringTools.endsWith(n,".mp4"); + } else { + return false; } - _gthis.cache.add(outName); - callback(outName); }); - }; - dlVideo.on("finish",function() { - onComplete("Video"); - }); - dlAudio.on("finish",function() { - onComplete("Audio"); + if(tmp == null) { + _gthis.cache.logWithAdmins(client,"Error: cannot find downloaded file with prefix " + inVideoName); + return; + } + js_node_Fs.renameSync("" + _gthis.cache.cacheDir + "/" + tmp,"" + _gthis.cache.cacheDir + "/" + outName); + _gthis.cleanYtInputFiles(inVideoName); + _gthis.cache.add(outName); + callback(outName); }); - dlVideo.on("progress",function(chunkLength,downloaded,contentLength) { - var v = downloaded / contentLength; - var ratio = v < 0 ? 0 : v > 1 ? 1 : v; - _gthis.main.send(client,{ type : "Progress", progress : { type : "Downloading", ratio : ratio}}); + }; + this.getInfoAsync(url,useCookies).then(onGetInfo).catch(function(err) { + haxe_Log.trace(err,{ fileName : "src/server/cache/YoutubeCache.hx", lineNumber : 178, className : "server.cache.YoutubeCache", methodName : "cacheYoutubeVideo"}); + useCookies = true; + return _gthis.getInfoAsync(url,useCookies).then(onGetInfo).catch(function(err) { + _gthis.cleanYtInputFiles(inVideoName); + _gthis.cancelProgress(clientName); + _gthis.log(clientName,"" + err); }); - }).catch(function(err) { - _gthis.cache.remove(inVideoName); - _gthis.cache.remove(inAudioName); - _gthis.cancelProgress(client); - _gthis.log(client,"" + err); }); } - ,getBestYoutubeVideoFormat: function(formats,ignoreQuality) { + ,getInfoAsync: function(url,useCookies) { + if(useCookies == null) { + useCookies = false; + } + return this.ytDlp.execAsync(url,{ dumpSingleJson : true, quiet : true, cookies : useCookies ? this.getCookiesPathOrNull() : null}).then(function(data) { + return JSON.parse(data); + }); + } + ,getCookiesPathOrNull: function() { + var cookiesPath = "" + this.main.userDir + "/cookies.txt"; + if(sys_FileSystem.exists(cookiesPath)) { + return cookiesPath; + } else { + return null; + } + } + ,getBestYoutubeVideoFormat: function(formats,ignoreQualities) { var qPriority = [1080,720,480,360,240,144]; + if(ignoreQualities != null) { + var _g = 0; + while(_g < ignoreQualities.length) HxOverrides.remove(qPriority,ignoreQualities[_g++]); + } + var format60 = this.findFormat(formats,qPriority,true); + if(format60 != null) { + return format60; + } else { + return this.findFormat(formats,qPriority,false); + } + } + ,findFormat: function(formats,qPriority,is60fps) { var _g = 0; while(_g < qPriority.length) { - var quality = "" + qPriority[_g++] + "p"; - if(quality == ignoreQuality) { - continue; - } + var q = qPriority[_g]; + ++_g; + var quality = "" + q + "p" + (is60fps ? "60" : ""); var _g1 = 0; while(_g1 < formats.length) { var format = formats[_g1]; ++_g1; - if(format.videoCodec == null) { + var tmp = format.height; + if(tmp == null) { + continue; + } + if(tmp > q) { continue; } - if(format.qualityLabel == quality) { + if(this.formatVideoQuality(format) == quality) { return format; } } } return null; } - ,log: function(client,msg) { - this.cache.log(client,msg); + ,formatVideoQuality: function(format) { + var tmp = format.height; + if(tmp == null) { + return null; + } + var tmp1 = format.format_note; + if(tmp1 != null) { + return tmp1; + } else { + return "" + tmp + "p"; + } + } + ,log: function(clientName,msg) { + this.cache.logByName(clientName,msg); } - ,cancelProgress: function(client) { - this.main.send(client,{ type : "Progress", progress : { type : "Canceled", ratio : 0}}); + ,cancelProgress: function(clientName) { + this.main.sendByName(clientName,{ type : "Progress", progress : { type : "Canceled", ratio : 0}}); } ,__class__: server_cache_YoutubeCache }; @@ -6644,10 +6725,12 @@ server_HttpServer.mimeTypes = (function($this) { _g.h["jpeg"] = "image/jpeg"; _g.h["gif"] = "image/gif"; _g.h["webp"] = "image/webp"; + _g.h["avif"] = "image/avif"; _g.h["svg"] = "image/svg+xml"; _g.h["ico"] = "image/x-icon"; _g.h["wav"] = "audio/wav"; _g.h["mp3"] = "audio/mpeg"; + _g.h["ogg"] = "audio/ogg"; _g.h["mp4"] = "video/mp4"; _g.h["webm"] = "video/webm"; _g.h["woff"] = "application/font-woff"; diff --git a/package-lock.json b/package-lock.json index e229b84..1785183 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,16 +9,17 @@ "version": "1.0.0", "license": "MIT", "dependencies": { - "ws": "^8.17.1" + "ws": "^8.17.2" }, "engines": { "node": ">=14.17.0" } }, "node_modules/ws": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", - "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "version": "8.18.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz", + "integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==", + "license": "MIT", "engines": { "node": ">=10.0.0" }, @@ -38,9 +39,9 @@ }, "dependencies": { "ws": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", - "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "version": "8.18.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz", + "integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==", "requires": {} } } diff --git a/package.json b/package.json index 808284b..be78b5f 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ }, "homepage": "https://github.com/RblSb/SyncTube#readme", "dependencies": { - "ws": "^8.17.1" + "ws": "^8.17.2" }, "engines": { "node": ">=14.17.0" diff --git a/res/client.js b/res/client.js index ba579ba..2780e56 100644 --- a/res/client.js +++ b/res/client.js @@ -1,4 +1,4 @@ -// Generated by Haxe 4.3.6 +// Generated by Haxe 4.3.7 (function ($hx_exports, $global) { "use strict"; $hx_exports["client"] = $hx_exports["client"] || {}; $hx_exports["client"]["JsApi"] = $hx_exports["client"]["JsApi"] || {}; @@ -3383,8 +3383,10 @@ client_Player.prototype = { } } ,pause: function() { - if(!this.isSyncActive()) { - return; + if(this.videoList.items.length > 0) { + if(!this.isSyncActive()) { + return; + } } if(this.player == null) { return; @@ -3489,7 +3491,7 @@ client_Player.prototype = { } }; http.onError = function(msg) { - haxe_Log.trace(msg,{ fileName : "src/client/Player.hx", lineNumber : 674, className : "client.Player", methodName : "skipAd"}); + haxe_Log.trace(msg,{ fileName : "src/client/Player.hx", lineNumber : 677, className : "client.Player", methodName : "skipAd"}); }; http.request(); } diff --git a/src/client/Player.hx b/src/client/Player.hx index 7fc021d..670d5da 100644 --- a/src/client/Player.hx +++ b/src/client/Player.hx @@ -601,7 +601,10 @@ class Player { } public function pause():Void { - if (!isSyncActive()) return; + // allow pausing when removing last video + if (videoList.length > 0) { + if (!isSyncActive()) return; + } if (player == null) return; if (!player.isVideoLoaded()) return; player.pause(); diff --git a/src/server/HttpServer.hx b/src/server/HttpServer.hx index b15018b..4f283a4 100644 --- a/src/server/HttpServer.hx +++ b/src/server/HttpServer.hx @@ -41,10 +41,12 @@ class HttpServer { "jpeg" => "image/jpeg", "gif" => "image/gif", "webp" => "image/webp", + "avif" => "image/avif", "svg" => "image/svg+xml", "ico" => "image/x-icon", "wav" => "audio/wav", "mp3" => "audio/mpeg", + "ogg" => "audio/ogg", "mp4" => "video/mp4", "webm" => "video/webm", "woff" => "application/font-woff", @@ -371,7 +373,7 @@ class HttpServer { if (Utils.isOutOfRange(start, 0, videoSize - 1)) start = 0; var end = Std.parseInt(ranges[2]); if (end == null) end = start + CHUNK_SIZE; - if (Utils.isOutOfRange(end, start, videoSize - 1)) end = videoSize - 1; + if (Utils.isOutOfRange(end, start, videoSize - 1)) end = (videoSize - 1).limitMin(0); return { start: start, end: end @@ -379,7 +381,10 @@ class HttpServer { } function isMediaExtension(ext:String):Bool { - return ext == "mp4" || ext == "webm" || ext == "mp3" || ext == "wav"; + return switch ext { + case "mp4", "webm", "mp3", "ogg", "wav": true; + case _: false; + } } final matchLang = ~/^[A-z]+/; @@ -452,7 +457,7 @@ class HttpServer { } function getMimeType(ext:String):String { - return mimeTypes[ext] ?? return "application/octet-stream"; + return mimeTypes[ext] ?? "application/octet-stream"; } final ctrlCharacters = ~/[\u0000-\u001F\u007F-\u009F\u2000-\u200D\uFEFF]/g; diff --git a/src/server/Main.hx b/src/server/Main.hx index 8164922..3cde049 100644 --- a/src/server/Main.hx +++ b/src/server/Main.hx @@ -58,7 +58,9 @@ class Main { public final clients:Array<Client> = []; final freeIds:Array<Int> = []; + #if !display final wsEventParser = new JsonParser<WsEvent>(); + #end final consoleInput:ConsoleInput; final cache:Cache; final cacheDir:String; @@ -960,6 +962,7 @@ class Main { serverMessage(client, "Free space: " + (cache.getFreeSpace() / 1024).toFixed() + "KiB"); + serverMessage(client, "Memory usage: " + js.Node.process.memoryUsage()); send(client, { type: Dump, dump: { @@ -1006,6 +1009,11 @@ class Main { client.ws.send(jsonStringify(data), null); } + public function sendByName(clientName:String, data:WsEvent):Void { + final client = clients.getByName(clientName) ?? return; + client.ws.send(jsonStringify(data), null); + } + public function broadcast(data:WsEvent):Void { final json = jsonStringify(data); for (client in clients) diff --git a/src/server/cache/Cache.hx b/src/server/cache/Cache.hx index f71b465..56749d8 100644 --- a/src/server/cache/Cache.hx +++ b/src/server/cache/Cache.hx @@ -39,19 +39,37 @@ class Cache { cachedFiles.resize(0); for (name in names) cachedFiles.push(name); + removeUntrackedFiles(); + } + + function removeUntrackedFiles():Void { final names = FileSystem.readDirectory(cacheDir); for (name in names) { if (name.startsWith(".")) continue; if (FileSystem.isDirectory('$cacheDir/$name')) continue; if (cachedFiles.contains(name)) continue; - trace('Remove non-tracked cache $name'); + trace('Remove untracked cache $name'); remove(name); } } public function log(client:Client, msg:String):Void { + trace(msg); main.serverMessage(client, msg); + } + + public function logByName(clientName:String, msg:String):Void { trace(msg); + final client = main.clients.getByName(clientName) ?? return; + main.serverMessage(client, msg); + } + + public function logWithAdmins(client:Client, msg:String):Void { + log(client, msg); + final admins = main.clients.filter(client -> client.isAdmin); + for (admin in admins) { + if (client != admin) main.serverMessage(admin, msg); + } } public function cacheYoutubeVideo(client:Client, url:String, callback:(name:String) -> Void) { @@ -152,6 +170,14 @@ class Cache { return FileSystem.exists(getFilePath(name)); } + public function findFile(callback:(name:String) -> Bool):Null<String> { + final names = FileSystem.readDirectory(cacheDir); + for (name in names) { + if (callback(name)) return name; + } + return null; + } + public function getFreeSpace():Int { return storageLimit - getUsedSpace(); } diff --git a/src/server/cache/RawCache.hx b/src/server/cache/RawCache.hx index ed8679c..1fb251d 100644 --- a/src/server/cache/RawCache.hx +++ b/src/server/cache/RawCache.hx @@ -61,37 +61,39 @@ class RawCache { } function handleMp4(client:Client, url:String, outName:String, callback:(name:String) -> Void) { + final clientName = client.name; downloadFile(client, url, outName, (downloaded, total) -> { - main.send(client, { + main.sendByName(clientName, { type: Progress, progress: { type: Downloading, - ratio: (downloaded / total).clamp(0, 1) + ratio: (downloaded / total).clamp(0, 1).toFixed(4) } }); }, () -> { cache.add(outName); callback(outName); }, (err) -> { - log(client, 'Mp4 download failed: $err'); - cancelProgress(client); + log(clientName, 'Mp4 download failed: $err'); + cancelProgress(clientName); }); } function handleM3u8(client:Client, url:String, outName:String, callback:(name:String) -> Void):Void { + final clientName = client.name; final useProxy = true; downloadM3u8Playlist(client, url, useProxy, (playlist, totalSize, segments) -> { // only playlist file donwloaded if (useProxy) totalSize = playlist.length; if (!cache.removeOlderCache(totalSize + cache.freeSpaceBlock)) { - log(client, cache.notEnoughSpaceErrorText); - cancelProgress(client); + log(clientName, cache.notEnoughSpaceErrorText); + cancelProgress(clientName); return; } if (useProxy) { - main.send(client, { + main.sendByName(clientName, { type: Progress, progress: { type: Caching, @@ -126,7 +128,7 @@ class RawCache { downloaded++; final progress = downloaded / segments.length; - main.send(client, { + main.sendByName(clientName, { type: Progress, progress: { type: Downloading, @@ -154,8 +156,8 @@ class RawCache { (err) -> { activeDownloads--; downloaded++; - log(client, 'TS segment ${segment.i} download failed: $err'); - cancelProgress(client); + log(clientName, 'TS segment ${segment.i} download failed: $err'); + cancelProgress(clientName); cleanupFiles(segments.map(item -> item.name)); } ); @@ -165,8 +167,8 @@ class RawCache { // Start the initial batch of downloads downloadNextBatch(); }, (err) -> { - log(client, 'M3U8 processing failed: $err'); - cancelProgress(client); + log(clientName, 'M3U8 processing failed: $err'); + cancelProgress(clientName); }); } @@ -327,17 +329,12 @@ class RawCache { } function buildTsFiles(tempFiles:Array<String>, outName:String, client:Client, callback:String->Void) { + final clientName = client.name; final missingFiles = tempFiles.filter(f -> !FileSystem.exists('${cache.cacheDir}/$f')); if (missingFiles.length > 0) { - log(client, 'Concatenation failed: ${missingFiles.length} segments are missing'); - main.send(client, { - type: Progress, - progress: { - type: Canceled, - ratio: 1 - } - }); + log(clientName, 'Concatenation failed: ${missingFiles.length} segments are missing'); + cancelProgress(clientName); cleanupFiles(tempFiles); return; } @@ -378,14 +375,8 @@ class RawCache { final timeout = 5 * 60 * 1000; // 5 minutes final timeoutId = js.Node.setTimeout(() -> { process.kill(); - log(client, 'FFmpeg process timed out after ${timeout / 1000} seconds'); - main.send(client, { - type: Progress, - progress: { - type: Canceled, - ratio: 1 - } - }); + log(clientName, 'FFmpeg process timed out after ${timeout / 1000} seconds'); + cancelProgress(clientName); cleanupFiles(tempFiles.concat([concatFile])); }, timeout); @@ -394,14 +385,13 @@ class RawCache { if (code != 0) { final errorMsg = Buffer.concat(errorOutput).toString(); - log(client, 'FFmpeg concatenation failed with code $code'); - trace('FFmpeg error output: $errorMsg'); + cache.logWithAdmins(client, 'FFmpeg concatenation failed with code $code'); + final ffmpegErr = 'FFmpeg error output: $errorMsg'; + trace(ffmpegErr); // Log detailed error to admins final admins = main.clients.filter(client -> client.isAdmin); - for (admin in admins) { - log(admin, 'FFmpeg error: $errorMsg'); - } + for (admin in admins) main.serverMessage(admin, ffmpegErr); main.send(client, { type: Progress, @@ -417,7 +407,7 @@ class RawCache { cache.add(outName); callback(outName); } else { - log(client, 'FFmpeg process completed but output file is missing or empty'); + log(clientName, 'FFmpeg process completed but output file is missing or empty'); main.send(client, { type: Progress, progress: { @@ -435,7 +425,7 @@ class RawCache { // Handle process errors (like if FFmpeg isn't found) process.on("error", (err) -> { js.Node.clearTimeout(timeoutId); - log(client, 'Failed to start FFmpeg: $err'); + log(clientName, 'Failed to start FFmpeg: $err'); main.send(client, { type: Progress, progress: { @@ -453,12 +443,12 @@ class RawCache { } } - function log(client:Client, msg:String):Void { - cache.log(client, msg); + function log(clientName:String, msg:String):Void { + cache.logByName(clientName, msg); } - function cancelProgress(client:Client):Void { - main.send(client, { + function cancelProgress(clientName:String):Void { + main.sendByName(clientName, { type: Progress, progress: { type: Canceled, diff --git a/src/server/cache/YoutubeCache.hx b/src/server/cache/YoutubeCache.hx index c0a5c4c..af142e2 100644 --- a/src/server/cache/YoutubeCache.hx +++ b/src/server/cache/YoutubeCache.hx @@ -2,17 +2,17 @@ package server.cache; import haxe.Json; import js.lib.Promise; -import js.node.Buffer; import js.node.ChildProcess; -import js.node.Fs.Fs; -import js.node.stream.Readable; import sys.FileSystem; -import sys.io.File; import utils.YoutubeUtils; +import ytdlp_nodejs.VideoFormat; +import ytdlp_nodejs.VideoInfo; +import ytdlp_nodejs.YtDlp; class YoutubeCache { final main:Main; final cache:Cache; + var ytDlp:Null<YtDlp>; public function new(main:Main, cache:Cache):Void { this.main = main; @@ -20,35 +20,32 @@ class YoutubeCache { } public function checkYtDeps():Bool { - final ytdl = try { - untyped require("@distube/ytdl-core"); - } catch (e) { - return false; - } try { - ChildProcess.execSync("ffmpeg -version", {stdio: "ignore", timeout: 3000}); + ChildProcess.execSync("ffmpeg -version", {stdio: "ignore", timeout: 5000}); + ytDlp = js.Syntax.code("new (require('ytdlp-nodejs')).YtDlp()"); return true; } catch (e) { return false; } } - public function cleanYtInputFiles():Void { + public function cleanYtInputFiles(prefix = "__tmp"):Void { final names = FileSystem.readDirectory(cache.cacheDir); for (name in names) { - if (!name.startsWith("__tmp")) continue; + if (!name.startsWith(prefix)) continue; cache.remove(name); } } public function cacheYoutubeVideo(client:Client, url:String, callback:(name:String) -> Void) { if (!cache.isYtReady) { - trace("Do `npm i @distube/ytdl-core@latest` to use cache feature (you also need to install `ffmpeg` to build mp4 from downloaded audio/video tracks)."); + trace("Do `npm i https://github.com/RblSb/ytdlp-nodejs` to use cache feature (you also need to install `ffmpeg` to build mp4 from downloaded audio/video tracks)."); return; } + final clientName = client.name; final videoId = YoutubeUtils.extractVideoId(url); if (videoId == "") { - log(client, 'Error: youtube video id not found in url: $url'); + log(clientName, 'Error: youtube video id not found in url: $url'); return; } final outName = videoId + ".mp4"; @@ -57,28 +54,25 @@ class YoutubeCache { return; } final inVideoName = '__tmp-video-$videoId'; - final inAudioName = '__tmp-audio-$videoId'; inline function removeInputFiles():Void { - cache.remove(inVideoName); - cache.remove(inAudioName); + cleanYtInputFiles(inVideoName); } inline function checkEnoughSpace(contentLength:Int):Bool { final hasSpace = cache.removeOlderCache(contentLength + cache.freeSpaceBlock); if (!hasSpace) { removeInputFiles(); - cancelProgress(client); - log(client, cache.notEnoughSpaceErrorText); + cancelProgress(clientName); + log(clientName, cache.notEnoughSpaceErrorText); } return hasSpace; } if (cache.isFileExists(inVideoName)) { - log(client, 'Caching $outName already in progress'); + log(clientName, 'Caching $outName already in progress'); return; } - final ytdl:Dynamic = untyped require("@distube/ytdl-core"); trace('Caching $url to $outName...'); - main.send(client, { + main.sendByName(clientName, { type: Progress, progress: { type: Caching, @@ -86,146 +80,158 @@ class YoutubeCache { data: outName } }); - var agent:Any = null; - final cookiesPath = '${main.userDir}/cookies.json'; - if (FileSystem.exists(cookiesPath)) { - agent = ytdl.createAgent(Json.parse(File.getContent(cookiesPath))); - } - final promise:Promise<YouTubeVideoInfo> = ytdl.getInfo(url, { - agent: agent, - }); - promise.then(info -> { + + var useCookies = false; + + function onGetInfo(info:VideoInfo):Void { trace('Get info with ${info.formats.length} formats'); - final audioFormat:YoutubeVideoFormat = try { - ytdl.chooseFormat(info.formats.filter(item -> { - return item.audioCodec?.startsWith("mp4a"); - }), {quality: "highestaudio"}); - } catch (e) { - log(client, "Error: audio format not found"); - trace(e); - trace(info.formats.filter(item -> item.hasAudio)); + var aformats = info.formats.filter(format -> format.vcodec == "none"); + if (aformats.length == 0) { + aformats = info.formats.filter(format -> format.acodec != "none"); + } + aformats.sort((a, b) -> (a?.filesize ?? 0) < (b?.filesize ?? 0) ? 1 : -1); + final audioFormat:VideoFormat = aformats[0] ?? { + log(clientName, "Error: format with audio not found"); + for (format in info.formats) trace(format); return; } - var videoFormat = getBestYoutubeVideoFormat(info.formats) ?? { - log(client, "Error: video format not found"); - trace(info.formats.filter(item -> item.hasVideo)); + final vformats = info.formats.filter(format -> format.vcodec != "none"); + vformats.sort((a, b) -> (a?.filesize ?? 0) < (b?.filesize ?? 0) ? 1 : -1); + var videoFormat = getBestYoutubeVideoFormat(vformats) ?? { + log(clientName, "Error: video format not found"); + for (format in info.formats) trace(format); return; } inline function getTotalFormatsSize():Int { - final videoSize = Std.parseInt(videoFormat.contentLength) ?? 0; - final audioSize = Std.parseInt(audioFormat.contentLength) ?? 0; + final videoSize:Int = cast(videoFormat.filesize ?? 0); + final audioSize:Int = cast(audioFormat.filesize ?? 0); return videoSize + audioSize; } // check if we have space for formats and video build - final hasSpace = cache.removeOlderCache(getTotalFormatsSize() * 2 - + cache.freeSpaceBlock); - if (!hasSpace) { + final ignoreQualities:Array<Int> = []; + for (i in 0...3) { + final hasSpace = cache.removeOlderCache(getTotalFormatsSize() * 2 + + cache.freeSpaceBlock); + if (hasSpace) break; // try fallback to worse video quality - videoFormat = getBestYoutubeVideoFormat(info.formats, videoFormat.qualityLabel); - if (!checkEnoughSpace(getTotalFormatsSize() * 2)) return; + ignoreQualities.push(Std.int(videoFormat.height ?? 0)); + videoFormat = getBestYoutubeVideoFormat(vformats, ignoreQualities) ?? break; } + if (!checkEnoughSpace(getTotalFormatsSize() * 2)) return; - final dlVideo:Readable<Dynamic> = ytdl(url, { - format: videoFormat, - agent: agent, - }); - dlVideo.pipe(Fs.createWriteStream('${cache.cacheDir}/$inVideoName')); - dlVideo.on("error", err -> { - log(client, "Error during video download: " + err); + final formatIds = if (videoFormat.format_id == audioFormat.format_id) { + videoFormat.format_id; + } else { + '${videoFormat.format_id}+${audioFormat.format_id}'; + } + var totalSize = getTotalFormatsSize().limitMin(10); + var videoSizeRatio = (videoFormat.filesize ?? 0).limitMin(8) / totalSize; + var audioSizeRatio = (audioFormat.filesize ?? 0).limitMin(2) / totalSize; + var isVideoFormatDownloading = true; + final dlVideo:Promise<String> = ytDlp.downloadAsync(url, { + format: formatIds, + output: '${cache.cacheDir}/$inVideoName', + remuxVideo: "mp4", + cookies: useCookies ? getCookiesPathOrNull() : null, + onProgress: p -> { + final isFinished = p.status == "finished"; + var ratio = if (isFinished) { + 1; + } else { + (p.downloaded / p.total).clamp(0, 1); + } + if (isVideoFormatDownloading) { + ratio = ratio * videoSizeRatio; + } else { + ratio = videoSizeRatio + ratio * audioSizeRatio; + } + if (isFinished) isVideoFormatDownloading = false; + main.sendByName(clientName, { + type: Progress, + progress: { + type: Downloading, + ratio: ratio.toFixed(4) + } + }); + } + }).catchError(err -> { + final err = "Error during video download: " + err; + cache.logWithAdmins(client, err); removeInputFiles(); - cancelProgress(client); + cancelProgress(clientName); }); - final dlAudio:Readable<Dynamic> = ytdl(url, { - format: audioFormat, - agent: agent, - }); - dlAudio.pipe(Fs.createWriteStream('${cache.cacheDir}/$inAudioName')); - dlAudio.on("error", err -> { - log(client, "Error during audio download: " + err); + dlVideo.then((v) -> { + final name = cache.findFile(n -> n.startsWith(inVideoName) && n.endsWith(".mp4")) ?? { + final err = 'Error: cannot find downloaded file with prefix $inVideoName'; + cache.logWithAdmins(client, err); + return; + }; + FileSystem.rename('${cache.cacheDir}/$name', '${cache.cacheDir}/$outName'); removeInputFiles(); - cancelProgress(client); + cache.add(outName); + callback(outName); }); + } - var count = 0; - function onComplete(type:String):Void { - count++; - trace('$type track downloaded ($count/2)'); - if (count < 2) return; - if (!cache.isFileExists(inVideoName) || !cache.isFileExists(inAudioName)) { - log(client, "Input files not found for making final video"); - removeInputFiles(); - cancelProgress(client); - return; - } - var size = FileSystem.stat('${cache.cacheDir}/$inVideoName').size; - size += FileSystem.stat('${cache.cacheDir}/$inAudioName').size; - // clean some space for full mp4 - if (!checkEnoughSpace(size)) return; - - final args = '-y -i ./$inVideoName -i ./$inAudioName -c copy -map 0:v -map 1:a ./$outName'.split(" "); - final process = ChildProcess.spawn("ffmpeg", args, { - cwd: cache.cacheDir, - // stdio: "ignore" - }); - final outputData:Array<Buffer> = []; - process.stderr.on("data", (data) -> outputData.push(data)); - process.on("close", (code:Int) -> { - removeInputFiles(); - if (code != 0) { - cancelProgress(client); - final errCodeMsg = 'Error: ffmpeg closed with code $code'; - final admins = main.clients.filter(client -> client.isAdmin); - for (client in admins) { - log(client, Buffer.concat(outputData).toString()); - log(client, errCodeMsg); - } - if (!admins.contains(client)) log(client, errCodeMsg); - return; - } - cache.add(outName); - - callback(outName); - }); - } - dlVideo.on("finish", () -> onComplete("Video")); - dlAudio.on("finish", () -> onComplete("Audio")); - dlVideo.on("progress", (chunkLength:Int, downloaded:Int, contentLength:Int) -> { - final ratio = (downloaded / contentLength).clamp(0, 1); - main.send(client, { - type: Progress, - progress: { - type: Downloading, - ratio: ratio - } - }); + getInfoAsync(url, useCookies).then(onGetInfo).catchError(err -> { + trace(err); + useCookies = true; + getInfoAsync(url, useCookies).then(onGetInfo).catchError(err -> { + removeInputFiles(); + cancelProgress(clientName); + log(clientName, "" + err); }); - }).catchError(err -> { - removeInputFiles(); - cancelProgress(client); - log(client, "" + err); }); } - function getBestYoutubeVideoFormat(formats:Array<YoutubeVideoFormat>, ?ignoreQuality:String):Null<YoutubeVideoFormat> { + function getInfoAsync(url:String, useCookies = false):Promise<VideoInfo> { + return ytDlp.execAsync(url, { + dumpSingleJson: true, + quiet: true, + cookies: useCookies ? getCookiesPathOrNull() : null, + }).then(data -> Json.parse(data)); + } + + function getCookiesPathOrNull():Null<String> { + final cookiesPath = '${main.userDir}/cookies.txt'; + return FileSystem.exists(cookiesPath) ? cookiesPath : null; + } + + function getBestYoutubeVideoFormat(formats:Array<VideoFormat>, ?ignoreQualities:Array<Int>):Null<VideoFormat> { final qPriority = [1080, 720, 480, 360, 240, 144]; + if (ignoreQualities != null) { + for (q in ignoreQualities) qPriority.remove(q); + } + final format60 = findFormat(formats, qPriority, true); + return format60 ?? findFormat(formats, qPriority, false); + } + + function findFormat(formats:Array<VideoFormat>, qPriority:Array<Int>, is60fps:Bool):Null<VideoFormat> { for (q in qPriority) { - final quality = '${q}p'; - if (quality == ignoreQuality) continue; + final quality = '${q}p' + (is60fps ? "60" : ""); for (format in formats) { - if (format.videoCodec == null) continue; - if (format.qualityLabel == quality) return format; + final height = format.height ?? continue; + if (height > q) continue; + final format_note = formatVideoQuality(format); + if (format_note == quality) return format; } } return null; } - function log(client:Client, msg:String):Void { - cache.log(client, msg); + function formatVideoQuality(format:VideoFormat):Null<String> { + final height = format.height ?? return null; + // when there is 720p and 720p60 formats + return format.format_note ?? '${height}p'; + } + + function log(clientName:String, msg:String):Void { + cache.logByName(clientName, msg); } - function cancelProgress(client:Client):Void { - main.send(client, { + function cancelProgress(clientName:String):Void { + main.sendByName(clientName, { type: Progress, progress: { type: Canceled, diff --git a/src/utils/YoutubeUtils.hx b/src/utils/YoutubeUtils.hx index b7cd739..95d3031 100644 --- a/src/utils/YoutubeUtils.hx +++ b/src/utils/YoutubeUtils.hx @@ -1,65 +1,5 @@ package utils; -typedef YoutubeVideoDetails = { - viewCount:String, - videoId:String, - title:String, - thumbnail:{ - thumbnails:Array<{ - url:String, - width:Int, - height:Int, - }> - }, - shortDescription:String, - lengthSeconds:String, - keywords:Array<String>, - isUnpluggedCorpus:Bool, - isPrivate:Bool, - isOwnerViewing:Bool, - isLiveContent:Bool, - isCrawlable:Bool, - channelId:String, - author:String, - allowRatings:Bool -} - -typedef YoutubeVideoFormat = { - ?signatureCipher:String, - itag:Int, - width:Int, - height:Int, - url:String, - qualityLabel:String, // 240p, 1080p, etc - quality:String, - projectionType:String, - mimeType:String, - lastModified:String, - bitrate:Int, - approxDurationMs:String, - ?initRange:{start:Int, end:Int}, - ?indexRange:{start:Int, end:Int}, - ?audioQuality:String, // AUDIO_QUALITY_LOW - ?audioSampleRate:Int, - ?audioChannels:Int, - - ?container:String, - ?videoCodec:String, - ?audioCodec:String, - ?hasVideo:Bool, - ?hasAudio:Bool, - ?contentLength:String, -} - -typedef YouTubeVideoInfo = { - public var videoDetails:YoutubeVideoDetails; - public var ?formats:Array<YoutubeVideoFormat>; - public var ?adaptiveFormats:Array<YoutubeVideoFormat>; - public var ?liveData:{ - manifestUrl:String, - }; -} - class YoutubeUtils { static final matchId = ~/youtube\.com.*v=([A-z0-9_-]+)/; static final matchShort = ~/youtu\.be\/([A-z0-9_-]+)/; @@ -1,6 +1,7 @@ --library hxnodejs --library hxnodejs-ws --library json2object:git:https://github.com/RblSb/json2object.git#nightly_safe_macros +--library ytdlp-nodejs:git:https://github.com/RblSb/ytdlp-nodejs-externs.git # Client libs for completion --library youtubeIFramePlayer:git:https://github.com/okawa-h/youtubeIFramePlayer-externs.git --library hls.js-extern:git:https://github.com/zoldesi-andor/hls.js-haxe-extern.git |
