aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore2
-rw-r--r--README.md2
-rw-r--r--build-server.hxml1
-rw-r--r--build/server.js539
-rw-r--r--package-lock.json15
-rw-r--r--package.json2
-rw-r--r--res/client.js10
-rw-r--r--src/client/Player.hx5
-rw-r--r--src/server/HttpServer.hx11
-rw-r--r--src/server/Main.hx8
-rw-r--r--src/server/cache/Cache.hx28
-rw-r--r--src/server/cache/RawCache.hx68
-rw-r--r--src/server/cache/YoutubeCache.hx266
-rw-r--r--src/utils/YoutubeUtils.hx60
-rw-r--r--tests.hxml1
15 files changed, 542 insertions, 476 deletions
diff --git a/.gitignore b/.gitignore
index c1a7f7a..c9d959b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,5 +6,5 @@
/user/state.json
/user/config.json
/user/users.json
-/user/cookies.json
+/user/cookies.txt
/user/res/
diff --git a/README.md b/README.md
index 248b194..600b61d 100644
--- a/README.md
+++ b/README.md
@@ -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_-]+)/;
diff --git a/tests.hxml b/tests.hxml
index f28775d..47bf393 100644
--- a/tests.hxml
+++ b/tests.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
send patches to the email below
yukais@pinapelz.com
include the subject [PATCH repo_name]
pinapelz.com
homepage