aboutsummaryrefslogtreecommitdiffstats
path: root/src/server/Main.hx
diff options
context:
space:
mode:
Diffstat (limited to 'src/server/Main.hx')
-rw-r--r--src/server/Main.hx243
1 files changed, 201 insertions, 42 deletions
diff --git a/src/server/Main.hx b/src/server/Main.hx
index 12a3bfa..b55f9b0 100644
--- a/src/server/Main.hx
+++ b/src/server/Main.hx
@@ -25,6 +25,7 @@ import js.npm.ws.WebSocket;
import json2object.ErrorUtils;
import json2object.JsonParser;
import server.cache.Cache;
+import server.cache.CacheCallbacks;
import sys.FileSystem;
import sys.io.File;
import utils.macro.Macro;
@@ -81,7 +82,6 @@ class Main {
to stopped server time.
**/
var emptyRoomCallbackTimer:Null<Timer>;
- var isServerPause = false;
var activeSkipVoteUrl:Null<String>;
final skipVotes:Map<Int, Bool> = [];
@@ -135,7 +135,11 @@ class Main {
consoleInput = new ConsoleInput(this);
consoleInput.initConsoleInput();
cache = new Cache(this, cacheDir);
- if (cache.isYtReady) playersCacheSupport.push(YoutubeType);
+ if (cache.isYtReady) {
+ playersCacheSupport.push(YoutubeType);
+ playersCacheSupport.push(VkType);
+ playersCacheSupport.push(VimeoType);
+ }
initIntergationHandlers();
loadState();
cache.setStorageLimit(cast config.cacheStorageLimitGiB * 1024 * 1024 * 1024);
@@ -203,6 +207,7 @@ class Main {
dir: dir,
customDir: '$userDir/res',
allowLocalRequests: config.localAdmins,
+ enableCORS: config.enableCORS,
cache: cache,
});
Lang.init('$dir/langs');
@@ -302,6 +307,7 @@ class Main {
if (isNoState) return config;
final customPath = '$userDir/config.json';
if (!FileSystem.exists(customPath)) return config;
+ final defaultYtDlp = config.ytDlp;
final customConfig:ServerConfig = Json.parse(File.getContent(customPath));
for (field in Reflect.fields(customConfig)) {
if (Reflect.field(config, field) == null) {
@@ -309,6 +315,8 @@ class Main {
}
Reflect.setField(config, field, Reflect.field(customConfig, field));
}
+ config.ytDlp.channel ??= defaultYtDlp.channel;
+ config.ytDlp.jsRuntime ??= defaultYtDlp.jsRuntime;
final emoteCopies:Map<String, Bool> = [];
for (emote in config.emotes) {
if (emoteCopies[emote.name]) trace('Warning: emote name "${emote.name}" has copy');
@@ -567,7 +575,7 @@ class Main {
if (!internal) return;
emptyRoomCallbackTimer?.stop();
if (clients.length == 1 && videoList.length > 0) {
- if (!isServerPause) {
+ if (!videoTimer.isServerPause) {
if (videoTimer.isPaused()) videoTimer.play();
}
}
@@ -590,6 +598,7 @@ class Main {
}
});
sendClientListExcept(client);
+ broadcastSkipVoteStats();
case Disconnected:
if (!internal) return;
@@ -600,9 +609,11 @@ class Main {
if (client.isLeader) {
if (videoList.length > 0) {
videoTimer.pause();
- isServerPause = true;
+ videoTimer.isServerPause = true;
}
}
+ skipVotes.remove(client.id);
+ updateSkipVote();
if (clients.length == 0) {
emptyRoomCallbackTimer?.stop();
emptyRoomCallbackTimer = Timer.delay(() -> {
@@ -697,6 +708,7 @@ class Main {
}
});
sendClientListExcept(client);
+ broadcastSkipVoteStats();
case PasswordRequest:
case LoginError:
@@ -715,6 +727,7 @@ class Main {
}
});
sendClientListExcept(client);
+ broadcastSkipVoteStats();
case Message:
if (!checkPermission(client, WriteChatPerm)) return;
@@ -734,6 +747,20 @@ class Main {
case ServerMessage:
case Progress:
+ final p = data.progress;
+ final url = p.url ?? return;
+ final index = videoList.findIndex(i -> i.url == url);
+ if (index == -1) return;
+ final item = videoList.getItem(index);
+ if (item.isIncomplete != true) return;
+ if (item.doCache) return;
+ if (item.author != client.name) return;
+ switch p.type {
+ case Completed: completeIncompleteItem(url);
+ case Canceled: removeIncompleteItem(url);
+ case _: broadcast(data);
+ }
+
case AddVideo:
if (isPlaylistLockedFor(client)) return;
if (!checkPermission(client, AddVideoPerm)) return;
@@ -766,7 +793,7 @@ class Main {
videoList.addItem(item, data.addVideo.atEnd);
broadcast(data);
// Initial timer start if VideoLoaded is not happen
- if (videoList.length == 1) restartWaitTimer();
+ if (videoList.length == 1 && item.isPlayable()) restartWaitTimer();
}
if (item.doCache && !client.isAdmin) {
item.doCache = false;
@@ -774,34 +801,45 @@ class Main {
}
if (!item.doCache) {
addVideo();
- } else {
- switch item.playerType {
- case RawType:
- cache.cacheRawVideo(client, item.url, (name) -> {
- item = item.withUrl(cache.getFileUrl(name));
- addVideo();
- });
- case YoutubeType:
- cache.cacheYoutubeVideo(client, item.url, (name) -> {
- item = item.withUrl(cache.getFileUrl(name));
- if (item.duration > 1) item.duration -= 1;
- addVideo();
- });
- case type:
- final name = '$type'.replace("Type", "");
- serverMessage(client, 'No cache support for $name player.');
- addVideo();
- }
+ return;
+ }
+ final isYoutube = item.playerType == YoutubeType;
+ if (!playersCacheSupport.contains(item.playerType)) {
+ final name = '${item.playerType}'.replace("Type", "");
+ serverMessage(client, 'No cache support for $name player.');
+ addVideo();
+ return;
+ }
+ final callbacks:CacheCallbacks = {
+ onResolved: finalUrl -> {
+ item = item.withUrl(finalUrl);
+ item.isIncomplete = true;
+ if (isYoutube && item.duration > 1) item.duration -= 1;
+ addVideo();
+ },
+ onProgress: ratio -> broadcastCacheProgress(item.url, ratio),
+ onComplete: () -> completeIncompleteItem(item.url),
+ onError: () -> removeIncompleteItem(item.url),
+ registerCancel: cancel -> cacheCancelers[item.url] = cancel,
+ onMetadata: (title, duration) ->
+ updateIncompleteItemMeta(item.url, title, duration),
+ };
+ switch (item.playerType) {
+ case YoutubeType:
+ cache.cacheYoutubeVideo(client, item.url, callbacks);
+ case VkType, VimeoType:
+ cache.cacheYtdlpVideo(client, item.url, callbacks);
+ case _:
+ cache.cacheRawVideo(client, item.url, callbacks);
}
case VideoLoaded:
// Called if client loads next video and can play it
- if (isServerPause) return;
+ if (videoTimer.isServerPause) return;
prepareVideoPlayback();
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;
@@ -809,17 +847,27 @@ class Main {
if (index == -1) return;
final isCurrent = videoList.currentItem.url == url;
+ if (isCurrent) resetSkipVote();
if (isCurrent && videoTimer.getTime() > FLASHBACK_DIST) {
saveFlashbackTime(videoList.currentItem);
}
videoList.removeItem(index);
+ cancelCache(url);
broadcast(data);
- if (isCurrent && videoList.length > 0) restartWaitTimer();
+ if (isCurrent && videoList.length > 0) {
+ if (!videoList.currentItem.isPlayable()) pauseForIncompleteItem();
+ else restartWaitTimer();
+ }
+ if (videoList.length == 0) {
+ videoTimer.stop();
+ }
case SkipVideo:
if (!checkPermission(client, RemoveVideoPerm)) return;
handleSkipVote(client, data);
+ case SkipVideoStats:
+
case Pause:
if (videoList.length == 0) return;
if (!client.isLeader) return;
@@ -840,7 +888,6 @@ class Main {
saveFlashbackTime(videoList.currentItem);
}
videoTimer.setTime(data.play.time);
- isServerPause = false;
videoTimer.play();
broadcast({
type: data.type,
@@ -849,6 +896,7 @@ class Main {
case GetTime:
if (videoList.length == 0) return;
+ if (videoList.currentItem.isIncomplete) return;
final maxTime = videoList.currentItem.duration - 0.01;
if (videoTimer.getTime() > maxTime) {
videoTimer.pause();
@@ -871,7 +919,7 @@ class Main {
}
};
if (videoTimer.isPaused()) obj.getTime.paused = true;
- if (isServerPause) obj.getTime.pausedByServer = true;
+ if (videoTimer.isServerPause) obj.getTime.pausedByServer = true;
if (videoTimer.getRate() != 1) {
if (!clients.hasLeader()) videoTimer.setRate(1);
else obj.getTime.rate = videoTimer.getRate();
@@ -944,7 +992,7 @@ class Main {
serverMessage(client, 'Client "$clientName" not found.');
return;
}
- isServerPause = false;
+ videoTimer.isServerPause = false;
clients.setLeader(clientName);
broadcast({
type: SetLeader,
@@ -966,9 +1014,9 @@ class Main {
case PlayItem:
if (!checkPermission(client, ChangeOrderPerm)) return;
- resetSkipVote();
final pos = data.playItem.pos;
if (!videoList.hasItem(pos)) return;
+ resetSkipVote();
if (videoTimer.getTime() > FLASHBACK_DIST) {
saveFlashbackTime(videoList.currentItem);
}
@@ -991,6 +1039,17 @@ class Main {
if (!checkPermission(client, ToggleItemTypePerm)) return;
final pos = data.toggleItemType.pos;
if (!videoList.hasItem(pos)) return;
+ if (data.toggleItemType.all == true) {
+ final targetIsTemp = !videoList.getItem(pos).isTemp;
+ videoList.setAllItemTypes(targetIsTemp);
+ broadcast({
+ type: UpdatePlaylist,
+ updatePlaylist: {
+ videoList: videoList.getItems()
+ }
+ });
+ return;
+ }
videoList.toggleItemType(pos);
broadcast(data);
@@ -1001,8 +1060,8 @@ class Main {
case ClearPlaylist:
if (isPlaylistLockedFor(client)) return;
- resetSkipVote();
if (!checkPermission(client, RemoveVideoPerm)) return;
+ resetSkipVote();
if (videoList.length != 0) {
if (videoTimer.getTime() > FLASHBACK_DIST) {
saveFlashbackTime(videoList.currentItem);
@@ -1200,36 +1259,137 @@ class Main {
}
skipVotes[client.id] = true;
+ updateSkipVote();
+ }
+
+ function updateSkipVote():Void {
+ if (activeSkipVoteUrl == null) return;
+ if (videoList.length == 0 || videoList.currentItem.url != activeSkipVoteUrl) {
+ resetSkipVote();
+ return;
+ }
final votes = getSkipVoteCount();
final required = getSkipVoteRequiredCount();
+ if (votes >= required) {
+ skipVideo({
+ type: SkipVideo,
+ skipVideo: {url: activeSkipVoteUrl}
+ });
+ return;
+ }
+ broadcastSkipVoteStats();
+ }
+
+ function broadcastSkipVoteStats():Void {
+ if (activeSkipVoteUrl == null) return;
broadcast({
- type: ServerMessage,
- serverMessage: {
- textId: '${client.name} voted to skip (${votes}/${required}).'
+ type: SkipVideoStats,
+ skipVideoStats: {
+ voted: getSkipVoteCount(),
+ total: getSkipVoteEligibleClients().length,
+ required: getSkipVoteRequiredCount()
}
});
-
- 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;
+ resetSkipVote();
final dur = videoList.currentItem.duration;
if (videoTimer.getTime() > FLASHBACK_DIST
&& videoTimer.getTime() < dur - FLASHBACK_DIST) {
saveFlashbackTime(videoList.currentItem);
}
videoList.skipItem();
- if (videoList.length > 0) restartWaitTimer();
+ if (videoList.length > 0) {
+ if (!videoList.currentItem.isPlayable()) pauseForIncompleteItem();
+ else restartWaitTimer();
+ } else {
+ videoTimer.stop();
+ }
broadcast(data);
}
+ function pauseForIncompleteItem():Void {
+ videoTimer.stop();
+ waitVideoStart?.stop();
+ }
+
+ final cacheCancelers:Map<String, () -> Void> = [];
+
+ function cancelCache(url:String):Void {
+ final cancel = cacheCancelers[url] ?? return;
+ cacheCancelers.remove(url);
+ cancel();
+ }
+
+ function broadcastCacheProgress(url:String, ratio:Float):Void {
+ broadcast({
+ type: Progress,
+ progress: {
+ type: Downloading,
+ ratio: ratio,
+ url: url
+ }
+ });
+ }
+
+ function updateIncompleteItemMeta(url:String, title:Null<String>, duration:Float):Void {
+ final item = videoList.find(item -> item.url == url) ?? return;
+ if (item.isIncomplete != true) return;
+ var changed = false;
+ if (item.duration <= 0 && duration > 0) {
+ item.duration = duration;
+ changed = true;
+ }
+ if (title != null && title.length > 0 && item.title.length == 0) {
+ item.title = title;
+ changed = true;
+ }
+ if (!changed) return;
+ broadcast({
+ type: UpdatePlaylist,
+ updatePlaylist: {
+ videoList: videoList.getItems()
+ }
+ });
+ }
+
+ function completeIncompleteItem(url:String):Void {
+ cacheCancelers.remove(url);
+ final index = videoList.findIndex(i -> i.url == url);
+ if (index == -1) return;
+ final item = videoList.getItem(index);
+ if (item.isIncomplete != true) return;
+ item.isIncomplete = false;
+ broadcast({
+ type: Progress,
+ progress: {
+ type: Completed,
+ ratio: 1,
+ url: url
+ }
+ });
+ // uploaded videos don't need timer restart
+ if (videoList.pos == index && item.doCache) restartWaitTimer();
+ }
+
+ function removeIncompleteItem(url:String):Void {
+ cacheCancelers.remove(url);
+ final index = videoList.findIndex(i -> i.url == url);
+ if (index == -1) return;
+ if (videoList.getItem(index).isIncomplete != true) return;
+ videoList.removeItem(index);
+ broadcast({
+ type: RemoveVideo,
+ removeVideo: {
+ url: url
+ }
+ });
+ }
+
function checkPermission(client:Client, perm:Permission):Bool {
if (client.isBanned) checkBan(client);
final state = client.hasPermission(perm, config.permissions);
@@ -1291,7 +1451,6 @@ class Main {
waitVideoStart?.stop();
loadedClientsCount = 0;
broadcast({type: VideoLoaded});
- isServerPause = false;
videoTimer.start();
}
send patches to the email below
yukais@pinapelz.com
include the subject [PATCH repo_name]
pinapelz.com
homepage