Version 1.4.0 - Correction de bugs
This commit is contained in:
21
.gitignore
vendored
21
.gitignore
vendored
@@ -144,4 +144,23 @@ test/
|
|||||||
data/
|
data/
|
||||||
|
|
||||||
__DEBUG.js
|
__DEBUG.js
|
||||||
__TEST.js
|
__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
|
||||||
@@ -1,37 +1,52 @@
|
|||||||
<div class="changelog-version changelog-actual">
|
<div class="changelog-version changelog-actual">
|
||||||
|
<h2>Chopin - Version /*1.4.0*/</h2>
|
||||||
|
<p class="changelog-date">*_Date de sortie_*: *-06/12/2025-*</p>
|
||||||
|
<ul>
|
||||||
|
<li>/#[FIX][BACKEND]#/ Résolution majeure du module Youtube : la lecture de flux est de nouveau pleinement fonctionnelle</li>
|
||||||
|
<li>/#[FIX][BACKEND]#/ Correction d'un bug de duplication des titres dans l'historique de lecture</li>
|
||||||
|
<li>/#[FIX][BACKEND]#/ Correctif de sécurité empêchant l'ajout de fichiers personnels dans les playlists partagées</li>
|
||||||
|
<li>/#[AJOUT][FRONTEND]#/ Ajout de tooltips (infobulles) sur les éléments interactifs pour améliorer l'accessibilité</li>
|
||||||
|
<li>/#[FIX][FRONTEND]#/ Correction de divers bugs visuels mineurs sur l'interface</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="changelog-version">
|
||||||
<h2>Chopin - Version /*1.3.0*/</h2>
|
<h2>Chopin - Version /*1.3.0*/</h2>
|
||||||
<p class="changelog-date">*_Date de sortie_*: *-07/10/2025-*</p>
|
<p class="changelog-date">*_Date de sortie_*: *-07/10/2025-*</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>/#[FIX][BACKEND]#/ Fix de l'erreur sur les fichiers médias importés qui ne fonctionnaient pas</li>
|
<li>/#[FIX][BACKEND]#/ Correction de l'erreur empêchant la lecture des fichiers médias importés</li>
|
||||||
<li>/#[BACKEND]#/ Migration du serveur vers une nouvelle architecture et Docker (instabilité prévu)</li>
|
<li>/#[BACKEND]#/ Migration de l'architecture serveur vers Docker (des instabilités sont à prévoir temporairement)</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="changelog-version">
|
<div class="changelog-version">
|
||||||
<h2>Chopin - Version /*1.2.0*/</h2>
|
<h2>Chopin - Version /*1.2.0*/</h2>
|
||||||
<p class="changelog-date">*_Date de sortie_*: *-07/09/2025-*</p>
|
<p class="changelog-date">*_Date de sortie_*: *-07/09/2025-*</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>/#[AJOUT][FRONTEND]#/ Suggestion de recherche, depuis Youtube</li>
|
<li>/#[AJOUT][FRONTEND]#/ Intégration des suggestions de recherche via YouTube</li>
|
||||||
<li>/#[FIX][FRONTEND]#/ Optimisation des bouttons, avec une modification du composant</li>
|
<li>/#[FIX][FRONTEND]#/ Refonte du composant bouton et optimisation des performances</li>
|
||||||
<li>/#[AJOUT][DISCORD]#/ Ajout de la sécurité, permettant de restreindre l'utilisation du Bot au Rôle paramétré</li>
|
<li>/#[AJOUT][DISCORD]#/ Implémentation du système de sécurité restreignant l'usage du Bot selon les Rôles configurés</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="changelog-version">
|
<div class="changelog-version">
|
||||||
<h2>Chopin - Version /*1.1.0*/</h2>
|
<h2>Chopin - Version /*1.1.0*/</h2>
|
||||||
<p class="changelog-date">*_Date de sortie_*: *-06/09/2025-*</p>
|
<p class="changelog-date">*_Date de sortie_*: *-06/09/2025-*</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>/#[AJOUT][FRONTEND]#/ Désormais, lorsque l'utilisateur n'est pas connecté, les bouttons de lecture sont désactivés au lieu d'être masqués</li>
|
<li>/#[AJOUT][FRONTEND]#/ Désactivation visuelle (plutôt que masquage) des boutons de lecture pour les utilisateurs non connectés</li>
|
||||||
<li>/#[AJOUT][FRONTEND] #/ Ajout de la prise en charge de plusieurs fichiers en même temps (Impossibilité sur Discord à cause de restriction) !</li>
|
<li>/#[AJOUT][FRONTEND]#/ Prise en charge de l'upload multi-fichiers (contournement des restrictions Discord)</li>
|
||||||
<li>/#[FIX][BACKEND]#/ La source Youtube est désormais fonctionnelle</li>
|
<li>/#[FIX][BACKEND]#/ Réparation du connecteur de source Youtube</li>
|
||||||
<li>/#[FIX][BACKEND]#/ Le changement de channel est désormais fonctionnel.</li>
|
<li>/#[FIX][BACKEND]#/ Le changement de salon vocal est désormais opérationnel</li>
|
||||||
<li>/#[FIX][FRONTEND]#/ Résolution d'un problème d'affichage du changelog et de la liste de lecture</li>
|
<li>/#[FIX][FRONTEND]#/ Correction des bugs d'affichage sur le changelog et la liste de lecture</li>
|
||||||
<li>/#[FIX][FRONTEND]#/ Le "En ligne" est désormais resynchronisé sur le Bot</li>
|
<li>/#[FIX][FRONTEND]#/ Resynchronisation correcte du statut "En ligne" du Bot</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="changelog-version">
|
<div class="changelog-version">
|
||||||
<h2>Chopin - Version /*1.0.0*/</h2>
|
<h2>Chopin - Version /*1.0.0*/</h2>
|
||||||
<p class="changelog-date">*_Date de sortie_*: *-29/08/2025-*</p>
|
<p class="changelog-date">*_Date de sortie_*: *-29/08/2025-*</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>/#[FRONTEND]#/ Sortie de la version 1.0.0 de Chopin, le bot Discord pour SubSonics. Refonte graphique et passage sur Vue JS</li>
|
<li>/#[FRONTEND]#/ Lancement officiel de la v1.0.0 de Chopin (Bot pour SubSonics). Refonte graphique complète et migration vers Vue JS</li>
|
||||||
<li>/#[BACKEND]#/ Refonte de toute la gestion de la musique et ajout de nouvelles fonctionnalités</li>
|
<li>/#[BACKEND]#/ Refonte intégrale du moteur audio et implémentation des nouvelles fonctionnalités</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
1
TODOS.md
1
TODOS.md
@@ -1,3 +1,4 @@
|
|||||||
# List
|
# List
|
||||||
|
|
||||||
TODO: Récupération des recommendations, playlists Youtube et Spotify
|
TODO: Récupération des recommendations, playlists Youtube et Spotify
|
||||||
|
TODO: Faire une interface Admin
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "chopin-backend",
|
"name": "chopin-backend",
|
||||||
"version": "1.3.2",
|
"version": "1.4.0",
|
||||||
"description": "Discord Bot for music - Fetching everywhere !",
|
"description": "Discord Bot for music - Fetching everywhere !",
|
||||||
"main": "src/main.js",
|
"main": "src/main.js",
|
||||||
"nodemonConfig": {
|
"nodemonConfig": {
|
||||||
|
|||||||
@@ -60,7 +60,6 @@ class List {
|
|||||||
}
|
}
|
||||||
this.setCurrent(song)
|
this.setCurrent(song)
|
||||||
process.emit("PLAYERS_UPDATE")
|
process.emit("PLAYERS_UPDATE")
|
||||||
//TODO: Check History and continuity
|
|
||||||
return song
|
return song
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,51 +2,146 @@ const { LogType } = require('loguix');
|
|||||||
const clog = new LogType("Youtube-Stream");
|
const clog = new LogType("Youtube-Stream");
|
||||||
const { spawn } = require('child_process');
|
const { spawn } = require('child_process');
|
||||||
|
|
||||||
async function getStream(song) {
|
// Variable globale pour stocker le processus actif
|
||||||
return new Promise((resolve, reject) => {
|
let currentYtProcess = null;
|
||||||
clog.log(`[YT-DLP] Lancement du processus natif pour : ${song.url}`);
|
|
||||||
|
|
||||||
// On lance yt-dlp directement.
|
/**
|
||||||
// ATTENTION : "yt-dlp" doit être reconnu dans ton terminal (installé dans le PATH)
|
* Tue le processus yt-dlp en cours proprement et attend sa fin réelle.
|
||||||
const yt = spawn('yt-dlp', [
|
* 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,
|
song.url,
|
||||||
'-o', '-', // Rediriger le son vers la sortie standard (stdout)
|
'-o', '-',
|
||||||
'-f', 'bestaudio', // Meilleure qualité audio
|
'-f', 'bestaudio[ext=webm]/bestaudio[ext=m4a]/bestaudio',
|
||||||
'--no-warnings', // Masquer les avertissements
|
// NETWORKS
|
||||||
'--no-check-certificate', // Évite les erreurs SSL courantes
|
'--force-ipv4', // INDISPENSABLE : Force la connexion via IPv4 (plus stable pour le streaming)
|
||||||
'--prefer-free-formats' // Préférer Opus/WebM (meilleur pour Discord)
|
'--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 = "";
|
let errorLogs = "";
|
||||||
|
|
||||||
// Capture des erreurs du processus (si yt-dlp râle)
|
|
||||||
yt.stderr.on('data', (data) => {
|
yt.stderr.on('data', (data) => {
|
||||||
// On ignore les infos de progression [download]
|
|
||||||
const msg = data.toString();
|
const msg = data.toString();
|
||||||
if (!msg.includes('[download]')) {
|
console.log(msg);
|
||||||
|
if (!msg.includes('[download]') && !msg.includes('[youtube]')) {
|
||||||
errorLogs += msg;
|
errorLogs += msg;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Gestion des erreurs de lancement (ex: yt-dlp n'est pas installé)
|
|
||||||
yt.on('error', (err) => {
|
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);
|
reject(err);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fin du processus
|
|
||||||
yt.on('close', (code) => {
|
yt.on('close', (code) => {
|
||||||
if (code !== 0) {
|
// Nettoyage de la référence globale si c'est bien nous
|
||||||
clog.warn(`[YT-DLP] Arrêt avec code ${code}. Détails : ${errorLogs}`);
|
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) {
|
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);
|
resolve(yt.stdout);
|
||||||
} else {
|
} else {
|
||||||
reject(new Error("Le processus yt-dlp n'a généré aucun flux."));
|
reject(new Error("Aucun flux stdout généré."));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -206,13 +206,13 @@ class Player {
|
|||||||
plog.log(`GUILD : ${this.guildId} - Lecture de la musique : ${song.title} - Type : ${song.type}`)
|
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
|
let stream = null
|
||||||
if(song.type == "attachment") {
|
if(song.type == "attachment") {
|
||||||
stream = await media.getStream(song)
|
stream = await media.getStream(song)
|
||||||
}
|
}
|
||||||
if(song.type == 'youtube') {
|
if(song.type == 'youtube') {
|
||||||
stream = await youtube.getStream(song)
|
stream = await youtube.getStream(song, duration)
|
||||||
}
|
}
|
||||||
if(song.type == "soundcloud") {
|
if(song.type == "soundcloud") {
|
||||||
stream = await soundcloud.getStream(song)
|
stream = await soundcloud.getStream(song)
|
||||||
@@ -284,18 +284,17 @@ class Player {
|
|||||||
|
|
||||||
async setDuration(duration) {
|
async setDuration(duration) {
|
||||||
|
|
||||||
//FIXME: SET DURATION FONCTIONNE TRES LENTEMENT
|
|
||||||
if (this.checkConnection()) return;
|
if (this.checkConnection()) return;
|
||||||
if (this.queue.current == null) return;
|
if (this.queue.current == null) return;
|
||||||
if (this.currentResource == null) return;
|
if (this.currentResource == null) return;
|
||||||
|
|
||||||
const maxDuration = this.queue.current.duration;
|
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.`);
|
plog.error(`GUILD : ${this.guildId} - La durée demandée dépasse la durée maximale de la musique.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.stream = await this.getStream(this.queue.current);
|
this.stream = await this.getStream(this.queue.current, duration.time);
|
||||||
if (this.stream === null) {
|
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}`);
|
plog.error(`GUILD : ${this.guildId} - Impossible de lire la musique : ${this.queue.current.title} avec le type : ${this.queue.current.type}`);
|
||||||
return;
|
return;
|
||||||
@@ -306,24 +305,13 @@ class Player {
|
|||||||
if(typeof this.stream === "string") {
|
if(typeof this.stream === "string") {
|
||||||
this.stream = fs.createReadStream(this.stream)
|
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.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.`);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,14 @@ function getPersonalHistory(userId) {
|
|||||||
*/
|
*/
|
||||||
function addToPersonalHistory(userId, entry) {
|
function addToPersonalHistory(userId, entry) {
|
||||||
hlog.log(`Ajout d'une entrée à l'historique personnel de l'utilisateur : ${userId}`);
|
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 history = getPersonalHistory(userId);
|
||||||
|
const existingIndex = history.findIndex(e => e.id === entry.id);
|
||||||
|
if (existingIndex !== -1) {
|
||||||
|
history.splice(existingIndex, 1);
|
||||||
|
}
|
||||||
// Limit to 25 entries
|
// Limit to 25 entries
|
||||||
if (history.length >= 25) {
|
if (history.length >= 25) {
|
||||||
history.shift();
|
history.shift();
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ const __glob = {
|
|||||||
SERVER_DB: root + path.sep + "data" + path.sep + "servers.json",
|
SERVER_DB: root + path.sep + "data" + path.sep + "servers.json",
|
||||||
VERSION: version,
|
VERSION: version,
|
||||||
CHANGELOG_PATH: root + path.sep + "CHANGELOG.html",
|
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"
|
PROXY: root + path.sep + "data" + path.sep + "proxy.json"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user