diff options
| author | Pinapelz <yukais@pinapelz.com> | 2026-10-19 15:32:53 -0700 |
|---|---|---|
| committer | Pinapelz <yukais@pinapelz.com> | 2026-10-19 15:32:53 -0700 |
| commit | 304bd1e77a5442f614aacd2b35f330777118b2cd (patch) | |
| tree | 970e7da13bb1bd8edf1f9b453af4d0994c665e94 /src/server/Main.hx | |
| parent | a4f3064542ddad6939598e7e60ca0090be07df60 (diff) | |
Diffstat (limited to 'src/server/Main.hx')
| -rw-r--r-- | src/server/Main.hx | 243 |
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(); } |
