aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorRblSb <msrblsb@gmail.com>2020-02-24 07:58:56 +0300
committerRblSb <msrblsb@gmail.com>2020-02-24 07:58:56 +0300
commit9c6cd2c2310d2e3ce3d1a6e3350a97e7ba0ca657 (patch)
tree3eb12f0eee7ba05b7e70c740561eff31e06b608a
parentc561fb9e2e42e4968f2b48cd535f208e90f8c12c (diff)
Start working on user folder
-rw-r--r--.gitignore6
-rw-r--r--README.md10
-rw-r--r--build-client.hxml2
-rw-r--r--build/server.js170
-rw-r--r--res/client.js (renamed from build/client.js)29
-rw-r--r--res/css/custom.css4
-rw-r--r--src/Client.hx11
-rw-r--r--src/Types.hx3
-rw-r--r--src/client/Main.hx39
-rw-r--r--src/client/Player.hx1
-rw-r--r--src/server/HttpServer.hx79
-rw-r--r--src/server/Main.hx66
-rw-r--r--src/server/Utils.hx11
-rw-r--r--user/README.md18
14 files changed, 332 insertions, 117 deletions
diff --git a/.gitignore b/.gitignore
index 5037965..807010e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,7 @@
/node_modules
/res/temp
-/config.json
+/user/config.json
+/user/state.json
+/user/res/
+/user/logs/
+/user/errors/
diff --git a/README.md b/README.md
index bbc3986..1138e8a 100644
--- a/README.md
+++ b/README.md
@@ -1,16 +1,15 @@
## SyncTube
Synchronized video viewing with chat and other features.
-Based on CyTube, but with lightweight implementation and very easy way to run locally.
+Based on CyTube layout, but with lightweight implementation and very easy way to run locally.
### New features
Even if some original features are not implemented yet, there is some new things:
- Multi-Language
- Mobile view with page fullscreen
-- Updated Des theme
-
-TODO:
- Way to play local videos for network users (without NAT loopback feature)
- `/30`, `/-21`, etc to rewind video playback in seconds
+- Override every front-end file you want (`user/res` folder)
+- Updated Des theme
### Setup
- Open `4200` and `4201` ports in your router settings
@@ -18,6 +17,9 @@ TODO:
- Run `node build/server.js`
- Open showed "Local" link for yourself and send "Global" link to friends
+### Configuration
+It's just works, but you can also check [user/ folder](/user/README.md) for server settings and additional customization.
+
### Development
- Install Haxe 4, VSCode and vshaxe extension.
- `haxelib install all` to install extern libs.
diff --git a/build-client.hxml b/build-client.hxml
index d8a77e3..f4eca40 100644
--- a/build-client.hxml
+++ b/build-client.hxml
@@ -2,4 +2,4 @@
--main client.Main
-D analyzer-optimize
--dce full
---js build/client.js
+--js res/client.js
diff --git a/build/server.js b/build/server.js
index 68ad69d..ce5bb7b 100644
--- a/build/server.js
+++ b/build/server.js
@@ -12,8 +12,9 @@ var ClientGroup = $hxEnums["ClientGroup"] = { __ename__ : true, __constructs__ :
,Leader: {_hx_index:1,__enum__:"ClientGroup",toString:$estr}
,Admin: {_hx_index:2,__enum__:"ClientGroup",toString:$estr}
};
-var Client = function(ws,id,name,group) {
+var Client = function(ws,req,id,name,group) {
this.ws = ws;
+ this.req = req;
this.id = id;
this.name = name;
var i = group;
@@ -308,6 +309,9 @@ StringTools.startsWith = function(s,start) {
return false;
}
};
+StringTools.replace = function(s,sub,by) {
+ return s.split(sub).join(by);
+};
var haxe_Log = function() { };
haxe_Log.__name__ = true;
haxe_Log.formatOutput = function(v,infos) {
@@ -514,36 +518,49 @@ var js_node_Path = require("path");
var js_npm_ws_Server = require("ws").Server;
var server_HttpServer = function() { };
server_HttpServer.__name__ = true;
-server_HttpServer.init = function(directory) {
- server_HttpServer.dir = directory;
+server_HttpServer.init = function(dir,customDir) {
+ server_HttpServer.dir = dir;
+ if(customDir == null) {
+ return;
+ }
+ server_HttpServer.customDir = customDir;
+ server_HttpServer.hasCustomRes = sys_FileSystem.exists(customDir);
};
server_HttpServer.serveFiles = function(req,res) {
- var filePath = server_HttpServer.dir + req.url;
- if(req.url == "/") {
- filePath = "" + server_HttpServer.dir + "/index.html";
+ var url = req.url;
+ if(url == "/") {
+ url = "/index.html";
}
+ var filePath = server_HttpServer.dir + url;
var extension = haxe_io_Path.extension(filePath).toLowerCase();
var contentType = server_HttpServer.getMimeType(extension);
+ var tmp;
+ if(req.connection.remoteAddress != req.connection.localAddress) {
+ var _this = server_HttpServer.allowedLocalFiles;
+ tmp = __map_reserved[url] != null ? _this.getReserved(url) : _this.h[url];
+ } else {
+ tmp = true;
+ }
+ if(tmp) {
+ if(server_HttpServer.serveLocalFile(res,url,extension,contentType)) {
+ return;
+ }
+ }
if(!server_HttpServer.isChildOf(server_HttpServer.dir,filePath)) {
res.statusCode = 500;
- var tmp = "Error getting the file: No access to " + js_node_Path.relative(server_HttpServer.dir,filePath) + ".";
- res.end(tmp);
+ var tmp1 = "Error getting the file: No access to " + js_node_Path.relative(server_HttpServer.dir,filePath) + ".";
+ res.end(tmp1);
return;
}
- if(filePath == "" + server_HttpServer.dir + "/client.js") {
- filePath = "" + __dirname + "/client.js";
+ if(server_HttpServer.hasCustomRes) {
+ var path = server_HttpServer.customDir + url;
+ if(js_node_Fs.existsSync(path)) {
+ filePath = path;
+ }
}
js_node_Fs.readFile(filePath,function(err,data) {
if(err != null) {
- if(err.code == "ENOENT") {
- res.statusCode = 404;
- var tmp1 = "File " + js_node_Path.relative(server_HttpServer.dir,filePath) + " not found.";
- res.end(tmp1);
- } else {
- res.statusCode = 500;
- var tmp2 = "Error getting the file: " + Std.string(err) + ".";
- res.end(tmp2);
- }
+ server_HttpServer.readFileError(err,res,filePath);
return;
}
res.setHeader("Content-Type",contentType);
@@ -551,24 +568,58 @@ server_HttpServer.serveFiles = function(req,res) {
data = server_HttpServer.localizeHtml(data.toString(),req.headers["accept-language"]);
}
res.end(data);
+ return;
});
};
+server_HttpServer.readFileError = function(err,res,filePath) {
+ if(err.code == "ENOENT") {
+ res.statusCode = 404;
+ res.end("File " + js_node_Path.relative(server_HttpServer.dir,filePath) + " not found.");
+ } else {
+ res.statusCode = 500;
+ res.end("Error getting the file: " + Std.string(err) + ".");
+ }
+};
+server_HttpServer.serveLocalFile = function(res,filePath,ext,contentType) {
+ if(ext != "mp4" && ext != "mp3" && ext != "wav") {
+ return false;
+ }
+ if(!js_node_Fs.existsSync(filePath)) {
+ return false;
+ }
+ var _this = server_HttpServer.allowedLocalFiles;
+ if(__map_reserved[filePath] != null) {
+ _this.setReserved(filePath,true);
+ } else {
+ _this.h[filePath] = true;
+ }
+ js_node_Fs.readFile(filePath,function(err,data) {
+ if(err != null) {
+ server_HttpServer.readFileError(err,res,filePath);
+ return;
+ }
+ res.setHeader("Content-Type",contentType);
+ res.end(data);
+ return;
+ });
+ return true;
+};
server_HttpServer.localizeHtml = function(data,lang) {
if(lang != null && server_HttpServer.matchLang.match(lang)) {
lang = server_HttpServer.matchLang.matched(0);
} else {
lang = "en";
}
- data = new EReg("\\${([A-z_]+)}","g").map(data,function(regExp) {
+ data = server_HttpServer.matchVarString.map(data,function(regExp) {
var key = regExp.matched(1);
return Lang.get(lang,key);
});
return data;
};
server_HttpServer.isChildOf = function(parent,child) {
- var relative = js_node_Path.relative(parent,child);
- if(relative.length > 0 && !StringTools.startsWith(relative,"..")) {
- return !js_node_Path.isAbsolute(relative);
+ var rel = js_node_Path.relative(parent,child);
+ if(rel.length > 0 && !StringTools.startsWith(rel,"..")) {
+ return !js_node_Path.isAbsolute(rel);
} else {
return false;
}
@@ -577,7 +628,7 @@ server_HttpServer.getMimeType = function(ext) {
var _this = server_HttpServer.mimeTypes;
var contentType = __map_reserved[ext] != null ? _this.getReserved(ext) : _this.h[ext];
if(contentType == null) {
- contentType = "application/octet-stream";
+ return "application/octet-stream";
}
return contentType;
};
@@ -595,6 +646,7 @@ var server_Main = function(port,wsPort) {
this.freeIds = [];
this.clients = [];
this.rootDir = "" + __dirname + "/..";
+ var _gthis = this;
this.config = this.getUserConfig();
this.wss = new js_npm_ws_Server({ port : wsPort});
this.wss.on("connection",$bind(this,this.onConnect));
@@ -603,21 +655,27 @@ var server_Main = function(port,wsPort) {
};
process.on("exit",exit);
process.on("SIGINT",exit);
+ process.on("SIGUSR1",exit);
+ process.on("SIGUSR2",exit);
process.on("uncaughtException",function(log) {
- haxe_Log.trace(log,{ fileName : "src/server/Main.hx", lineNumber : 41, className : "server.Main", methodName : "new"});
+ haxe_Log.trace(log,{ fileName : "src/server/Main.hx", lineNumber : 49, className : "server.Main", methodName : "new"});
return;
});
process.on("unhandledRejection",function(reason,promise) {
- haxe_Log.trace("Unhandled Rejection at:",{ fileName : "src/server/Main.hx", lineNumber : 44, className : "server.Main", methodName : "new", customParams : [reason]});
+ haxe_Log.trace("Unhandled Rejection at:",{ fileName : "src/server/Main.hx", lineNumber : 52, className : "server.Main", methodName : "new", customParams : [reason]});
return;
});
+ this.localIp = server_Utils.getLocalIp();
+ this.globalIp = this.localIp;
+ this.port = port;
server_Utils.getGlobalIp(function(ip) {
- haxe_Log.trace("Local: http://" + server_Utils.getLocalIp() + ":" + port,{ fileName : "src/server/Main.hx", lineNumber : 49, className : "server.Main", methodName : "new"});
- haxe_Log.trace("Global: http://" + ip + ":" + port,{ fileName : "src/server/Main.hx", lineNumber : 50, className : "server.Main", methodName : "new"});
+ _gthis.globalIp = ip;
+ haxe_Log.trace("Local: http://" + _gthis.localIp + ":" + port,{ fileName : "src/server/Main.hx", lineNumber : 60, className : "server.Main", methodName : "new"});
+ haxe_Log.trace("Global: http://" + _gthis.globalIp + ":" + port,{ fileName : "src/server/Main.hx", lineNumber : 61, className : "server.Main", methodName : "new"});
return;
});
var dir = "" + this.rootDir + "/res";
- server_HttpServer.init(dir);
+ server_HttpServer.init(dir,"" + this.rootDir + "/user/res");
Lang.init("" + dir + "/langs");
js_node_Http.createServer(function(req,res) {
server_HttpServer.serveFiles(req,res);
@@ -631,7 +689,7 @@ server_Main.main = function() {
server_Main.prototype = {
getUserConfig: function() {
var config = JSON.parse(js_node_Fs.readFileSync("" + this.rootDir + "/default-config.json",{ encoding : "utf8"}));
- var customPath = "" + this.rootDir + "/config.json";
+ var customPath = "" + this.rootDir + "/user/config.json";
if(!sys_FileSystem.exists(customPath)) {
return config;
}
@@ -642,7 +700,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 : 68, className : "server.Main", methodName : "getUserConfig"});
+ haxe_Log.trace("Warning: config field \"" + field + "\" is unknown",{ fileName : "src/server/Main.hx", lineNumber : 79, className : "server.Main", methodName : "getUserConfig"});
}
config[field] = Reflect.field(customConfig,field);
}
@@ -653,8 +711,8 @@ server_Main.prototype = {
var ip = req.connection.remoteAddress;
var id = this.freeIds.length > 0 ? this.freeIds.shift() : this.clients.length;
var name = "Guest " + (id + 1);
- haxe_Log.trace("" + name + " connected (" + ip + ")",{ fileName : "src/server/Main.hx", lineNumber : 78, className : "server.Main", methodName : "onConnect"});
- var client = new Client(ws,id,name,0);
+ haxe_Log.trace("" + name + " connected (" + ip + ")",{ fileName : "src/server/Main.hx", lineNumber : 89, className : "server.Main", methodName : "onConnect"});
+ var client = new Client(ws,req,id,name,0);
if(req.connection.localAddress == ip) {
client.group |= 4;
}
@@ -671,7 +729,7 @@ server_Main.prototype = {
var _g1 = 0;
var _g2 = this.clients;
while(_g1 < _g2.length) _g.push(_g2[_g1++].getData());
- this.send(client,{ type : "Connected", connected : { config : tmp, history : tmp1, isUnknownClient : true, clientName : client1, clients : _g, videoList : this.videoList}});
+ this.send(client,{ type : "Connected", connected : { config : tmp, history : tmp1, isUnknownClient : true, clientName : client1, clients : _g, videoList : this.videoList, globalIp : this.globalIp}});
this.sendClientList();
ws.on("message",function(data) {
var tmp2 = JSON.parse(data);
@@ -679,8 +737,8 @@ server_Main.prototype = {
return;
});
ws.on("close",function(err) {
- haxe_Log.trace("Client " + client.name + " disconnected",{ fileName : "src/server/Main.hx", lineNumber : 105, className : "server.Main", methodName : "onConnect"});
- _gthis.sortedPush(_gthis.freeIds,client.id);
+ haxe_Log.trace("Client " + client.name + " disconnected",{ fileName : "src/server/Main.hx", lineNumber : 117, className : "server.Main", methodName : "onConnect"});
+ server_Utils.sortedPush(_gthis.freeIds,client.id);
HxOverrides.remove(_gthis.clients,client);
_gthis.sendClientList();
if((client.group & 2) != 0) {
@@ -697,25 +755,18 @@ server_Main.prototype = {
return;
});
}
- ,sortedPush: function(ids,id) {
- var _g = 0;
- var _g1 = ids.length;
- while(_g < _g1) {
- var i = _g++;
- if(id < ids[i]) {
- ids.splice(i,0,id);
- return;
- }
- }
- ids.push(id);
- }
,onMessage: function(client,data) {
switch(data.type) {
case "AddVideo":
+ var item = data.addVideo.item;
+ var localOrigin = "" + this.localIp + ":" + this.port;
+ if(item.url.indexOf(localOrigin) != -1) {
+ item.url = StringTools.replace(item.url,localOrigin,"" + this.globalIp + ":" + this.port);
+ }
if(data.addVideo.atEnd) {
- this.videoList.push(data.addVideo.item);
+ this.videoList.push(item);
} else {
- this.videoList.splice(1,0,data.addVideo.item);
+ this.videoList.splice(1,0,item);
}
this.broadcast(data);
if(this.videoList.length == 1) {
@@ -808,8 +859,8 @@ server_Main.prototype = {
if(this.videoList[0].url == url) {
this.videoTimer.stop();
}
- HxOverrides.remove(this.videoList,Lambda.find(this.videoList,function(item) {
- return item.url == url;
+ HxOverrides.remove(this.videoList,Lambda.find(this.videoList,function(item1) {
+ return item1.url == url;
}));
this.broadcast(data);
if(this.videoList.length > 0) {
@@ -954,6 +1005,18 @@ server_Utils.getLocalIp = function() {
}
return "127.0.0.1";
};
+server_Utils.sortedPush = function(ids,id) {
+ var _g = 0;
+ var _g1 = ids.length;
+ while(_g < _g1) {
+ var i = _g++;
+ if(id < ids[i]) {
+ ids.splice(i,0,id);
+ return;
+ }
+ }
+ ids.push(id);
+};
server_Utils.shuffle = function(arr) {
var _g = 0;
var _g1 = arr.length;
@@ -1127,6 +1190,9 @@ server_HttpServer.mimeTypes = (function($this) {
$r = _g;
return $r;
}(this));
+server_HttpServer.hasCustomRes = false;
+server_HttpServer.allowedLocalFiles = new haxe_ds_StringMap();
server_HttpServer.matchLang = new EReg("^[A-z]+","");
+server_HttpServer.matchVarString = new EReg("\\${([A-z_]+)}","g");
server_Main.main();
})(typeof window != "undefined" ? window : typeof global != "undefined" ? global : typeof self != "undefined" ? self : this);
diff --git a/build/client.js b/res/client.js
index 4b9954b..d9cc67b 100644
--- a/build/client.js
+++ b/res/client.js
@@ -12,7 +12,7 @@ var ClientGroup = $hxEnums["ClientGroup"] = { __ename__ : true, __constructs__ :
,Leader: {_hx_index:1,__enum__:"ClientGroup",toString:$estr}
,Admin: {_hx_index:2,__enum__:"ClientGroup",toString:$estr}
};
-var Client = function(ws,id,name,group) {
+var Client = function(name,group) {
this.name = name;
var i = group;
if(group == null) {
@@ -22,7 +22,7 @@ var Client = function(ws,id,name,group) {
};
Client.__name__ = true;
Client.fromData = function(data) {
- return new Client(null,null,data.name,data.group);
+ return new Client(data.name,data.group);
};
Client.prototype = {
setGroupFlag: function(type,flag) {
@@ -465,8 +465,9 @@ var client_Main = function(host,port) {
this.matchNumbers = new EReg("^-?[0-9]+$","");
this.onTimeGet = new haxe_Timer(2000);
this.isConnected = false;
- this.personal = new Client(null,null,"Unknown",0);
+ this.personal = new Client("Unknown",0);
this.filters = [];
+ this.globalIp = "";
this.pageTitle = window.document.title;
this.clients = [];
var _gthis = this;
@@ -477,8 +478,12 @@ var client_Main = function(host,port) {
if(host == "") {
host = "localhost";
}
+ this.host = host;
this.initListeners();
this.onTimeGet.run = function() {
+ if(_gthis.player.isListEmpty()) {
+ return;
+ }
_gthis.send({ type : "GetTime"});
return;
};
@@ -570,8 +575,12 @@ client_Main.prototype = {
}
,addVideo: function(url,atEnd,callback) {
var _gthis = this;
+ var protocol = window.location.protocol;
+ if(StringTools.startsWith(url,"/")) {
+ url = "" + protocol + "//" + window.location.hostname + ":" + window.location.port + url;
+ }
if(!StringTools.startsWith(url,"http")) {
- url = "" + window.location.protocol + "//" + url;
+ url = "" + protocol + "//" + url;
}
var pos = url.lastIndexOf("/") + 1;
var name = HxOverrides.substr(url,pos,null);
@@ -597,6 +606,12 @@ client_Main.prototype = {
while(_g1 < items.length) _g.push(items[_g1++].url);
return _g;
}
+ ,replaceLocalIp: function(url) {
+ if(this.host == this.globalIp) {
+ return url;
+ }
+ return StringTools.replace(url,this.globalIp,this.host);
+ }
,getRemoteVideoDuration: function(src,callback) {
var player = window.document.querySelector("#ytapiplayer");
var video = window.document.createElement("video");
@@ -618,7 +633,7 @@ client_Main.prototype = {
var data = JSON.parse(e.data);
var t = data.type;
var t1 = t.charAt(0).toLowerCase() + HxOverrides.substr(t,1,null);
- haxe_Log.trace("Event: " + data.type,{ fileName : "src/client/Main.hx", lineNumber : 171, className : "client.Main", methodName : "onMessage", customParams : [data[t1]]});
+ haxe_Log.trace("Event: " + data.type,{ fileName : "src/client/Main.hx", lineNumber : 188, className : "client.Main", methodName : "onMessage", customParams : [data[t1]]});
switch(data.type) {
case "AddVideo":
if(this.player.isListEmpty()) {
@@ -668,7 +683,7 @@ client_Main.prototype = {
break;
case "Logout":
this.updateClients(data.logout.clients);
- this.personal = new Client(null,null,data.logout.clientName,0);
+ this.personal = new Client(data.logout.clientName,0);
this.showGuestLoginPanel();
break;
case "Message":
@@ -730,6 +745,7 @@ client_Main.prototype = {
}
,onConnected: function(data) {
var connected = data.connected;
+ this.globalIp = connected.globalIp;
this.setConfig(connected.config);
if(connected.isUnknownClient) {
this.updateClients(connected.clients);
@@ -981,6 +997,7 @@ client_Player.prototype = {
this.isLoaded = false;
this.video = window.document.createElement("video");
this.video.id = "videoplayer";
+ item.url = this.main.replaceLocalIp(item.url);
this.video.src = item.url;
this.video.controls = true;
this.video.oncanplaythrough = function(e) {
diff --git a/res/css/custom.css b/res/css/custom.css
index e69de29..05b94fa 100644
--- a/res/css/custom.css
+++ b/res/css/custom.css
@@ -0,0 +1,4 @@
+/*
+ Create user/res/css/custom.css
+ in project folder to override this file.
+*/
diff --git a/src/Client.hx b/src/Client.hx
index 7aa14c5..4604ab7 100644
--- a/src/Client.hx
+++ b/src/Client.hx
@@ -1,9 +1,8 @@
package;
#if nodejs
+import js.node.http.IncomingMessage;
import js.npm.ws.WebSocket;
-#elseif js
-import js.html.WebSocket;
#end
import haxe.EnumFlags;
@@ -23,15 +22,21 @@ class Client {
#if nodejs
public final ws:WebSocket;
public final id:Int;
+ public final req:IncomingMessage;
#end
public var name:String;
public var group:EnumFlags<ClientGroup>;
public var isLeader(get, set):Bool;
public var isAdmin(get, set):Bool;
- public function new(?ws:WebSocket, ?id:Int, name:String, group:Int) {
+ #if nodejs
+ public function new(?ws:WebSocket, ?req:IncomingMessage, ?id:Int, name:String, group:Int) {
+ #else
+ public function new(name:String, group:Int) {
+ #end
#if nodejs
this.ws = ws;
+ this.req = req;
this.id = id;
#end
this.name = name;
diff --git a/src/Types.hx b/src/Types.hx
index f2b506f..5706111 100644
--- a/src/Types.hx
+++ b/src/Types.hx
@@ -46,7 +46,8 @@ typedef WsEvent = {
clients:Array<ClientData>,
isUnknownClient:Bool,
clientName:String,
- videoList:Array<VideoItem>
+ videoList:Array<VideoItem>,
+ globalIp:String
},
?login:{
clientName:String,
diff --git a/src/client/Main.hx b/src/client/Main.hx
index 96353f8..432a095 100644
--- a/src/client/Main.hx
+++ b/src/client/Main.hx
@@ -20,6 +20,8 @@ class Main {
final clients:Array<Client> = [];
var pageTitle = document.title;
+ final host:String;
+ var globalIp = "";
var config:Null<Config>;
final filters:Array<{regex:EReg, replace:String}> = [];
var personal = new Client("Unknown", 0);
@@ -35,9 +37,13 @@ class Main {
player = new Player(this);
if (host == null) host = Browser.location.hostname;
if (host == "") host = "localhost";
+ this.host = host;
initListeners();
- onTimeGet.run = () -> send({type: GetTime});
+ onTimeGet.run = () -> {
+ if (player.isListEmpty()) return;
+ send({type: GetTime});
+ }
document.onvisibilitychange = () -> {
if (!document.hidden && onBlinkTab != null) {
document.title = getPageTitle();
@@ -117,7 +123,13 @@ class Main {
}
function addVideo(url:String, atEnd:Bool, callback:()->Void):Void {
- if (!url.startsWith("http")) url = '${Browser.location.protocol}//$url';
+ final protocol = Browser.location.protocol;
+ if (url.startsWith("/")) {
+ final host = Browser.location.hostname;
+ final port = Browser.location.port;
+ url = '$protocol//$host:$port$url';
+ }
+ if (!url.startsWith("http")) url = '$protocol//$url';
var name = url.substr(url.lastIndexOf('/') + 1);
final matchName = ~/^(.+)\./;
if (matchName.match(name)) name = matchName.matched(1);
@@ -149,6 +161,11 @@ class Main {
];
}
+ public function tryLocalIp(url:String):String {
+ if (host == globalIp) return url;
+ return url.replace(globalIp, host);
+ }
+
function getRemoteVideoDuration(src:String, callback:(duration:Float)->Void):Void {
final player:Element = ge("#ytapiplayer");
final video = document.createVideoElement();
@@ -173,38 +190,49 @@ class Main {
case Connected:
onConnected(data);
onTimeGet.run();
+
case Login:
onLogin(data.login.clients, data.login.clientName);
+
case LoginError:
final text = Lang.get("usernameError")
.replace("$MAX", '${config.maxLoginLength}');
serverMessage(4, text);
+
case Logout:
updateClients(data.logout.clients);
personal = new Client(data.logout.clientName, 0);
showGuestLoginPanel();
+
case UpdateClients:
updateClients(data.updateClients.clients);
personal = clients.getByName(personal.name, personal);
+
case Message:
addMessage(data.message.clientName, data.message.text);
+
case AddVideo:
if (player.isListEmpty()) player.setVideo(data.addVideo.item);
player.addVideoItem(data.addVideo.item, data.addVideo.atEnd);
+
case VideoLoaded:
player.setTime(0);
player.play();
+
case RemoveVideo:
player.removeItem(data.removeVideo.url);
if (player.isListEmpty()) player.pause();
+
case Pause:
if (isLeader()) return;
player.pause();
player.setTime(data.pause.time);
+
case Play:
if (isLeader()) return;
player.setTime(data.play.time);
player.play();
+
case GetTime:
final newTime = data.getTime.time;
final time = player.getTime();
@@ -219,23 +247,29 @@ class Main {
else player.pause();
if (Math.abs(time - newTime) < 2) return;
player.setTime(newTime);
+
case SetTime:
final newTime = data.setTime.time;
final time = player.getTime();
if (Math.abs(time - newTime) < 2) return;
player.setTime(newTime);
+
case Rewind:
player.setTime(data.rewind.time);
+
case SetLeader:
clients.setLeader(data.setLeader.clientName);
updateUserList();
setLeaderButton(isLeader());
if (isLeader()) player.setTime(player.getTime(), false);
+
case ClearChat:
ge("#messagebuffer").innerHTML = "";
+
case ClearPlaylist:
player.clearItems();
if (player.isListEmpty()) player.pause();
+
case ShufflePlaylist: // server-only
case UpdatePlaylist:
player.setItems(data.updatePlaylist.videoList);
@@ -244,6 +278,7 @@ class Main {
function onConnected(data:WsEvent):Void {
final connected = data.connected;
+ globalIp = connected.globalIp;
setConfig(connected.config);
if (connected.isUnknownClient) {
updateClients(connected.clients);
diff --git a/src/client/Player.hx b/src/client/Player.hx
index 34ef716..ca3d78f 100644
--- a/src/client/Player.hx
+++ b/src/client/Player.hx
@@ -25,6 +25,7 @@ class Player {
isLoaded = false;
video = document.createVideoElement();
video.id = "videoplayer";
+ item.url = main.tryLocalIp(item.url);
video.src = item.url;
video.controls = true;
video.oncanplaythrough = e -> {
diff --git a/src/server/HttpServer.hx b/src/server/HttpServer.hx
index b49301b..a5b752b 100644
--- a/src/server/HttpServer.hx
+++ b/src/server/HttpServer.hx
@@ -1,12 +1,11 @@
package server;
+import sys.FileSystem;
import js.node.Buffer;
import haxe.io.Path;
import js.node.Fs;
-import sys.io.File;
import js.node.http.IncomingMessage;
import js.node.http.ServerResponse;
-import js.Node.__dirname;
import js.node.Path as JsPath;
using StringTools;
@@ -33,18 +32,31 @@ class HttpServer {
];
static var dir:String;
+ static var customDir:String;
+ static var hasCustomRes = false;
+ static var allowedLocalFiles:Map<String, Bool> = [];
- public static function init(directory:String):Void {
- dir = directory;
+ public static function init(dir:String, ?customDir:String):Void {
+ HttpServer.dir = dir;
+ if (customDir == null) return;
+ HttpServer.customDir = customDir;
+ hasCustomRes = FileSystem.exists(customDir);
}
public static function serveFiles(req:IncomingMessage, res:ServerResponse):Void {
- var filePath = dir + req.url;
- if (req.url == "/") filePath = '$dir/index.html';
+ var url = req.url;
+ if (url == "/") url = "/index.html";
+ var filePath = dir + url;
final extension = Path.extension(filePath).toLowerCase();
final contentType = getMimeType(extension);
+ if (req.connection.remoteAddress == req.connection.localAddress
+ || allowedLocalFiles[url]) {
+ final isExists = serveLocalFile(res, url, extension, contentType);
+ if (isExists) return;
+ }
+
if (!isChildOf(dir, filePath)) {
res.statusCode = 500;
var rel = JsPath.relative(dir, filePath);
@@ -52,21 +64,14 @@ class HttpServer {
return;
}
- // load client code from build folder
- if (filePath == '$dir/client.js') {
- filePath = '$__dirname/client.js';
+ if (hasCustomRes) {
+ final path = customDir + url;
+ if (Fs.existsSync(path)) filePath = path;
}
- Fs.readFile(filePath, function(err:Dynamic, data:Buffer) {
+ Fs.readFile(filePath, (err:Dynamic, data:Buffer) -> {
if (err != null) {
- if (err.code == "ENOENT") {
- res.statusCode = 404;
- var rel = JsPath.relative(dir, filePath);
- res.end('File $rel not found.');
- } else {
- res.statusCode = 500;
- res.end('Error getting the file: $err.');
- }
+ readFileError(err, res, filePath);
return;
}
res.setHeader("Content-Type", contentType);
@@ -78,13 +83,40 @@ class HttpServer {
});
}
+ static function readFileError(err:Dynamic, res:ServerResponse, filePath:String):Void {
+ if (err.code == "ENOENT") {
+ res.statusCode = 404;
+ var rel = JsPath.relative(dir, filePath);
+ res.end('File $rel not found.');
+ } else {
+ res.statusCode = 500;
+ res.end('Error getting the file: $err.');
+ }
+ }
+
+ static function serveLocalFile(res:ServerResponse, filePath:String, ext:String, contentType:String):Bool {
+ if (ext != "mp4" && ext != "mp3" && ext != "wav") return false;
+ if (!Fs.existsSync(filePath)) return false;
+ allowedLocalFiles[filePath] = true;
+ Fs.readFile(filePath, (err:Dynamic, data:Buffer) -> {
+ if (err != null) {
+ readFileError(err, res, filePath);
+ return;
+ }
+ res.setHeader("Content-Type", contentType);
+ res.end(data);
+ });
+ return true;
+ }
+
static final matchLang = ~/^[A-z]+/;
+ static final matchVarString = ~/\${([A-z_]+)}/g;
static function localizeHtml(data:String, lang:String):String {
if (lang != null && matchLang.match(lang)) {
lang = matchLang.matched(0);
} else lang = "en";
- data = ~/\${([A-z_]+)}/g.map(data, (regExp) -> {
+ data = matchVarString.map(data, (regExp) -> {
final key = regExp.matched(1);
return Lang.get(lang, key);
});
@@ -92,14 +124,13 @@ class HttpServer {
}
static function isChildOf(parent:String, child:String):Bool {
- final path = JsPath;
- final relative = path.relative(parent, child);
- return relative.length > 0 && !relative.startsWith('..') && !path.isAbsolute(relative);
+ final rel = JsPath.relative(parent, child);
+ return rel.length > 0 && !rel.startsWith('..') && !JsPath.isAbsolute(rel);
}
static function getMimeType(ext:String):String {
- var contentType = mimeTypes[ext];
- if (contentType == null) contentType = "application/octet-stream";
+ final contentType = mimeTypes[ext];
+ if (contentType == null) return "application/octet-stream";
return contentType;
}
diff --git a/src/server/Main.hx b/src/server/Main.hx
index e2b9b18..442abc8 100644
--- a/src/server/Main.hx
+++ b/src/server/Main.hx
@@ -10,8 +10,10 @@ import js.Node.process;
import js.Node.__dirname;
import js.npm.ws.Server as WSServer;
import js.npm.ws.WebSocket;
+import js.node.http.IncomingMessage;
import js.node.Http;
import Types;
+using StringTools;
using ClientTools;
using Lambda;
@@ -19,6 +21,9 @@ class Main {
final rootDir = '$__dirname/..';
final wss:WSServer;
+ final localIp:String;
+ var globalIp:String;
+ final port:Int;
final config:Config;
final clients:Array<Client> = [];
final freeIds:Array<Int> = [];
@@ -33,25 +38,31 @@ class Main {
wss = new WSServer({port: wsPort});
wss.on("connection", onConnect);
function exit() {
+ // TODO save state
process.exit();
}
process.on("exit", exit);
process.on("SIGINT", exit); // ctrl+c
+ process.on("SIGUSR1", exit); // kill pid
+ process.on("SIGUSR2", exit);
process.on("uncaughtException", (log) -> {
trace(log);
});
process.on("unhandledRejection", (reason, promise) -> {
trace("Unhandled Rejection at:", reason);
});
+ localIp = Utils.getLocalIp();
+ globalIp = localIp;
+ this.port = port;
Utils.getGlobalIp(ip -> {
- final local = Utils.getLocalIp();
- trace('Local: http://$local:$port');
- trace('Global: http://$ip:$port');
+ globalIp = ip;
+ trace('Local: http://$localIp:$port');
+ trace('Global: http://$globalIp:$port');
});
final dir = '$rootDir/res';
- HttpServer.init(dir);
+ HttpServer.init(dir, '$rootDir/user/res');
Lang.init('$dir/langs');
Http.createServer((req, res) -> {
@@ -61,7 +72,7 @@ class Main {
function getUserConfig():Config {
final config:Config = Json.parse(File.getContent('$rootDir/default-config.json'));
- final customPath = '$rootDir/config.json';
+ final customPath = '$rootDir/user/config.json';
if (!FileSystem.exists(customPath)) return config;
final customConfig:Config = Json.parse(File.getContent(customPath));
for (field in Reflect.fields(customConfig)) {
@@ -71,13 +82,13 @@ class Main {
return config;
}
- function onConnect(ws:WebSocket, req):Void {
+ function onConnect(ws:WebSocket, req:IncomingMessage):Void {
final ip = req.connection.remoteAddress;
final id = freeIds.length > 0 ? freeIds.shift() : clients.length;
final name = 'Guest ${id + 1}';
trace('$name connected ($ip)');
final isAdmin = req.connection.localAddress == ip;
- final client = new Client(ws, id, name, 0);
+ final client = new Client(ws, req, id, name, 0);
if (isAdmin) client.group.set(Admin);
clients.push(client);
if (clients.length == 1 && videoList.length > 0)
@@ -93,7 +104,8 @@ class Main {
clients: [
for (client in clients) client.getData()
],
- videoList: videoList
+ videoList: videoList,
+ globalIp: globalIp
}
});
sendClientList();
@@ -103,7 +115,7 @@ class Main {
});
ws.on("close", err -> {
trace('Client ${client.name} disconnected');
- sortedPush(freeIds, client.id);
+ Utils.sortedPush(freeIds, client.id);
clients.remove(client);
sendClientList();
if (client.isLeader) {
@@ -116,17 +128,6 @@ class Main {
});
}
- function sortedPush(ids:Array<Int>, id:Int):Void {
- for (i in 0...ids.length) {
- final n = ids[i];
- if (id < n) {
- ids.insert(i, id);
- return;
- }
- }
- ids.push(id);
- }
-
function onMessage(client:Client, data:WsEvent):Void {
switch (data.type) {
case Connected:
@@ -149,6 +150,7 @@ class Main {
}
});
sendClientList();
+
case LoginError:
case Logout:
final oldName = client.name;
@@ -163,6 +165,7 @@ class Main {
}
});
sendClientList();
+
case Message:
var text = data.message.text;
if (text.length == 0) return;
@@ -175,15 +178,23 @@ class Main {
messages.push({text: text, name: client.name, time: time});
if (messages.length > config.serverChatHistory) messages.shift();
broadcast(data);
+
case AddVideo:
- if (data.addVideo.atEnd) videoList.push(data.addVideo.item);
- else videoList.insert(1, data.addVideo.item);
+ final item = data.addVideo.item;
+ final localOrigin = '$localIp:$port';
+ if (item.url.indexOf(localOrigin) != -1) {
+ item.url = item.url.replace(localOrigin, '$globalIp:$port');
+ }
+ if (data.addVideo.atEnd) videoList.push(item);
+ else videoList.insert(1, item);
broadcast(data);
// Initial timer start if VideoLoaded is not happen
if (videoList.length == 1) restartWaitTimer();
+
case VideoLoaded:
// Called if client loads next video and can play it
prepareVideoPlayback();
+
case RemoveVideo:
if (videoList.length == 0) return;
final url = data.removeVideo.url;
@@ -193,16 +204,19 @@ class Main {
);
broadcast(data);
if (videoList.length > 0) restartWaitTimer();
+
case Pause:
if (videoList.length == 0) return;
if (!client.isLeader) return;
videoTimer.pause();
broadcast(data);
+
case Play:
if (videoList.length == 0) return;
if (!client.isLeader) return;
videoTimer.play();
broadcast(data);
+
case GetTime:
if (videoList.length == 0) return;
if (videoTimer.getTime() > videoList[0].duration) {
@@ -220,11 +234,13 @@ class Main {
time: videoTimer.getTime(),
paused: videoTimer.isPaused()
}});
+
case SetTime:
if (videoList.length == 0) return;
if (!client.isLeader) return;
videoTimer.setTime(data.setTime.time);
broadcastExcept(client, data);
+
case Rewind:
if (videoList.length == 0) return;
// TODO permission
@@ -232,6 +248,7 @@ class Main {
if (data.rewind.time < 0) data.rewind.time = 0;
videoTimer.setTime(data.rewind.time);
broadcast(data);
+
case SetLeader:
clients.setLeader(data.setLeader.clientName);
broadcast({
@@ -248,12 +265,15 @@ class Main {
}
});
}
+
case ClearChat:
if (client.isAdmin) broadcast(data);
+
case ClearPlaylist:
videoTimer.stop();
videoList.resize(0);
broadcast(data);
+
case ShufflePlaylist:
if (videoList.length == 0) return;
final first = videoList.shift();
@@ -262,7 +282,7 @@ class Main {
broadcast({type: UpdatePlaylist, updatePlaylist: {
videoList: videoList
}});
- case UpdatePlaylist:
+ case UpdatePlaylist: // client-only
}
}
diff --git a/src/server/Utils.hx b/src/server/Utils.hx
index 2ecbd42..22ddc77 100644
--- a/src/server/Utils.hx
+++ b/src/server/Utils.hx
@@ -28,6 +28,17 @@ class Utils {
return "127.0.0.1";
}
+ public static function sortedPush(ids:Array<Int>, id:Int):Void {
+ for (i in 0...ids.length) {
+ final n = ids[i];
+ if (id < n) {
+ ids.insert(i, id);
+ return;
+ }
+ }
+ ids.push(id);
+ }
+
public static function shuffle<T>(arr:Array<T>):Void {
for (i in 0...arr.length) {
final n = Std.random(arr.length);
diff --git a/user/README.md b/user/README.md
new file mode 100644
index 0000000..f59e576
--- /dev/null
+++ b/user/README.md
@@ -0,0 +1,18 @@
+## User-specific config
+You can create `config.json` file in this folder to override `default-config.json` options.
+All root config fields are optional to override, so you need to create only what you want to change.
+File example:
+```json
+{
+ "channelName": "-=SuperChannel=-",
+ "videoLimit": 10,
+}
+```
+## User-specific resources
+You can patch any file you want in project `res/` by creating `user/res/sameName` files.
+For example `user/res/index.html` or `user/res/css/custom.css`.
+You can also add any new files.
+## Other files here
+- `state.json` - saved state of latest server session (messages, videos, video time).
+- `logs/` - latest 10 logs. You can change count in config.
+- `crashes/` - folder with latest error logs, when the server had to restart itself.
send patches to the email below
yukais@pinapelz.com
include the subject [PATCH repo_name]
pinapelz.com
homepage