aboutsummaryrefslogtreecommitdiffstats
path: root/src/server/Main.hx
diff options
context:
space:
mode:
authorPinapelz <yukais@pinapelz.com>2026-07-02 22:17:17 -0700
committerPinapelz <yukais@pinapelz.com>2026-07-02 22:17:17 -0700
commit04eb44d685ba677a444b73c7cfc2427d2740497f (patch)
tree94b35f31d59b2268e40a054304191be38848a7f6 /src/server/Main.hx
parentece9cf2749ac592299a34a2cf0f24672eedbd19e (diff)
implement better leader, queue, and upload management
- Majority vote skip management (Admins and the original video queuer overrides this behavior) - Leader request now requires the current leader to give up the role (via `/giveup <new-leader>`) - Overhauled default CSS and improved mobile view - Caching and uploading files is limited to Admin users only Signed-off-by: Pinapelz <yukais@pinapelz.com>
Diffstat (limited to 'src/server/Main.hx')
-rw-r--r--src/server/Main.hx113
1 files changed, 108 insertions, 5 deletions
diff --git a/src/server/Main.hx b/src/server/Main.hx
index 8b9ade1..12a3bfa 100644
--- a/src/server/Main.hx
+++ b/src/server/Main.hx
@@ -82,6 +82,8 @@ class Main {
**/
var emptyRoomCallbackTimer:Null<Timer>;
var isServerPause = false;
+ var activeSkipVoteUrl:Null<String>;
+ final skipVotes:Map<Int, Bool> = [];
static function main():Void {
new Main({
@@ -426,6 +428,11 @@ class Main {
return req.socket.remoteAddress;
}
+ function isLoopbackIp(ip:Null<String>):Bool {
+ if (ip == null) return false;
+ return ip == "127.0.0.1" || ip == "::1" || ip == "::ffff:127.0.0.1";
+ }
+
public function addAdmin(name:String, password:String):Void {
password += config.salt;
final hash = Sha256.encode(password);
@@ -504,7 +511,8 @@ class Main {
final id = freeIds.length > 0 ? freeIds.shift() : clients.length;
final name = 'Guest ${id + 1}';
trace(Date.now().toString(), '$name connected ($ip)');
- final isAdmin = config.localAdmins && req.socket.localAddress == ip;
+ final remoteIp = req.socket.remoteAddress;
+ final isAdmin = config.localAdmins && isLoopbackIp(remoteIp);
final client = new Client(ws, req, id, name, 0);
client.uuid = uuid;
client.isAdmin = isAdmin;
@@ -760,6 +768,10 @@ class Main {
// Initial timer start if VideoLoaded is not happen
if (videoList.length == 1) restartWaitTimer();
}
+ if (item.doCache && !client.isAdmin) {
+ item.doCache = false;
+ serverMessage(client, "Only admins may cache videos.");
+ }
if (!item.doCache) {
addVideo();
} else {
@@ -789,6 +801,7 @@ class Main {
case RemoveVideo:
if (videoList.length == 0) return;
+ if (activeSkipVoteUrl == data.removeVideo.url) resetSkipVote();
if (isPlaylistLockedFor(client)) return;
if (!checkPermission(client, RemoveVideoPerm)) return;
final url = data.removeVideo.url;
@@ -805,7 +818,7 @@ class Main {
case SkipVideo:
if (!checkPermission(client, RemoveVideoPerm)) return;
- skipVideo(data);
+ handleSkipVote(client, data);
case Pause:
if (videoList.length == 0) return;
@@ -910,11 +923,26 @@ class Main {
});
case SetLeader:
- final clientName = data.setLeader.clientName;
+ var clientName = data.setLeader.clientName;
if (client.name == clientName) {
if (!checkPermission(client, RequestLeaderPerm)) return;
- } else if (!client.isLeader && clientName != "") {
- if (!checkPermission(client, SetLeaderPerm)) return;
+ final currentLeader = clients.find(c -> c.isLeader);
+ final hasOtherLeader = currentLeader != null && currentLeader != client;
+ if (!client.isAdmin && hasOtherLeader) {
+ serverMessage(client,
+ 'Leader request sent to ${currentLeader.name}. Waiting for /giveup ${client.name}.');
+ serverMessage(currentLeader,
+ '${client.name} requested leader. Use /giveup ${client.name} to transfer leadership.');
+ return;
+ }
+ } else if (clientName == "") {
+ if (!client.isLeader && !checkPermission(client, SetLeaderPerm)) return;
+ } else if (!client.isLeader && !checkPermission(client, SetLeaderPerm)) {
+ return;
+ }
+ if (clientName != "" && clients.getByName(clientName) == null) {
+ serverMessage(client, 'Client "$clientName" not found.');
+ return;
}
isServerPause = false;
clients.setLeader(clientName);
@@ -938,6 +966,7 @@ class Main {
case PlayItem:
if (!checkPermission(client, ChangeOrderPerm)) return;
+ resetSkipVote();
final pos = data.playItem.pos;
if (!videoList.hasItem(pos)) return;
if (videoTimer.getTime() > FLASHBACK_DIST) {
@@ -972,6 +1001,7 @@ class Main {
case ClearPlaylist:
if (isPlaylistLockedFor(client)) return;
+ resetSkipVote();
if (!checkPermission(client, RemoveVideoPerm)) return;
if (videoList.length != 0) {
if (videoTimer.getTime() > FLASHBACK_DIST) {
@@ -1113,8 +1143,81 @@ class Main {
return value;
}
+ function resetSkipVote():Void {
+ activeSkipVoteUrl = null;
+ skipVotes.clear();
+ }
+
+ function getSkipVoteEligibleClients():Array<Client> {
+ return [
+ for (c in clients)
+ if (!c.isBanned && c.hasPermission(RemoveVideoPerm, config.permissions)) c
+ ];
+ }
+
+ function getSkipVoteCount():Int {
+ var count = 0;
+ for (c in getSkipVoteEligibleClients()) {
+ if (skipVotes[c.id]) count++;
+ }
+ return count;
+ }
+
+ function getSkipVoteRequiredCount():Int {
+ final total = getSkipVoteEligibleClients().length;
+ if (total <= 1) return 1;
+ return Std.int(Math.floor(total / 2)) + 1;
+ }
+
+ function handleSkipVote(client:Client, data:WsEvent):Void {
+ if (videoList.length == 0) return;
+ final item = videoList.currentItem;
+ if (item.url != data.skipVideo.url) return;
+
+ final isOwnItemAuthor = item.author == client.name;
+ if (client.isAdmin || isOwnItemAuthor) {
+ if (activeSkipVoteUrl != null) {
+ final reason = client.isAdmin ? "forced skip" : "skipped own queued video";
+ broadcast({
+ type: ServerMessage,
+ serverMessage: {
+ textId: '${client.name} ${reason}.'
+ }
+ });
+ }
+ resetSkipVote();
+ skipVideo(data);
+ return;
+ }
+
+ if (activeSkipVoteUrl != item.url) {
+ activeSkipVoteUrl = item.url;
+ skipVotes.clear();
+ }
+ if (skipVotes[client.id]) {
+ serverMessage(client, "You already voted to skip this video.");
+ return;
+ }
+ skipVotes[client.id] = true;
+
+ final votes = getSkipVoteCount();
+ final required = getSkipVoteRequiredCount();
+ broadcast({
+ type: ServerMessage,
+ serverMessage: {
+ textId: '${client.name} voted to skip (${votes}/${required}).'
+ }
+ });
+
+ if (votes >= required) {
+ resetSkipVote();
+ skipVideo(data);
+ }
+ }
+
function skipVideo(data:WsEvent):Void {
if (videoList.length == 0) return;
+ resetSkipVote();
final item = videoList.currentItem;
if (item.url != data.skipVideo.url) return;
final dur = videoList.currentItem.duration;
send patches to the email below
yukais@pinapelz.com
include the subject [PATCH repo_name]
pinapelz.com
homepage