Version 2.1.0 - Update UI

This commit is contained in:
raphix committed 2026-10-08 10:06:46 +02:00
1 parent 6017e03eac
commit dcc350241e
18 files changed
+3508 -330

No files matched your search

+121 -15
View File
@@ -19,6 +19,7 @@ const players = require('../player/Player')
const discordBot = require('../discord/Bot')
const serverSettings = require('../discord/ServerSettings')
const playlists = require('../playlists/PlaylistManager')
const guildPlaylists = require('../playlists/GuildPlaylists')
const warp = require('../utils/WarpProxy')
const SafeMetric = require('../utils/SafeMetric')
const LogReader = require('../utils/LogReader')
@@ -53,7 +54,9 @@ const SPOTIFY_TEST_TIMEOUT = 15 * 1000
// Changement des processus yt-dlp actifs (Youtube.js ne prévient pas le player) : sondage léger
const YTDLP_PROCESS_POLL = 2000
// Jamais "config" : contient le token du bot et les secrets
const DATABASES = ["Users", "Playlists", "history", "media", "previous", "server_settings"]
const DATABASES = ["Users", "Playlists", "GuildPlaylists", "history", "media", "previous", "server_settings"]
// Playlists partagées : qui peut les modifier (tous les membres / modérateurs)
const SHARED_PLAYLISTS_EDITORS = ["members", "mods"]
const PLAYER_ACTIONS = ["pause", "resume", "skip", "previous", "stop", "leave", "loop", "shuffle", "clear"]
const LOG_LEVELS = ["INFO", "WARN", "ERROR", "FATAL"]
const USER_FILTERS = ["all", "online", "admin", "banned", "deleted"]
@@ -447,7 +450,42 @@ function guildSummary(guildData, ctx = buildContext()) {
missingPermissions,
isMediaGuild: !!id && id === configuration.getMediaGuildId(),
player: isConnectedPlayer(player) ? playerSummary(player) : null,
stats: guildStats(id)
stats: guildStats(id),
sharedPlaylists: sharedPlaylistsSummary(id)
}
}
/**
* Option des playlists partagées d'un serveur + nombre de playlists enregistrées (même masquées)
*/
function sharedPlaylistsSummary(guildId) {
const settings = typeof serverSettings.getSharedPlaylists === "function"
? serverSettings.getSharedPlaylists(guildId)
: { enabled: false, editors: "members" }
return {
enabled: !!settings?.enabled,
editors: SHARED_PLAYLISTS_EDITORS.includes(settings?.editors) ? settings.editors : "members",
count: guildPlaylists.countPlaylists(guildId)
}
}
/**
* Résumé d'une playlist pour les listes d'administration (jamais la liste des titres)
*/
function playlistLite(p) {
return {
playlistId: p?.playlistId != null ? String(p.playlistId) : null,
title: p?.title ?? null,
type: p?.type ?? null,
// "google" : synchronisée depuis le compte Google du propriétaire
source: p?.source ?? null,
url: typeof p?.url === "string" ? p.url : null,
thumbnail: typeof p?.thumbnail === "string" ? p.thumbnail : null,
duration: safeNum(p?.duration),
syncedAt: toIso(p?.syncedAt),
createdAt: toIso(p?.createdAt),
updatedAt: toIso(p?.updatedAt),
songCount: Array.isArray(p?.songs) ? p.songs.length : 0
}
}
@@ -1601,6 +1639,27 @@ function attach({ io: ioInstance, socket, socketUser }) {
return { secureRole: role ? { id: role.id, name: role.name } : null }
})
// Playlists partagées d'un serveur : {guildId, enabled?, editors?} (désactiver masque sans rien supprimer)
AdminRequest("/ADMIN/GUILDS/SET_SHARED_PLAYLISTS", async (payload, me) => {
const data = asObject(payload)
const guildId = requireId(data.guildId, "guildId")
if (!discordBot.getGuilds().has(guildId)) throw new AdminError("NOT_FOUND", "Serveur introuvable")
const patch = {}
if (data.enabled !== undefined) patch.enabled = requireBool(data.enabled, "enabled")
if (data.editors !== undefined) {
if (!SHARED_PLAYLISTS_EDITORS.includes(data.editors)) throw new AdminError("INVALID_PARAMS", "Paramètre \"editors\" invalide (members ou mods)")
patch.editors = data.editors
}
if (Object.keys(patch).length === 0) throw new AdminError("INVALID_PARAMS", "Aucune modification demandée (enabled ou editors)")
const settings = serverSettings.setSharedPlaylists(guildId, patch)
if (!settings) throw new AdminError("INVALID_PARAMS", "Réglage des playlists partagées invalide")
audit(me, "playlists partagées", `${guildId} -> ${settings.enabled ? "activées" : "désactivées"}, modification : ${settings.editors}`)
process.emit("GUILDS_UPDATE")
// Les membres présents sur ce serveur rechargent leurs playlists partagées
process.emit("GUILD_PLAYLISTS_UPDATE", guildId)
return { sharedPlaylists: sharedPlaylistsSummary(guildId) }
})
AdminRequest("/ADMIN/GUILDS/REFRESH", async (payload, me) => {
if (typeof discordBot.refreshGuilds !== "function") throw new AdminError("UNAVAILABLE", "Rafraîchissement indisponible")
await discordBot.refreshGuilds()
@@ -1763,27 +1822,63 @@ function attach({ io: ioInstance, socket, socketUser }) {
username: owner?.identity?.username ?? null,
// Identité légère pour l'affichage (jamais auth ni tokens)
owner: owner ? { ...userLite(owner.identity), isAdmin: owner.labels?.includes("ADMIN") ?? false, isDeleted: owner.labels?.includes("DELETED") ?? false } : null,
playlists: list.map(p => ({
playlistId: p?.playlistId != null ? String(p.playlistId) : null,
title: p?.title ?? null,
type: p?.type ?? null,
// "google" : synchronisée depuis le compte Google du propriétaire
source: p?.source ?? null,
url: typeof p?.url === "string" ? p.url : null,
thumbnail: typeof p?.thumbnail === "string" ? p.thumbnail : null,
duration: safeNum(p?.duration),
syncedAt: toIso(p?.syncedAt),
songCount: Array.isArray(p?.songs) ? p.songs.length : 0
}))
playlists: list.map(playlistLite)
})
}
return result
})
// Playlists partagées des serveurs : {guildId?}. Serveurs ayant des playlists (même désactivées ou quittés
// par le bot) ou l'option activée
AdminRequest("/ADMIN/PLAYLISTS/GUILDS", async (payload) => {
const data = asObject(payload)
const filter = data.guildId === undefined || data.guildId === null ? null : requireId(data.guildId, "guildId")
const botGuilds = discordBot.getGuilds()
const ids = new Set(guildPlaylists.listGuildIds())
if (typeof serverSettings.getSharedPlaylists === "function") {
for (const guildId of botGuilds.keys()) {
if (serverSettings.getSharedPlaylists(guildId)?.enabled) ids.add(guildId)
}
}
if (filter) {
ids.clear()
ids.add(filter)
}
const result = []
for (const guildId of ids) {
const list = guildPlaylists.getPlaylists(guildId)
const shared = sharedPlaylistsSummary(guildId)
if (!filter && list.length === 0 && !shared.enabled) continue
const guild = botGuilds.get(guildId)
result.push({
guildId,
guildName: guild?.name ?? null,
guildIcon: guild?.icon ?? null,
settings: { enabled: shared.enabled, editors: shared.editors },
playlists: list.map(p => ({
...playlistLite(p),
createdBy: p?.createdBy ? userLite(p.createdBy) : null
}))
})
}
return result.sort((a, b) => (a.guildName || a.guildId).localeCompare(b.guildName || b.guildId))
})
// {userId, playlistId} : playlist personnelle ; {guildId, playlistId} : playlist partagée d'un serveur
AdminRequest("/ADMIN/PLAYLISTS/DELETE", async (payload, me) => {
const data = asObject(payload)
const userId = requireId(data.userId)
const playlistId = requireString(typeof data.playlistId === "number" ? String(data.playlistId) : data.playlistId, "playlistId", 1, 64)
if (data.guildId !== undefined && data.guildId !== null) {
const guildId = requireId(data.guildId, "guildId")
const playlist = guildPlaylists.getPlaylist(guildId, playlistId)
if (!playlist) throw new AdminError("NOT_FOUND", "Playlist introuvable")
if (!guildPlaylists.deletePlaylist(guildId, playlist.playlistId)) throw new AdminError("NOT_FOUND", "Playlist introuvable")
audit(me, "suppression de la playlist partagée", `"${playlist.title}" (${playlistId}) du serveur ${guildId}`)
process.emit("GUILDS_UPDATE")
process.emit("GUILD_PLAYLISTS_UPDATE", guildId)
return { deleted: true }
}
const userId = requireId(data.userId)
const list = dbData("Playlists", {})[userId]
const playlist = Array.isArray(list) ? list.find(p => p?.playlistId != null && String(p.playlistId) === playlistId) : null
if (!playlist) throw new AdminError("NOT_FOUND", "Playlist introuvable")
@@ -2016,6 +2111,17 @@ function init(ioInstance, deps = {}) {
}, 500)
process.on("GUILDS_UPDATE", pushGuilds)
// Playlists modifiées (personnelles ou partagées) : la section Playlists ouverte se relit d'elle-même
const changedScopes = new Set()
const pushPlaylists = debounce(() => {
const scopes = [...changedScopes]
changedScopes.clear()
if (roomSize(ADMIN_ROOM) === 0) return
io.to(ADMIN_ROOM).emit("/ADMIN/PLAYLISTS/UPDATE", { scopes })
}, 800)
process.on("PLAYLISTS_UPDATE", () => { changedScopes.add("personal"); pushPlaylists() })
process.on("GUILD_PLAYLISTS_UPDATE", () => { changedScopes.add("guild"); pushPlaylists() })
// WARP : changement d'état du proxy
process.on("WARP_UPDATE", () => {
if (roomSize(ADMIN_ROOM) === 0) return