diff --git a/.gitignore b/.gitignore index e8f57ff..9dc20d2 100644 --- a/.gitignore +++ b/.gitignore @@ -144,4 +144,23 @@ test/ data/ __DEBUG.js -__TEST.js \ No newline at end of file +__TEST.js + +# --- YT-DLP / Téléchargements temporaires --- +# Ignore tous les fichiers contenant "Frag" (votre demande spécifique) +*Frag* + +# Ignore les fichiers partiels classiques (.part, .part-Frag...) +*.part* + +# Ignore les fichiers temporaires de l'ancien format youtube-dl +*.ytdl + +# Ignore les formats temporaires lors du merge (ex: vidéo.f137.webm) +*.f[0-9]* + +# --- SÉCURITÉ (INDISPENSABLE) --- +# N'oubliez pas d'ignorer votre fichier de cookies pour ne pas le partager ! +cookies.txt +cookies-*.txt +tmp \ No newline at end of file diff --git a/CHANGELOG.html b/CHANGELOG.html index ce3d20c..e1d0d82 100644 --- a/CHANGELOG.html +++ b/CHANGELOG.html @@ -1,37 +1,52 @@
+

Chopin - Version /*1.4.0*/

+

*_Date de sortie_*: *-06/12/2025-*

+ +
+ +

Chopin - Version /*1.3.0*/

*_Date de sortie_*: *-07/10/2025-*

+

Chopin - Version /*1.2.0*/

*_Date de sortie_*: *-07/09/2025-*

+

Chopin - Version /*1.1.0*/

*_Date de sortie_*: *-06/09/2025-*

+

Chopin - Version /*1.0.0*/

*_Date de sortie_*: *-29/08/2025-*

-
+ \ No newline at end of file diff --git a/TODOS.md b/TODOS.md index 043c975..42c73fa 100644 --- a/TODOS.md +++ b/TODOS.md @@ -1,3 +1,4 @@ # List TODO: Récupération des recommendations, playlists Youtube et Spotify +TODO: Faire une interface Admin \ No newline at end of file diff --git a/package.json b/package.json index 2c996cd..05fc2c1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "chopin-backend", - "version": "1.3.2", + "version": "1.4.0", "description": "Discord Bot for music - Fetching everywhere !", "main": "src/main.js", "nodemonConfig": { diff --git a/src/player/List.js b/src/player/List.js index 614344c..1bfcc15 100644 --- a/src/player/List.js +++ b/src/player/List.js @@ -60,7 +60,6 @@ class List { } this.setCurrent(song) process.emit("PLAYERS_UPDATE") - //TODO: Check History and continuity return song } diff --git a/src/player/Method/Youtube.js b/src/player/Method/Youtube.js index ad6a8e8..d889afb 100644 --- a/src/player/Method/Youtube.js +++ b/src/player/Method/Youtube.js @@ -2,51 +2,146 @@ const { LogType } = require('loguix'); const clog = new LogType("Youtube-Stream"); const { spawn } = require('child_process'); -async function getStream(song) { - return new Promise((resolve, reject) => { - clog.log(`[YT-DLP] Lancement du processus natif pour : ${song.url}`); +// Variable globale pour stocker le processus actif +let currentYtProcess = null; - // On lance yt-dlp directement. - // ATTENTION : "yt-dlp" doit être reconnu dans ton terminal (installé dans le PATH) - const yt = spawn('yt-dlp', [ +/** + * Tue le processus yt-dlp en cours proprement et attend sa fin réelle. + * Cela garantit qu'aucun flux ne se chevauche. + */ +function killCurrentProcess() { + return new Promise((resolve) => { + if (!currentYtProcess || currentYtProcess.exitCode !== null) { + currentYtProcess = null; + return resolve(); + } + + const pid = currentYtProcess.pid; + clog.log(`[YT-DLP] Nettoyage violent du processus PID: ${pid}`); + + // Détection de l'OS pour utiliser la bonne commande de kill + const isWindows = process.platform === 'win32'; + + if (isWindows) { + // Sur Windows, taskkill /T (Tree) /F (Force) est nécessaire pour tuer les enfants (ffmpeg) + try { + exec(`taskkill /pid ${pid} /T /F`, (err) => { + // Peu importe l'erreur (ex: processus déjà mort), on considère que c'est fini + currentYtProcess = null; + resolve(); + }); + } catch (e) { + // Fallback si taskkill échoue + try { currentYtProcess.kill('SIGKILL'); } catch (e2) {} + currentYtProcess = null; + resolve(); + } + } else { + // Sur Linux/Mac, on tente de tuer le groupe de processus + try { + process.kill(-pid, 'SIGKILL'); + } catch (e) { + // Fallback si le group kill échoue + try { currentYtProcess.kill('SIGKILL'); } catch (e2) {} + } + currentYtProcess = null; + resolve(); + } + }); +} + +/** + * @param {Object} song - L'objet contenant l'URL + * @param {number} seekTime - Le temps de démarrage en secondes (par défaut 0) + */ +async function getStream(song, seekTime = 0) { + // ÉTAPE 1 : On s'assure que l'ancien processus est BIEN mort avant de faire quoi que ce soit. + await killCurrentProcess(); + + return new Promise((resolve, reject) => { + clog.log(`[YT-DLP] Lancement pour : ${song.url} (Début: ${seekTime}s)`); + + const ytArgs = [ song.url, - '-o', '-', // Rediriger le son vers la sortie standard (stdout) - '-f', 'bestaudio', // Meilleure qualité audio - '--no-warnings', // Masquer les avertissements - '--no-check-certificate', // Évite les erreurs SSL courantes - '--prefer-free-formats' // Préférer Opus/WebM (meilleur pour Discord) - ]); + '-o', '-', + '-f', 'bestaudio[ext=webm]/bestaudio[ext=m4a]/bestaudio', + // NETWORKS + '--force-ipv4', // INDISPENSABLE : Force la connexion via IPv4 (plus stable pour le streaming) + '--user-agent', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', // Masque yt-dlp + '--no-warnings', + // --- GESTION DE LA RAM & FICHIERS --- + '--no-part', // Ne pas créer de fichiers .part + '--no-keep-fragments', // Supprimer les fragments immédiatement (s'ils sont créés) + '--buffer-size', '16K', // Petit buffer pour forcer le flux continu (évite les pics d'écriture) + // --- OPTIONS DIVERS --- + '--no-check-certificate', + '--prefer-free-formats', + '--ignore-config' + ]; + + // --- GESTION DU TIMECODE (SEEK) --- + if (seekTime && seekTime > 0) { + // --begin accepte un format "10s", "1m30s" ou juste des secondes. + // C'est la méthode la plus fiable pour yt-dlp en streaming. + clog.log(`[YT-DLP] Positionnement à ${Math.round(seekTime)} secondes.`); + ytArgs.push('--download-sections', `*${Math.round(seekTime)}-inf`); + } + + // --- GESTION DES COOKIES --- + if (typeof __glob !== 'undefined' && __glob.COOKIES) { + // clog.log(`[YT-DLP] Cookies chargés.`); + ytArgs.push('--cookies', __glob.COOKIES); + ytArgs.push('--no-cache-dir'); + } + + // Lancement du nouveau processus + const yt = spawn('yt-dlp', ytArgs); + + // On met à jour la variable globale tout de suite + currentYtProcess = yt; let errorLogs = ""; - // Capture des erreurs du processus (si yt-dlp râle) yt.stderr.on('data', (data) => { - // On ignore les infos de progression [download] const msg = data.toString(); - if (!msg.includes('[download]')) { + console.log(msg); + if (!msg.includes('[download]') && !msg.includes('[youtube]')) { errorLogs += msg; } }); - // Gestion des erreurs de lancement (ex: yt-dlp n'est pas installé) yt.on('error', (err) => { - clog.error("[YT-DLP] Impossible de lancer la commande. Vérifie que yt-dlp est bien installé sur le PC/Serveur !", err); + clog.error("[YT-DLP] Erreur au lancement.", err); + // Si c'est ce processus qui est en cours, on le clean + if (currentYtProcess === yt) currentYtProcess = null; reject(err); }); - // Fin du processus yt.on('close', (code) => { - if (code !== 0) { - clog.warn(`[YT-DLP] Arrêt avec code ${code}. Détails : ${errorLogs}`); + // Nettoyage de la référence globale si c'est bien nous + if (currentYtProcess === yt) currentYtProcess = null; + + if (code !== 0 && code !== null && code !== 143 && code !== 137) { // 137 = SIGKILL + clog.warn(`[YT-DLP] Arrêt code ${code}. Logs : ${errorLogs}`); } }); - // Si le flux est créé, on le renvoie immédiatement if (yt.stdout) { - clog.log("[YT-DLP] Flux audio capturé avec succès."); + // --- SÉCURITÉ ANTI-OVERRIDE SUR LE FLUX --- + // Si le stream se ferme (le bot quitte le vocal ou skip), on tue yt-dlp + yt.stdout.on('close', () => { + if (!yt.killed && currentYtProcess === yt) { + yt.kill(); + } + }); + + yt.stdout.on('error', () => { + if (!yt.killed && currentYtProcess === yt) yt.kill(); + }); + resolve(yt.stdout); } else { - reject(new Error("Le processus yt-dlp n'a généré aucun flux.")); + reject(new Error("Aucun flux stdout généré.")); } }); } diff --git a/src/player/Player.js b/src/player/Player.js index 0ddb852..2318f46 100644 --- a/src/player/Player.js +++ b/src/player/Player.js @@ -206,13 +206,13 @@ class Player { plog.log(`GUILD : ${this.guildId} - Lecture de la musique : ${song.title} - Type : ${song.type}`) } - async getStream(song) { + async getStream(song, duration = 0) { let stream = null if(song.type == "attachment") { stream = await media.getStream(song) } if(song.type == 'youtube') { - stream = await youtube.getStream(song) + stream = await youtube.getStream(song, duration) } if(song.type == "soundcloud") { stream = await soundcloud.getStream(song) @@ -284,18 +284,17 @@ class Player { async setDuration(duration) { - //FIXME: SET DURATION FONCTIONNE TRES LENTEMENT if (this.checkConnection()) return; if (this.queue.current == null) return; if (this.currentResource == null) return; const maxDuration = this.queue.current.duration; - if (duration > maxDuration) { + if (duration.time > maxDuration) { plog.error(`GUILD : ${this.guildId} - La durée demandée dépasse la durée maximale de la musique.`); return; } - this.stream = await this.getStream(this.queue.current); + this.stream = await this.getStream(this.queue.current, duration.time); if (this.stream === null) { plog.error(`GUILD : ${this.guildId} - Impossible de lire la musique : ${this.queue.current.title} avec le type : ${this.queue.current.type}`); return; @@ -306,24 +305,13 @@ class Player { if(typeof this.stream === "string") { this.stream = fs.createReadStream(this.stream) } - - const passThroughStream = new PassThrough(); - duration = Math.floor(duration.time); - ffmpeg(this.stream) - .setStartTime(duration) // Démarrer à la position demandée (en secondes) - .outputOptions('-f', 'mp3') // Specify output format if needed - .on('error', (err) => { - plog.error(`GUILD : ${this.guildId} - Une erreur est survenue avec ffmpeg : ${err.message}`); - }) - .pipe(passThroughStream, { end: true }); - - this.stream = passThroughStream; - this.playStream(this.stream); // Jouer le nouveau flux - this.currentResource.playbackDuration = duration * 1000; // Mettre à jour la durée de lecture du resource + this.currentResource.playbackDuration = duration.time * 1000; // Mettre à jour la durée de lecture du resource + console.log(this.currentResource.playbackDuration) + console.log(this.getDuration()) - plog.log(`GUILD : ${this.guildId} - Lecture déplacée à ${duration}s.`); + plog.log(`GUILD : ${this.guildId} - Lecture déplacée à ${duration.time}s.`); } diff --git a/src/playlists/History.js b/src/playlists/History.js index d003be4..1cf041e 100644 --- a/src/playlists/History.js +++ b/src/playlists/History.js @@ -28,7 +28,14 @@ function getPersonalHistory(userId) { */ function addToPersonalHistory(userId, entry) { hlog.log(`Ajout d'une entrée à l'historique personnel de l'utilisateur : ${userId}`); + + // Check if there is already the same entry (by ID) and remove it to avoid duplicates + const history = getPersonalHistory(userId); + const existingIndex = history.findIndex(e => e.id === entry.id); + if (existingIndex !== -1) { + history.splice(existingIndex, 1); + } // Limit to 25 entries if (history.length >= 25) { history.shift(); diff --git a/src/utils/GlobalVars.js b/src/utils/GlobalVars.js index c04a589..9d3f48d 100644 --- a/src/utils/GlobalVars.js +++ b/src/utils/GlobalVars.js @@ -18,7 +18,7 @@ const __glob = { SERVER_DB: root + path.sep + "data" + path.sep + "servers.json", VERSION: version, CHANGELOG_PATH: root + path.sep + "CHANGELOG.html", - COOKIES: root + path.sep + "data" + path.sep + "cookies.json", + COOKIES: root + path.sep + "data" + path.sep + "cookies.txt", PROXY: root + path.sep + "data" + path.sep + "proxy.json" }