Compare commits

...

32 Commits

Author SHA1 Message Date
914edbbf13 Version 1.1.2-rc3 - Modification Cookie
All checks were successful
Deployment Pipeline / deploy (push) Successful in 40s
2025-08-29 15:25:22 +02:00
c376e3259c Version 1.1.2-rc2 - Ajout Cookies
All checks were successful
Deployment Pipeline / deploy (push) Successful in 35s
2025-08-29 12:44:25 +02:00
f3b237b74f Version 1.1.2-rc1 - Modif Ytdl
All checks were successful
Deployment Pipeline / deploy (push) Successful in 34s
2025-08-29 12:29:02 +02:00
83e11f3341 Merge branch 'main' of https://git.raphix.fr/subsonics/chopin
All checks were successful
Deployment Pipeline / deploy (push) Successful in 35s
2025-08-29 12:26:32 +02:00
b132041d16 Version 1.1.2 - Modification User-Agent 2025-08-29 12:26:25 +02:00
33da8e8527 Version 1.1.1 - Edit Cookies
All checks were successful
Deployment Pipeline / deploy (push) Successful in 36s
2025-08-29 12:01:13 +02:00
c613d67c60 Version 1.1.1 - Modification du Changelog
All checks were successful
Deployment Pipeline / deploy (push) Successful in 35s
2025-08-29 00:23:21 +02:00
48e5bfad60 Version 1.1.1.rc8 - Changement permissions
All checks were successful
Deployment Pipeline / deploy (push) Successful in 33s
2025-08-29 00:07:11 +02:00
54fd731ab9 Version 1.1.1-rc7.1 - Modif Pipeline
All checks were successful
Deployment Pipeline / deploy (push) Successful in 35s
2025-08-28 23:58:19 +02:00
5b8f591216 Version 1.1.1-rc7 - Modification des valeurs Path
Some checks failed
Deployment Pipeline / deploy (push) Failing after 35s
2025-08-28 23:55:51 +02:00
2fe5d35efe Version 1.1.1-rc6.1 - Modif Erreur Pipeline
Some checks failed
Deployment Pipeline / deploy (push) Failing after 40s
2025-08-28 23:47:34 +02:00
b0f5ccbe5a Version 1.1.1-rc6 - Modif Pipeline
Some checks failed
Deployment Pipeline / deploy (push) Failing after 9s
2025-08-28 23:46:27 +02:00
98e5c41fa5 Version 1.1.1-rc5 - Modif Pipeline Final
Some checks failed
Deployment Pipeline / deploy (push) Failing after 27s
2025-08-28 23:32:40 +02:00
f41eddf1dc Version 1.1.1-rc4.1 - Modif Pipeline
Some checks failed
Deployment Pipeline / deploy (push) Failing after 8s
2025-08-28 23:29:50 +02:00
f12bbe8ad2 Version 1.1.1-rc4 - Modif Pipeline
Some checks failed
Deployment Pipeline / deploy (push) Failing after 8s
2025-08-28 23:28:12 +02:00
0c50874644 Version 1.1.1-rc3 - Modification Pipeline
Some checks failed
Deployment Pipeline / deploy (push) Failing after 7s
2025-08-28 23:27:30 +02:00
59ea576181 Version 1.1.1-rc2 - Premier deploy
Some checks failed
Deployment Pipeline / deploy (push) Failing after 7s
2025-08-28 23:20:57 +02:00
8b2728622c Version 1.1.1 - Premier Deploy 2025-08-28 23:15:27 +02:00
e313d4228c Version 1.1.1 - Modification du serveur pour un mode de gestion des utilisateurs plus pratique 2025-07-25 22:57:10 +02:00
98cdae97c0 Version 1.1.0 - Refactor + Intergration Backend 2025-07-25 17:56:30 +02:00
a59d7a66db Version 1.1.0 - BACKEND - Release STABLE - Ajout de l'historique, de l'ajout de playlist privé, lecture de fichier média 2025-05-30 11:37:55 +02:00
e686edb0e6 Version 1.0.2 - STABLE - Backend complété et prêt à l'usage 2025-05-05 19:48:47 +02:00
3aa4201dd2 Version 1.0.1 - Fix de nombreux bugs 2025-05-03 18:26:16 +02:00
812d5c72fa Version 1.0.0-rc1 - Version initiale (Ajout du serveur, des playlists) 2025-04-20 23:09:46 +02:00
12c4e2740a Version 0.4.0-alpha1 2025-03-02 21:37:04 +01:00
407d9d6b9a Merge pull request 'backend-0.2.0 => main' (#1) from backend-0.2.0 into main
Reviewed-on: #1
2025-03-01 17:03:17 +00:00
2a934d14ae Version 0.3.0 - Ajout des premières fonctionnalités du Player 2025-03-01 18:01:17 +01:00
c8c8fd71be Version 0.3.0-alpha1 - Youtube and Spotify support 2025-02-28 19:21:47 +01:00
a060d00599 Version 0.2.0 - Change Youtube Information Program 2025-02-28 19:21:16 +01:00
f99fc24aa9 Version 0.2.0 - Ajout de Player (Media) 2025-02-26 18:02:43 +01:00
5e722e9dbc Version 0.2.0-alpha2 - Test Git 2025-02-25 17:01:21 +01:00
6f3847138b Version 0.2.0-alpha - Ajout du Player 2025-02-25 16:39:18 +01:00
70 changed files with 12485 additions and 1938 deletions

View File

@@ -0,0 +1,86 @@
name: Deployment Pipeline
on:
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Setup SSH
env:
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
run: |
mkdir -p ~/.ssh
echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
eval "$(ssh-agent -s)"
ssh-add ~/.ssh/id_rsa
ssh-keyscan git.raphix.fr >> ~/.ssh/known_hosts
- name: Deploy Subsonics as gitlab-ci
run: |
ssh -A -o StrictHostKeyChecking=no raphix@alpha.raphix.fr << 'EOF'
sudo su - gitlab-ci -c '
set -e
# Variables PM2 et npm
export PM2_HOME=/home/gitlab-ci/.pm2
export NPM_CONFIG_CACHE=/home/gitlab-ci/.npm
mkdir -p $PM2_HOME $NPM_CONFIG_CACHE
chown -R gitlab-ci:gitlab-ci $PM2_HOME $NPM_CONFIG_CACHE
echo "[Subsonics-Deploy] - Stage - Déploiement - START"
echo "[Subsonics-Deploy] - Arrêt de Subsonics : Processing"
cd /home/gitlab-ci
pm2 stop "Subsonics - Backend" || true
pm2 delete "Subsonics - Backend" || true
echo "[Subsonics-Deploy] - Arrêt de Subsonics : Success"
# Préparer tempdata
if [ ! -d "/home/gitlab-ci/backend/data" ]; then
mkdir -p /home/gitlab-ci/backend/data
fi
mv /home/gitlab-ci/backend/data/ /home/gitlab-ci/tempdata || true
echo "[Subsonics-Deploy] - Suppression de Subsonics : Processing"
rm -rf ./backend
echo "[Subsonics-Deploy] - Suppression de Subsonics : Success"
echo "[Subsonics-Deploy] - Installation de Subsonics : Processing"
git clone https://git.raphix.fr/subsonics/chopin backend
echo "[Subsonics-Deploy] - Installation de Subsonics : Success"
echo "[Subsonics-Deploy] - Installation des dépendances : Processing"
cd /home/gitlab-ci/backend
# Nettoyage node_modules et tempdata
rm -rf node_modules
if [ -d "/home/gitlab-ci/tempdata" ]; then
mv /home/gitlab-ci/tempdata/ ./data
fi
# Assurer la propriété gitlab-ci
chown -R gitlab-ci:gitlab-ci /home/gitlab-ci/backend
mkdir -p $NPM_CONFIG_CACHE
chown -R gitlab-ci:gitlab-ci $NPM_CONFIG_CACHE
npm install --omit=dev
echo "[Subsonics-Deploy] - Installation des dépendances : Success"
echo "[Subsonics-Deploy] - Démarrage de Subsonics : Processing"
cd /home/gitlab-ci
pm2 start subsonic.config.js
echo "[Subsonics-Deploy] - Démarrage de Subsonics : Success"
echo "[Subsonics-Deploy] - Stage - Déploiement - END"
'
EOF

5
.gitignore vendored
View File

@@ -141,4 +141,7 @@ docs/_book
# TODO: where does this rule come from?
test/
data/
data/
__DEBUG.js
__TEST.js

9
CHANGELOG.html Normal file
View File

@@ -0,0 +1,9 @@
<div class="changelog-version changelog-actual">
<h2>Chopin - Version /*1.0.0*/</h2>
<p class="changelog-date">*_Date de sortie_*: *-29/08/2025-*</p>
<p>VERSION /*BETA*/</p>
<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>[BACKEND] Refonte de toute la gestion de la musique et ajout de nouvelles fonctionnalités</li>
</ul>
</div>

View File

@@ -1,20 +1,19 @@
# **Subsonics - Chopin**
# **Subsonics - Chopin**
> Cette version est une refonte complète et intégrale de [Subsonics - Web](https://git.raphix.fr/subsonics/web)
### Bienvenue sur Chopin, la nouvelle version de Subsonics.
## Bienvenue sur Chopin, la nouvelle version de Subsonics
### **Fonctionnalités**
## **Fonctionnalités**
> - Lecture de vidéos depuis Youtube, Spotify et SoundClound
> - Lecture de fichiers locaux *(Uniquement sur le site)*
> - Gestion et lecture de playlist
> - Gestion et lecture de playlist
> - Accéder à votre propre historique et à l'historique du Bot
> - Affichage des paroles de la musique en cours
> - Une interface refaite pour toutes les platformes.
> - Récupération de vos recommendations & Playlists Spotify / Youtube
Le FrontEnd est gérée par VueJS ✌et le BackEnd a été entièrement refait localement pour des réponses plus rapide et plus stable
[CHANGELOG](https://git.raphix.fr/subsonics/chopin/src/branch/main/changelog.md)
[CHANGELOG](https://git.raphix.fr/subsonics/chopin/src/branch/main/changelog.md)

3
TODOS.md Normal file
View File

@@ -0,0 +1,3 @@
# List
TODO: Récupération des recommendations, playlists Youtube et Spotify

1717
backend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,30 +0,0 @@
{
"name": "chopin-backend",
"version": "0.1.0",
"description": "Discord Bot for music - Fetching everywhere !",
"main": "src/main.js",
"nodemonConfig": {
"ext": "js, html",
"ignore": [
"*.json",
"*.html"
],
"delay": "2000000"
},
"scripts": {
"start": "nodemon src/main.js"
},
"keywords": [],
"author": "Raphix",
"license": "ISC",
"dependencies": {
"cors": "^2.8.5",
"discord.js": "^14.18.0",
"express": "^4.21.2",
"loguix": "^1.4.2",
"nodemon": "^3.1.9",
"socket.io": "^4.8.1",
"uuid": "^11.1.0",
"webmetrik": "^0.1.4"
}
}

View File

@@ -1,78 +0,0 @@
const { Client, GatewayIntentBits, Collection, ActivityType, REST, Routes } = require("discord.js")
const fs = require("node:fs")
const path = require("path")
const { __glob } = require("../utils/GlobalVars")
const { LogType } = require("loguix")
const config = require("../utils/Database/Configuration")
const metric = require("webmetrik")
const dlog = new LogType("Discord")
const client = new Client({
intents:[GatewayIntentBits.Guilds, GatewayIntentBits.GuildVoiceStates, GatewayIntentBits.GuildMembers],
})
//Getter for the client
function getClient() {
return client
}
function init() {
client.once('ready', () => {
dlog.log("Connexion au Bot Discord réussi ! Connecté en tant que : " + client.user.tag)
const Activity = require("./Activity")
Activity.idleActivity()
const CommandUpdater = require("./CommandUpdater")
CommandUpdater.init()
const commandManager = client.application.commands;
if (!commandManager) {
dlog.error('Command manager not available.');
} else {
commandManager.set([]);
}
dlog.step.end("d_init")
});
client.on("interactionCreate", (interaction) => {
if(!interaction.isCommand()) return;
var numberOfCommands = new metric.Metric("numberOfCommands", "Nombre de commandes éxécutées")
numberOfCommands.setValue(numberOfCommands.getValue() + 1)
const command = client.commands.get(interaction.commandName)
try {
// Create a metric to count the number of commands executed by each user
const userCommand = new metric.Metric("userCommand_" + interaction.member.user.username, "Nombre de commandes éxécutées par l'utilisateur : " + interaction.member.user.username)
userCommand.setValue(userCommand.getValue() + 1)
dlog.log(interaction.member.user.username + "-> /" + interaction.commandName)
command.execute(client, interaction)
} catch(error) {
dlog.error(interaction.member.user.username + "-> /" + interaction.commandName + " : ERREUR RENCONTRE")
dlog.error(error)
interaction.reply({content:"Erreur lors de l'éxécution de la commande !", ephemeral: true})
}
})
client.login(config.getToken())
}
module.exports = {init, getClient}

View File

@@ -1,14 +0,0 @@
const { Command } = require('../Command');
const { Embed } = require('../Embed');
const command = new Command("web", "Affiche le lien vers le site web pour contrôler le bot", (client, interaction) => {
const embed = new Embed()
embed.setColor(0xffffff)
embed.setTitle('Subsonics - Chopin')
embed.addBotPicture(client)
embed.addField('Lien',"https://subsonics.raphix.fr/")
embed.send(interaction)
})
module.exports = {command}

View File

@@ -1,33 +0,0 @@
const {Database} = require("./Database")
const {__glob} = require("../GlobalVars")
const {LogType} = require("loguix")
const path = require("path")
const clog = new LogType("Configuration")
const config = new Database("config", __glob.DATA + path.sep + "config.json", {
token: "",
report: {
channel : "",
contact : ""
}
})
function getToken() {
return config.data.token
}
function getReportChannel() {
return config.data.report.channel
}
function getReportContact() {
return config.data.report.contact
}
if(getToken() == "") {
clog.error("Impossible de démarrer sans token valide")
process.exit(1)
}
module.exports = {getToken, getReportChannel, getReportContact}

View File

@@ -1,14 +0,0 @@
const path = require("path")
const root = path.resolve(__dirname, '../../')
const __glob = {
PACKAGEINFO: root + path.sep + "package.json",
ROOT: root + + path.sep,
SRC: root + path.sep + "src",
LOGS: root + path.sep + "logs",
DATA: root + path.sep + "data",
COMMANDS: root + path.sep + "src" + path.sep + "discord" + path.sep + "commands",
METRIC_FILE: root + path.sep + "data" + path.sep + "metrics.json"
}
module.exports = {__glob}

View File

@@ -1,17 +0,0 @@
# **Changelog**
- Express JS
- Vue JS
- Discord JS
- Player Youtube
## **Légende de version** :
* Version X.Y.Z
> **X** : Indique une version de travail (Période d'activité) \
> **Y** : Indique l'ajout d'une fonctionnalité \
> **Z** : Indique la modification ou la réparation d'une fonctionnalité
* Tags
> **-alpha** : Indique une version de dévelopement inutilisable \
> **-rcX** : Indique une sous-version qui ne modifie rien mais qui peux corriger un bug de facon très superficiel

7124
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

46
package.json Normal file
View File

@@ -0,0 +1,46 @@
{
"name": "chopin-backend",
"version": "1.1.2",
"description": "Discord Bot for music - Fetching everywhere !",
"main": "src/main.js",
"nodemonConfig": {
"ext": "js, html",
"ignore": [
"*.json",
"*.html"
],
"delay": "2000000"
},
"scripts": {
"start": "nodemon src/main.js",
"stop": "ssh raphix@raphix.fr sudo -S -u gitlab-ci pm2 stop 'Subsonics'",
"restart": "ssh raphix@raphix.fr sudo -S -u gitlab-ci pm2 start 'Subsonics'"
},
"keywords": [],
"author": "Raphix",
"license": "ISC",
"dependencies": {
"@discordjs/voice": "^0.18.0",
"@distube/ytdl-core": "^4.16.12",
"cors": "^2.8.5",
"discord-player": "^7.1.0",
"discord.js": "^14.18.0",
"ffmpeg-static": "^5.2.0",
"ffprobe": "^1.1.2",
"ffprobe-static": "^3.1.0",
"fluent-ffmpeg": "^2.1.3",
"googleapis": "^149.0.0",
"libsodium-wrappers": "^0.7.15",
"loguix": "^1.4.2",
"mime-types": "^3.0.1",
"nodemon": "^3.1.10",
"pm2": "^5.4.3",
"socket.io": "^4.8.1",
"soundcloud.ts": "^0.6.3",
"spotify-web-api-node": "^5.0.2",
"uuid": "^11.1.0",
"webmetrik": "^0.1.4",
"yt-search": "^2.13.1",
"ytfps": "^1.2.0"
}
}

View File

@@ -13,10 +13,7 @@ function setMusicActivity(songName, artistName, imageUrl) {
const client = bot.getClient()
client.user.setActivity(`${songName} - ${artistName}`,{
type: ActivityType.Listening,
/*assets: {
largeImage: imageUrl,
largeText: songName
}*/
url: imageUrl
});
dlog.log(`Activité mise à jour : ${songName} - ${artistName}`)
}

224
src/discord/Bot.js Normal file
View File

@@ -0,0 +1,224 @@
const { Client, GatewayIntentBits, Collection, ActivityType, REST, Routes } = require("discord.js")
const fs = require("node:fs")
const path = require("path")
const { __glob } = require("../utils/GlobalVars")
const { LogType } = require("loguix")
const config = require("../utils/Database/Configuration")
const metric = require("webmetrik")
const { Player } = require("../player/Player")
const {refreshAllUserInformation} = require("../server/auth/User")
const dlog = new LogType("Discord")
const glog = new LogType("GuildUpdater")
dlog.log("Initialisation du Bot Discord")
const membersVoices = new Map()
const timers = new Map()
const guilds = new Map()
var operational = false
const client = new Client({
intents:[GatewayIntentBits.Guilds, GatewayIntentBits.GuildVoiceStates, GatewayIntentBits.GuildMembers],
})
//Getter for the client
function getClient() {
return client
}
function getGuilds() {
return guilds
}
function getMembersVoices() {
return membersVoices
}
function isReady() {
return operational
}
function getGuildMembers(guildId) {
const guild = client.guilds.cache.get(guildId)
if(!guild) {
dlog.error("Guild not found: " + guildId)
return []
}
return guild.members.cache.map(member => member.user.id)
}
function getChannel(guildId, channelId) {
return client.guilds.cache.get(guildId).channels.cache.get(channelId)
}
function init() {
client.once('ready', async () => {
dlog.log("Connexion au Bot Discord réussi ! Connecté en tant que : " + client.user.tag)
await refreshGuilds()
await refreshAllUserInformation()
const Activity = require("./Activity")
Activity.idleActivity()
const CommandUpdater = require("./CommandUpdater")
CommandUpdater.init()
const commandManager = client.application.commands
if (!commandManager) {
dlog.error('Command manager not available.')
} else {
commandManager.set([])
}
dlog.step.end("d_init")
operational = true
})
client.on("interactionCreate", (interaction) => {
if(!interaction.isCommand()) return;
var numberOfCommands = new metric.Metric("numberOfCommands", "Nombre de commandes éxécutées")
numberOfCommands.setValue(numberOfCommands.getValue() + 1)
var numberOfCommandsServer = new metric.Metric("numberOfCommands_" + interaction.guild.id, "Nombre de commandes éxécutées sur le serveur : " + interaction.guild.name)
numberOfCommandsServer.setValue(numberOfCommandsServer.getValue() + 1)
const command = client.commands.get(interaction.commandName)
try {
// Create a metric to count the number of commands executed by each user
const userCommand = new metric.Metric("userCommand_" + interaction.member.user.username, "Nombre de commandes éxécutées par l'utilisateur : " + interaction.member.user.username)
userCommand.setValue(userCommand.getValue() + 1)
dlog.log(interaction.member.user.username + "-> /" + interaction.commandName)
command.execute(client, interaction)
} catch(error) {
dlog.error(interaction.member.user.username + "-> /" + interaction.commandName + " : ERREUR RENCONTRE")
dlog.error(error)
interaction.reply({content:"Erreur lors de l'éxécution de la commande !", ephemeral: true})
}
})
client.on("guildMemberAdd", async (member) => {
dlog.log("Nouveau membre dans la guilde : " + member.guild.name + " (" + member.guild.id + ") - Membre : " + member.user.username + " (" + member.user.id + ")")
await refreshGuilds()
process.emit("USERS_UPDATE")
})
client.on("guildMemberRemove", async (member) => {
dlog.log("Membre quitté la guilde : " + member.guild.name + " (" + member.guild.id + ") - Membre : " + member.user.username + " (" + member.user.id + ")")
await refreshGuilds()
membersVoices.delete(member.user.id)
process.emit("USERS_UPDATE")
})
// If a new guild is added, we will add it to the guilds map
client.on("guildCreate", async (guild) => {
await refreshGuilds()
glog.log("Guilde ajoutée : " + guild.name + " (" + guild.id + ")")
process.emit("USERS_UPDATE")
})
client.on("guildDelete", (guild) => {
dlog.log("Guilde supprimée : " + guild.name)
guilds.delete(guild.id)
glog.log("Guilde supprimée : " + guild.name + " (" + guild.id + ")")
process.emit("USERS_UPDATE")
})
client.on('guildUpdate', async () => {
await refreshGuilds()
process.emit("USERS_UPDATE")
})
client.on("voiceStateUpdate", (oldMember, newMember) => {
membersVoices.set(newMember.id, {
guildId: newMember.guild.id,
channelId: newMember.channelId,
})
const player = new Player(newMember.guild.id)
if(player.connection && player.channelId) {
client.channels.fetch(player.channelId).then(channel => {
if(channel.members.size <= 1) {
// If the player is alone in the channel, we will destroy it in 10 minutes
// 10 minutes = 600000 ms
// 10 second = 10000 ms
timers.set(newMember.guild.id, setTimeout(() => {
const getPlayer = new Player(newMember.guild.id)
if(getPlayer.connection && player.channelId) {
getPlayer.leave()
dlog.log("[Automatic Task] Guild Id :" + newMember.guild.id + " - Player supprimé : " + channel.name)
}
}, 600000))
dlog.log("[Automatic Task] Guild Id :" + newMember.guild.id + " - Player supprimé dans 10 minutess : " + channel.name)
} else {
dlog.log("[Automatic Task] Guild Id :" + newMember.guild.id + " - Player n'est pas seul dans le channel : " + channel.name)
clearTimeout(timers.get(newMember.guild.id))
timers.delete(newMember.guild.id)
}
})
}
process.emit("VOCAL_UPDATE")
})
client.login(config.getToken())
}
async function refreshGuilds() {
glog.step.init("d_refresh_guilds", "Rafraichissement des guildes")
await client.guilds.fetch()
for(const guild of client.guilds.cache.values()) {
await guild.members.fetch()
var allMembersOfGuild = guild.members.cache.map(member => member.user.id)
const missingPermissions = checkRequiredPermission(guild.members.me)
if(missingPermissions.length > 0) {
dlog.error("Le bot n'a pas les permissions nécessaires pour rejoindre la guilde : " + guild.name)
guild.leave()
return
}
guilds.set(guild.id, {
id: guild.id,
name: guild.name,
allMembers: allMembersOfGuild,
icon: guild.iconURL(),
banner: guild.bannerURL(),
description: guild.description,
features: guild.features,
owner: guild.ownerId,
joinedAt: guild.joinedAt,
createdAt: guild.createdAt,
})
glog.log("Guilde rafraichie : " + guild.name + " (" + guild.id + ")")
}
glog.step.end("d_refresh_guilds")
}
function checkRequiredPermission(guildMember) {
const requiredPermissions = [
'ViewChannel', // Voir les salons
'SendMessages', // Envoyer des messages texte
'ReadMessageHistory', // Lire lhistorique des messages
'Connect', // Se connecter aux salons vocaux
'Speak' // Parler dans les salons vocaux
]
return requiredPermissions.filter(permission => !guildMember.permissions.has(permission));
}
module.exports = {init, getClient, getGuilds, getMembersVoices, getChannel, getGuildMembers, isReady}

26
src/discord/Button.js Normal file
View File

@@ -0,0 +1,26 @@
const { ButtonBuilder, ButtonStyle } = require('discord.js');
class Button extends ButtonBuilder {
constructor(label, customId, style = ButtonStyle.Primary, link = null) {
super()
.setLabel(label)
if (link) {
this.setURL(link);
this.setStyle(ButtonStyle.Link);
} else{
this.setCustomId(customId)
}
this.setStyle(style);
}
setDisabled(disabled) {
return this.setDisabled(disabled);
}
setEmoji(emoji) {
return this.setEmoji(emoji);
}
}
module.exports = { Button };

View File

@@ -53,10 +53,18 @@ class Command {
})
SlashCommand.addStringOption(option => option.setName(SelOption.name).setDescription(SelOption.description).setRequired(SelOption.required).addChoices(choices))
}
if(SelOption.type === "FILE") {
SlashCommand.addAttachmentOption(option => option.setName(SelOption.name).setDescription(SelOption.description).setRequired(SelOption.required))
}
})
}
/**
* @type {SlashCommandBuilder}
* @param {Client} client
* @param {Interaction} interaction
*/
this.data = {data: SlashCommand, async execute(client, interaction) {callback(client, interaction)}}
}

View File

@@ -50,10 +50,6 @@ client.commands = new Collection()
}
dlog.step.end("d_commands_refresh")
} catch (error) {
// And of course, make sure you catch and log any errors!

View File

@@ -10,8 +10,8 @@ const command = new Command("about", "Affiche des informations sur le bot", (cli
const minutes = Math.floor((uptime % 3600) / 60);
const seconds = Math.floor(uptime % 60);
const embed = new Embed()
embed.setColor(0xb0f542)
const embed = new Embed(interaction)
embed.setColor(237, 12, 91)
embed.setThumbnail("https://cdn.discordapp.com/avatars/" + client.user.id + "/" + client.user.avatar + ".png")
embed.setTitle('Subsonics - Chopin')
embed.addField('Informations',"")
@@ -20,15 +20,18 @@ const command = new Command("about", "Affiche des informations sur le bot", (cli
embed.addField("Ping", `${client.ws.ping} ms `, true)
embed.addField("Réalisé par", "Raphix - 2025", true)
embed.addColumn()
embed.addField('Versions',"")
embed.addField('Versions :',"")
embed.addField('Node.js', process.version,true)
embed.addField('Discord.js', packageJson.dependencies["discord.js"].replace("^", ""),true)
embed.addColumn()
embed.addField('Webmetrik', packageJson.dependencies["webmetrik"].replace("^", ""),true)
embed.addField('Loguix', packageJson.dependencies["loguix"].replace("^", ""),true)
embed.addColumn()
embed.addField('FFmpeg', packageJson.dependencies["ffmpeg-static"].replace("^", ""),true)
embed.addField('Ytdl', packageJson.dependencies["@distube/ytdl-core"].replace("^", ""),true)
embed.addColumn()
embed.send(interaction)
embed.send()
})

View File

@@ -3,7 +3,7 @@ const { Embed } = require('../Embed');
const command = new Command("help", "Affiche la liste des commandes", (client, interaction) => {
const embed = new Embed()
const embed = new Embed(interaction)
embed.setColor(0x03ff2d)
embed.setTitle('Comment assister au concert ?')
embed.setDescription("**Eh ! Tu as eu ton ticket ? Tant mieux ! Voici la liste des commandes à utiliser dans le salon prévu à cet effet !**")
@@ -18,7 +18,7 @@ const command = new Command("help", "Affiche la liste des commandes", (client, i
option.choices.forEach(choice => {
choices.push(choice.name)
})
CommandName += " " + choices.join(" | ")
CommandName += " <" + choices.join(" | ") +">"
}
})
}
@@ -28,7 +28,7 @@ const command = new Command("help", "Affiche la liste des commandes", (client, i
})
embed.addField("La queue et la gestion du redémarrage se fait par le site https://subsonics.raphix.fr/", ":star:" )
embed.setThumbnail("https://static.wikia.nocookie.net/codelyoko/images/9/95/Subdigitals.jpg/revision/latest/scale-to-width-down/180?cb=20120105180510&path-prefix=fr");
embed.send(interaction)
embed.send()
})
module.exports = {command}

View File

@@ -0,0 +1,20 @@
const {Command } = require("../Command")
const {Embed, EmbedError} = require("../Embed")
const {Button} = require("../Button")
const command = new Command("invite", "Invite moi sur d'autres serveurs", (client, interaction) => {
const embed = new Embed(interaction)
embed.setColor(0xFF007F)
embed.setTitle('**Inviter le bot sur d\'autres serveurs**')
embed.setDescription('Vous pouvez m\'inviter sur d\'autres serveurs en cliquant sur le bouton ci-dessous.')
embed.addBotPicture(client)
const linkButton = new Button("Invite", null, 5, "https://discord.com/oauth2/authorize?client_id=" + client.user.id + "&scope=bot+applications.commands&permissions=8")
embed.addButton(linkButton)
embed.send()
})
module.exports = {command}

View File

@@ -0,0 +1,34 @@
const {Command} = require("../Command")
const {Embed, EmbedError} = require("../Embed")
const {Player, AllPlayers} = require("../../player/Player")
const command = new Command("leave", "Quitter le salon vocal", (client, interaction) => {
if(!interaction.member.voice.channel) return new EmbedError("Vous devez rejoindre un salon vocal pour arrêter le bot !", interaction)
const channel = interaction.member.voice.channel
var embed = new Embed(interaction)
if(AllPlayers.has(channel.guildId)) {
const player = AllPlayers.get(channel.guildId)
if(!player?.connected) {
return embed.returnError("Le bot n'est pas connecté à ce salon vocal")
}
player.leave()
embed.setColor(200, 20, 20)
embed.setTitle('**Déconnexion**')
embed.setDescription('Déconnexion du salon vocal')
embed.setThumbnail("https://www.iconsdb.com/icons/download/white/phone-51-64.png")
embed.send()
} else {
embed.returnError("Le bot n'est pas connecté à ce salon vocal")
}
})
module.exports = {command}

View File

@@ -0,0 +1,55 @@
const {Command} = require('../Command');
const {Embed, EmbedError} = require('../Embed');
const { Player } = require('../../player/Player');
const { Song } = require('../../player/Song');
const history = require('../../playlists/History');
const command = new Command("media", "Lire un média MP3/WAV dans un salon vocal", async (client, interaction) => {
if(!interaction.member.voice.channel) return new EmbedError("Vous devez rejoindre un salon vocal pour jouer un média !", interaction, true)
const media = interaction.options.get("media")
const now = interaction.options.getBoolean("now") || false
if(media.attachment.contentType != "audio/mpeg" && media.attachment.contentType != "audio/wav") return new EmbedError("Le média doit être un fichier audio MP3 ou WAV !", interaction)
const embed = new Embed(interaction)
embed.setColor(0x15e6ed)
const channel = interaction.member.voice.channel
const song = new Song()
await song.processMedia(media, interaction.user.username)
const player = new Player(channel.guildId)
player.join(channel)
if(now) {
player.play(song)
embed.setTitle('**Lecture immédiate**')
} else {
player.add(song)
embed.setTitle('**Ajout à liste de lecture**')
}
history.addToPersonalHistory(interaction.user.id, song)
embed.setDescription('**Titre : **' + song.title)
embed.addField('**Durée : **', song.readduration)
embed.addField("**Artiste : **",song.author)
embed.addField('**Demandé par **' + interaction.member.user.username, "")
embed.setThumbnail(song.thumbnail)
embed.send()
}, [{type: "FILE", name: "media", description: "Fichier audio à lire", required: true},
{type:"BOOLEAN", name: "now", description: "Lire le média maintenant", required: false}]
)
module.exports = {command}

View File

@@ -0,0 +1,44 @@
const {Command} = require("../Command")
const {Embed, EmbedError} = require("../Embed")
const { Player } = require("../../player/Player")
const command = new Command("pause", "Mettre en pause / Reprendre la musique en cours", (client, interaction) => {
if(!interaction.member.voice.channel) return new EmbedError("Vous devez rejoindre un salon vocal pour mettre en pause la musique !", interaction)
const channel = interaction.member.voice.channel
const player = new Player(channel.guildId)
const result = player.pause()
var embed = new Embed(interaction)
embed.setColor(0x03ff2d)
result.then((pause) => {
if(pause == "no_music") {
embed.returnError("Il n'y a pas de musique en cours de lecture")
} else if(pause) {
embed.setTitle('Musique en pause')
embed.setDescription("La musique a été mise en pause")
embed.setThumbnail("https://www.iconsdb.com/icons/download/white/pause-64.png")
} else {
embed.setTitle('Musique reprise')
embed.setDescription("La musique a été reprise")
embed.setThumbnail("https://www.iconsdb.com/icons/download/white/play-64.png")
}
embed.send()
})
// Réponse en embed
})
module.exports = {command}

View File

@@ -0,0 +1,93 @@
const { Command } = require("../Command");
const { Embed, EmbedError } = require("../Embed");
const { Player } = require("../../player/Player");
const Finder = require("../../player/Finder");
const { Playlist } = require("../../playlists/Playlist");
const spotify = require("../../media/SpotifyInformation");
const history = require("../../playlists/History");
const command = new Command("play", "Jouer une musique à partir d'un lien dans un salon vocal", async (client, interaction) => {
if(!interaction.member.voice.channel) return new EmbedError("Vous devez rejoindre un salon vocal pour jouer une musique !", interaction)
const url = interaction.options.get("url")
const channel = interaction.member.voice.channel
const now = interaction.options.getBoolean("now") || false
const embed = new Embed(interaction)
await Finder.search(url.value).then(async (song) => {
if(!song) return embed.returnError("Impossible de trouver la musique à partir du lien donné ou des mots clés donnés")
const player = new Player(channel.guildId)
player.join(channel)
embed.setColor(0x15e6ed)
// Check if song is playlist
if(song instanceof Playlist) {
embed.setDescription('**Playlist : **' + song.songs.length + ' musiques')
embed.addField('**Titre : **' + song.title, "")
embed.addField('**Demandé par : **', interaction.member.user.username,)
embed.addField('**Auteur : **', song.author)
embed.addField('**Provient de : **', song.type.replace(/^\w/, (c) => c.toUpperCase()))
if(!song.type == "spotify") {
embed.addField('**Durée : **', song.readduration)
}
embed.addField('**Lien : **', song.url)
embed.addField(":warning: La récupération des musiques peut prendre du temps", "Veuillez patienter ... et éviter de lancer d'autres commandes")
embed.setThumbnail(song.thumbnail)
} else {
embed.setDescription('**Titre : **' + song.title)
embed.addField('**Durée : **', song.readduration)
embed.addField("**Artiste : **",song.author)
embed.addField('**Demandé par **' + interaction.member.user.username, "")
embed.addField("**Lien :** ", song.url)
embed.setThumbnail(song.thumbnail)
}
if(now) {
embed.setTitle("Lecture immédiate")
} else {
embed.setTitle("Ajoutée à la file d'attente")
}
embed.send()
if(song instanceof Playlist) {
if(song.type == "spotify") {
song = await spotify.getTracks(song)
}
player.readPlaylist(song, now)
} else {
if(now) {
player.play(song)
} else {
player.add(song)
}
history.addToPersonalHistory(interaction.user.id, song)
}
})
}, [{type: "STRING", name: "url", description: "Recherche / Lien audio (Youtube / Soundclound / Spotify)", required: true},
{type:"BOOLEAN", name: "now", description: "Lire le média maintenant", required: false}]
)
module.exports = {command}

View File

@@ -0,0 +1,51 @@
const {Command} = require("../Command")
const {Embed, EmbedError} = require("../Embed")
const { Player, AllPlayers } = require("../../player/Player")
const command = new Command("previous", "Passe à la musique précédente", (client, interaction) => {
if(!interaction.member.voice.channel) return new EmbedError("Vous devez rejoindre un salon vocal pour passer à la musique suivante !", interaction)
const channel = interaction.member.voice.channel
var embed = new Embed(interaction)
if(AllPlayers.has(channel.guildId)) {
const player = new Player(channel.guildId)
const result = player.previous()
embed.setColor(0x15e6ed)
result.then((song) => {
if(song == "no_music") {
embed.returnError("Il n'y a pas de musique précédemment jouée")
} else if(song) {
// Result is a song
embed.setTitle('**Musique précédente !**')
embed.setDescription('**Titre : **' + song.title)
embed.addField('**Durée : **'+ song.readduration, "")
embed.addField("**Artiste : **" + song.author, "")
embed.setThumbnail(song.thumbnail)
embed.send()
}
})
} else {
return embed.returnError("Le bot n'est pas connecté", interaction)
}
})
module.exports = {command}

View File

@@ -0,0 +1,43 @@
const {Command} = require("../Command")
const {Embed, EmbedError} = require("../Embed")
const { Player, AllPlayers } = require("../../player/Player")
const command = new Command("liste", "Affiche la file d'attente", (client, interaction) => {
const channel = interaction.member.voice.channel
var embed = new Embed(interaction)
if(AllPlayers.has(channel.guildId)) {
const player = new Player(channel.guildId)
const queue = player.queue.getNext()
if(queue.length == 0) {
embed.returnError("Il n'y a pas de musique en file d'attente")
} else if(queue.length > 0) {
// Result is a song
embed.setColor(0x15e6ed)
embed.setThumbnail("https://www.iconsdb.com/icons/download/white/list-2-64.png")
embed.setTitle('**File d\'attente :**')
embed.setDescription('**' + queue.length + ' musiques**')
queue.forEach((song, index) => {
// max 24 fields
if(index > 10) return
embed.addField(`**${index+1} - ${song.title}**`, `**Durée : **${song.readduration}\n**Artiste : **${song.author}`)
})
embed.send()
}
} else {
embed.returnError("Le bot n'est pas connecté")
}
})
module.exports = {command}

View File

@@ -5,7 +5,7 @@ const { Report } = require('../ReportSender');
const command = new Command("report", "Signaler un problème avec le bot", (client, interaction) => {
const report = new Report(interaction.user.username, interaction.options.getString("type"), interaction.options.getString("description"))
const result = report.send()
const embed = new Embed()
const embed = new Embed(interaction)
result.then((res) => {
@@ -20,7 +20,7 @@ const command = new Command("report", "Signaler un problème avec le bot", (clie
embed.setDescription("Votre rapport a bien été envoyé !")
}
embed.send(interaction)
embed.send()
})

View File

@@ -0,0 +1,27 @@
const {Embed} = require("../Embed")
const {Command} = require("../Command")
const {restart} = require("../../utils/Maintenance")
const users = require("../../server/auth/User")
// Nécéssite une raison pour redémarrer le bot
const command = new Command("restart", "Redémarre le bot", (client, interaction) => {
// Check if user is admin from users list
const user = users.getUserById(interaction.user.id)
if(!user || !user.isAdmin()) {
interaction.reply({content: "Vous n'êtes pas admin", ephemeral: true})
return
}
const reason = interaction.options.getString("reason")
restart(reason)
const embed = new Embed(interaction)
embed.setColor(150, 20, 20)
embed.setTitle('Redémarrage')
embed.setDescription("Veuillez patientez, le bot va redémarrer dans un instant ! :arrows_counterclockwise:")
embed.addField('Raison', reason)
embed.send()
},
[{type: "STRING", name: "reason", description: "Raison du redémarrage", required: true}]
)
module.exports = {command}

View File

@@ -0,0 +1,48 @@
const {Command} = require("../Command")
const {Embed, EmbedError} = require("../Embed")
const { Player, AllPlayers } = require("../../player/Player")
const command = new Command("skip", "Passe à la musique suivante", (client, interaction) => {
if(!interaction.member.voice.channel) return new EmbedError("Vous devez rejoindre un salon vocal pour passer à la musique suivante !", interaction)
const channel = interaction.member.voice.channel
var embed = new Embed(interaction)
if(AllPlayers.has(channel.guildId)) {
const player = new Player(channel.guildId)
const result = player.skip()
embed.setColor(0x15e6ed)
result.then((song) => {
if(song == "no_music") {
embed.returnError("Il n'y a pas de musique en file d'attente", interaction)
} else if(song) {
// Result is a song
embed.setColor(0x15e6ed)
embed.setTitle('**Musique suivante !**')
embed.setDescription('**Titre : **' + song.title)
embed.addField('**Durée : **'+ song.readduration, "")
embed.addField("**Artiste : **" + song.author, "")
embed.setThumbnail(song.thumbnail)
}
embed.send()
})
} else {
return embed.returnError("Le bot n'est pas connecté", interaction)
}
})
module.exports = {command}

View File

@@ -0,0 +1,34 @@
const {Command} = require("../Command")
const {Embed, EmbedError} = require("../Embed")
const {Player} = require("../../player/Player")
const command = new Command("state", "Affiche la musique en cours", (client, interaction) => {
const channel = interaction.member.voice.channel
const player = new Player(channel.guildId)
const song = player.queue.getCurrent()
if(!song) {
var embed = new EmbedError("Il n'y a pas de musique en cours de lecture", interaction)
} else if(song) {
var embed = new Embed(interaction)
// Result is a song
embed.setColor(0x15e6ed)
embed.setTitle('**Musique en cours :**')
embed.setDescription('**Titre : **' + song.title)
embed.addField('**Durée : **', song.readduration)
embed.addField("**Artiste : **", song.author)
embed.setThumbnail(song.thumbnail)
embed.send()
}
})
module.exports = {command}

View File

@@ -0,0 +1,20 @@
const { Command } = require('../Command');
const { Button } = require('../Button');
const { Embed } = require('../Embed');
const config = require('../../utils/Database/Configuration')
const command = new Command("web", "Affiche le lien vers le site web pour contrôler le bot", (client, interaction) => {
const embed = new Embed(interaction)
embed.setColor(0xffffff)
embed.setTitle('Subsonics - Chopin')
embed.addBotPicture(client)
embed.setDescription('Vous pouvez contrôler le bot depuis le site web ! \n Nécéssite une connexion avec votre compte Discord.')
const linkButton = new Button("Site web", null, 5, config.getWebsiteLink())
embed.addButton(linkButton)
embed.send()
})
module.exports = {command}

View File

@@ -1,10 +1,20 @@
const { EmbedBuilder } = require("discord.js");
const { EmbedBuilder, ActionRowBuilder } = require("discord.js");
class Embed {
fields;
constructor() {
buttons;
constructor (interaction, ephemeral) {
this.embed = new EmbedBuilder().setTimestamp()
this.fields = []
this.buttons = []
this.isSended = false
if(interaction) {
interaction.deferReply({ ephemeral: ephemeral }).then(() => {
this.isSended = true
})
this.interaction = interaction
this.ephemeral = ephemeral
}
}
setTitle(title) {
@@ -75,15 +85,48 @@ class Embed {
return this
}
addButton(button) {
this.buttons.push(button)
return this
}
build() {
//Add Fields to an object
this.embed.addFields(this.fields)
if(this.buttons.length > 0) {
this.actionRow = new ActionRowBuilder()
.addComponents(this.buttons);
}
return this.embed
}
send(interaction) {
interaction.reply({embeds: [this.build()]})
async send() {
// Add a secutiry check to avoid sending an embed if the interaction is not defined and retry one again
while(!this.isSended) {
await new Promise(resolve => setTimeout(resolve, 50));
}
if(this.ephemeral === undefined) this.ephemeral = false;
this.interaction.editReply({ embeds: [this.build()], components: this.buttons.length > 0 ? [this.actionRow] : [] })
}
async returnError(message) {
this.setColor(150, 20, 20)
this.setTitle('Erreur')
this.setThumbnail("https://upload.wikimedia.org/wikipedia/commons/thumb/9/97/Dialog-error-round.svg/2048px-Dialog-error-round.svg.png")
this.setDescription(message)
await this.send()
}
}
module.exports = {Embed}
class EmbedError extends Embed {
constructor(message, interaction, ephemeral) {
super(interaction, ephemeral)
this.setColor(150, 20, 20)
this.setTitle('Erreur')
this.setThumbnail("https://upload.wikimedia.org/wikipedia/commons/thumb/9/97/Dialog-error-round.svg/2048px-Dialog-error-round.svg.png")
this.setDescription(message)
this.send()
}
}
module.exports = {Embed, EmbedError}

158
src/discord/MediaBase.js Normal file
View File

@@ -0,0 +1,158 @@
const {LogType} = require("loguix")
const config = require("../utils/Database/Configuration")
const wlog = new LogType("MediaBase")
const { Database } = require("../utils/Database/Database")
const {__glob} = require("../utils/GlobalVars")
const { AttachmentBuilder } = require("discord.js")
const { Song } = require("../player/Song")
const { getMediaInformationFromUrl } = require("../media/MediaInformation")
const discordBot = require("./Bot")
var connected = false
var mediaDB = new Database("media", __glob.MEDIA_DB, [])
wlog.step.init("init_db", "Initialisation de la base de données multimédia")
if(!config.getMediaGuildId() || !config.getMediaChannelId()) {
wlog.warn("La configuration de la base de données multimédia n'est pas définie, vérifiez le fichier de configuration.")
wlog.step.error("init_db","Impossible d'initialiser la base de données multimédia, vérifiez le fichier de configuration.")
}
var channel = null
discordBot.getClient().on("ready", () => {
try {
channel = discordBot.getChannel(config.getMediaGuildId(), config.getMediaChannelId())
} catch (e) {
}
if(!channel) {
wlog.warn("Le canal multimédia n'existe pas, vérifiez le fichier de configuration.")
wlog.step.error("init_db","Impossible d'initialiser la base de données multimédia, vérifiez le fichier de configuration.")
}
try {
const dateTime = new Date()
const date = dateTime.toLocaleDateString('fr-FR', { timeZone: 'Europe/Paris' })
const time = dateTime.toLocaleTimeString('fr-FR', { timeZone: 'Europe/Paris' })
const message = `[LOGS] La base de données multimédia a été initialisée le ${date} à ${time}`
channel.send(message)
wlog.log("La base de données multimédia a été initialisée avec succès.")
wlog.step.end("init_db")
connected = true
} catch (e) {
wlog.error("Impossible d'envoyer un message au canal multimédia, vérifiez le fichier de configuration.")
wlog.step.error("init_db","Impossible d'envoyer un message au canal multimédia, vérifiez le fichier de configuration.")
connected = false
}
})
// SEND FILE TO DISCORD AND GET THE URL ID
async function postMedia(file, userId) {
if(!connected) {
wlog.error("La base de données multimédia n'est pas connectée, impossible d'envoyer le fichier.")
return null
}
try {
const attachment = new AttachmentBuilder(file.file)
attachment.setName(file.name) // Set the name of the file
attachment.setDescription("Fichier envoyé par Subsonics Chopin - Raphix")
const message = await channel.send({ files: [attachment] })
const url = message.attachments.first().url
wlog.log(`Fichier envoyé avec succès : ${url}`)
// add the file to the database
mediaDB.data.push({
id: message.id,
url: url,
name: file.name,
size: file.size,
createdAt: new Date().toISOString(),
userId: userId
})
mediaDB.save()
return url
} catch (error) {
wlog.error(`Erreur lors de l'envoi du fichier : ${error.message}`)
return null
}
}
async function getAllMedia(userId) {
if(!connected) {
wlog.error("La base de données multimédia n'est pas connectée, impossible de récupérer les fichiers.")
return []
}
const allSongs = mediaDB.data.filter(m => m.userId === userId)
const songs = []
for(const songDB of allSongs) {
const song = new Song()
const information = await getMediaInformationFromUrl(song, songDB.url)
if(!information) {
mediaDB.data = mediaDB.data.filter(m => m.id !== songDB.id)
mediaDB.save()
continue
}
song.type = "attachment"
song.author = songDB.author
song.createdAt = songDB.createdAt
song.author = songDB.userId
song.title = songDB.name
song.id = songDB.id
song.url = songDB.url
songs.push(song)
}
return songs
}
async function getMedia(id) {
if(!connected) {
wlog.error("La base de données multimédia n'est pas connectée, impossible de récupérer le fichier.")
return null
}
const media = mediaDB.data.find(m => m.id === id)
if(!media) {
wlog.error(`Aucun média trouvé avec l'ID : ${id}`)
return null
}
try {
return media.url
} catch (error) {
wlog.error(`Erreur lors de la récupération du média : ${error.message}`)
return null
}
}
function deleteMedia(data, userId) {
if(!connected) {
wlog.error("La base de données multimédia n'est pas connectée, impossible de supprimer le fichier.")
return false
}
const mediaIndex = mediaDB.data.findIndex(m => m.id === data.id && m.userId === userId)
if(mediaIndex === -1) {
wlog.error(`Aucun média trouvé avec l'ID : ${data.id} pour l'utilisateur : ${userId}`)
return false
}
mediaDB.data.splice(mediaIndex, 1)
mediaDB.save()
return true
}
module.exports = {
postMedia,
getMedia,
deleteMedia,
getAllMedia
}

View File

@@ -46,6 +46,7 @@ class Report {
}
}
module.exports = {Report}

58
src/lyrics/Lyrics.js Normal file
View File

@@ -0,0 +1,58 @@
const { LogType } = require("loguix");
const plog = new LogType('Lyrics');
const urls = require('./urls.json');
// Make sure Url exists and get lyrics for the first item only
async function getLyrics(name) {
let result = null;
try {
const searchResponse = await fetch(`${urls.urlSearch}${encodeURIComponent(name)}`, {
method: 'GET',
headers: {
'content-type': 'application/json'
}
});
const searchData = await searchResponse.json();
// Check if data exists and has at least one item
if (searchData && searchData.data && searchData.data.length > 0) {
const firstItem = searchData.data[0];
const artist = firstItem.artist && firstItem.artist.name ? firstItem.artist.name : null;
const title = firstItem.title || null;
if (artist && title) {
try {
const lyricsResponse = await fetch(`${urls.urlGet}${encodeURIComponent(artist)}/${encodeURIComponent(title)}`, {
method: 'GET',
headers: {
'content-type': 'application/json'
}
});
const lyricsData = await lyricsResponse.json();
console.log(lyricsData);
if (lyricsData && lyricsData && lyricsData.lyrics) {
result = lyricsData.lyrics;
} else {
plog.error('Invalid response structure:', lyricsData);
return null;
}
} catch (error) {
plog.error('Error fetching lyrics data:', error);
return null;
}
} else {
plog.error('Artist or title missing in search result');
return null;
}
} else {
plog.error('No search results found');
return null;
}
} catch (error) {
plog.error('Error fetching search data:', error);
return null;
}
return result;
}
module.exports = { getLyrics };

4
src/lyrics/urls.json Normal file
View File

@@ -0,0 +1,4 @@
{
"urlSearch": "http://api.deezer.com/search?limit=5&q=",
"urlGet": "https://api.lyrics.ovh/v1/"
}

View File

@@ -5,20 +5,20 @@
*/
const { LogType } = require('loguix');
const { __glob } = require("./utils/GlobalVars")
require("loguix").setup(__glob.LOGS, __glob.PACKAGEINFO)
const config = require("./utils/Database/Configuration")
const metric = require("webmetrik")
metric.setMetricFile(__glob.METRIC_FILE)
metric.publishMetrics("8001", "raphraph")
metric.publishMetrics("8001", "subsonicsMetricsRaph")
// SETUP
// SETUP
setup();
function setup() {
async function setup() {
const DiscordBot = require("./discord/Bot")
DiscordBot.init()
await DiscordBot.init()
const Server = require("./server/Server")
await Server.init()
}

View File

@@ -0,0 +1,58 @@
const ffprobe = require('ffprobe');
const ffprobeStatic = require('ffprobe-static');
const { getReadableDuration } = require('../utils/TimeConverter');
const clog = require("loguix").getInstance("Song")
async function getMediaInformation(instance, media, provider) {
try {
const info = await ffprobe(media.attachment.url, { path: ffprobeStatic.path });
if (info.streams?.[0]?.duration_ts) {
instance.duration = info.streams[0].duration;
instance.readduration = getReadableDuration(instance.duration)
}
// Vérification pour éviter une erreur si `streams[0]` ou `tags` n'existe pas
instance.thumbnail = info.streams?.[0]?.tags?.thumbnail ??
"https://radomisol.fr/wp-content/uploads/2016/08/cropped-note-radomisol-musique.png";
// Obtenir le titre (sinon utiliser le nom du fichier)
instance.title = info.streams?.[0]?.tags?.title ?? media.attachment.name;
// Obtenir l'auteur (s'il existe)
instance.author = info.streams?.[0]?.tags?.artist ?? instance.author;
return true;
} catch (err) {
clog.error("Impossible de récupérer les informations de la musique : " + media.attachment.name)
clog.error(err)
return null;
}
}
async function getMediaInformationFromUrl(instance, url) {
try {
const info = await ffprobe(url, { path: ffprobeStatic.path });
if (info.streams?.[0]?.duration_ts) {
instance.duration = info.streams[0].duration;
instance.readduration = getReadableDuration(instance.duration);
}
// Vérification pour éviter une erreur si `streams[0]` ou `tags` n'existe pas
instance.thumbnail = info.streams?.[0]?.tags?.thumbnail ??
"https://radomisol.fr/wp-content/uploads/2016/08/cropped-note-radomisol-musique.png";
// Obtenir le titre (sinon utiliser le nom du fichier)
instance.title = info.streams?.[0]?.tags?.title ?? "Titre inconnu";
// Obtenir l'auteur (s'il existe)
instance.author = info.streams?.[0]?.tags?.artist ?? "Auteur inconnu";
return true;
} catch (err) {
clog.error("Impossible de récupérer les informations de la musique depuis l'URL : " + url);
console.log(err)
clog.error(err);
return null;
}
}
module.exports = {getMediaInformation, getMediaInformationFromUrl};

View File

@@ -0,0 +1,85 @@
const {LogType} = require('loguix');
const clog = new LogType("SoundcloudInformation");
const {Song} = require('../player/Song');
const {Playlist} = require('../playlists/Playlist');
const {Soundcloud} = require('soundcloud.ts')
const {getReadableDuration} = require('../utils/TimeConverter');
const soundcloud = new Soundcloud();
async function getTrack(url) {
try {
const info = await soundcloud.tracks.get(url)
if(!info) {
clog.error("Impossible de récupérer les informations de la piste Soundcloud à partir de l'URL");
return null;
}
const song = new Song();
song.title = info.title;
song.author = info.user.username;
song.url = info.permalink_url;
song.thumbnail = info.artwork_url;
song.id = info.id;
song.duration = info.duration / 1000;
song.readduration = getReadableDuration(info.duration / 1000);
song.type = "soundcloud";
return song;
} catch (error) {
clog.error('Erreur lors de la recherche Soundcloud (Track): ' + error);
return null;
}
}
async function getPlaylist(url) {
try {
const info = await soundcloud.playlists.get(url)
if(!info) {
clog.error("Impossible de récupérer les informations de la playlist Soundcloud à partir de l'URL");
return null;
}
const playlist = new Playlist();
playlist.title = info.title;
playlist.author = info.user.username;
playlist.url = info.permalink_url;
playlist.thumbnail = info.artwork_url;
playlist.id = info.id;
playlist.duration = 0;
playlist.songs = [];
playlist.type = "soundcloud";
for(const track of info.tracks) {
const song = new Song();
song.title = track.title;
song.author = track.user.username;
song.url = track.permalink_url;
song.thumbnail = track.artwork_url;
song.id = track.id;
song.duration = track.duration / 1000;
song.readduration = getReadableDuration(track.duration / 1000);
song.type = "soundcloud";
playlist.duration += track.duration / 1000;
playlist.songs.push(song);
}
playlist.readduration = getReadableDuration(playlist.duration);
return playlist;
} catch (error) {
clog.error('Erreur lors de la recherche Soundcloud (Playlist): ' + error);
return null;
}
}
module.exports = {getTrack, getPlaylist}

View File

@@ -0,0 +1,179 @@
const {LogType} = require('loguix');
const clog = new LogType("SpotifyInformation");
const config = require('../utils/Database/Configuration');
const SPOTIFY_CLIENT_ID = config.getSpotifyClientId()
const SPOTIFY_CLIENT_SECRET = config.getSpotifyClientSecret()
const SpotifyWebApi = require('spotify-web-api-node');
const {Playlist} = require('../playlists/Playlist');
const {Song} = require('../player/Song');
const youtube = require("../media/YoutubeInformation");
const {getReadableDuration} = require('../utils/TimeConverter');
const spotifyApi = new SpotifyWebApi({
clientId: SPOTIFY_CLIENT_ID,
clientSecret: SPOTIFY_CLIENT_SECRET,
});
async function getSong(url) {
try {
const data = await spotifyApi.clientCredentialsGrant();
spotifyApi.setAccessToken(data.body['access_token']);
const parts = url.split('/');
const trackId = parts[parts.length - 1];
if(!trackId) {
clog.error("Impossible de récupérer l'identifiant de la piste Spotify à partir de l'URL");
return null;
}
const trackInfo = await spotifyApi.getTrack(trackId);
const trackName = trackInfo.body.name;
const artistName = trackInfo.body.artists[0].name;
return `${trackName} - ${artistName}`;
} catch (error) {
clog.error("Impossible de récupérer les informations de la piste Spotify à partir de l'URL");
clog.error(error);
return null;
}
}
async function getAlbum(url) {
try {
const creditdata = await spotifyApi.clientCredentialsGrant();
spotifyApi.setAccessToken(creditdata.body['access_token']);
const parts = url.split('/');
const albumId = parts[parts.indexOf('album') + 1].split('?')[0];
const data = await spotifyApi.getAlbum(albumId);
const info = data.body;
if(!info) {
clog.error("Impossible de récupérer les informations de l'album Spotify à partir de l'URL");
return null;
}
clog.log("Informations de l'album récupérées : " + info.name);
const playlist = new Playlist()
playlist.title = info.name;
playlist.author = info.artists[0].name;
playlist.authorId = info.artists[0].id;
playlist.thumbnail = info.images[0].url;
playlist.url = info.external_urls.spotify;
playlist.id = albumId;
playlist.type = "spotify";
playlist.songs = info.tracks.items;
return playlist;
} catch (error) {
clog.error("Impossible de récupérer les informations de l'album Spotify à partir de l'URL");
clog.error(error);
return null;
}
}
async function getPlaylist(url) {
// Get the playlist and return a Playlist Object
try {
const creditdata = await spotifyApi.clientCredentialsGrant();
spotifyApi.setAccessToken(creditdata.body['access_token']);
const parts = url.split('/');
const playlistId = parts[parts.indexOf('playlist') + 1].split('?')[0];
const data = await spotifyApi.getPlaylist(playlistId)
const info = data.body;
if(!info) {
clog.error("Impossible de récupérer les informations de la playlist Spotify à partir de l'URL");
return null;
}
clog.log("Informations de la playlist récupérées : " + info.name);
const playlist = new Playlist()
playlist.title = info.name;
playlist.author = info.owner.display_name;
playlist.authorId = info.owner.id;
playlist.thumbnail = info.images[0].url;
playlist.url = info.external_urls.spotify;
playlist.id = playlistId;
playlist.type = "spotify";
for(const track of info.tracks.items) {
playlist.songs.push(track.track);
}
return playlist;
} catch (error) {
clog.error("Impossible de récupérer les informations de l'album Spotify à partir de l'URL");
clog.error(error);
return null;
}
}
async function getTracks(playlist) {
const tracks = playlist.songs
playlistSongs = [];
for(const track of tracks) {
var trackName = track.name;
var artistName = track.artists[0].name;
var queryForYoutube = `${trackName} - ${artistName}`;
var urlYoutubeFounded = await youtube.getQuery(queryForYoutube).then(function(songFind) {
if(!songFind) return null;
return songFind.url;
});
clog.log("URL de la vidéo YouTube trouvée : " + urlYoutubeFounded);
if(!urlYoutubeFounded) {
clog.error("Impossible de récupérer l'URL de la vidéo YouTube à partir de la requête " + queryForYoutube);
} else {
const song = new Song();
song.title = track.name;
song.author = track.artists[0].name;
song.url = urlYoutubeFounded;
song.thumbnail = playlist.thumbnail;
song.id = track.id;
song.duration = track.duration_ms / 1000;
song.readduration = getReadableDuration(track.duration_ms / 1000);
song.type = "youtube";
playlist.duration += track.duration_ms / 1000;
playlistSongs.push(song);
}
// When finish do this
}
playlist.readduration = getReadableDuration(playlist.duration);
playlist.songs = playlistSongs;
return playlist;
}
module.exports = {getSong, getAlbum, getPlaylist, getTracks}

View File

@@ -0,0 +1,164 @@
const { LogType } = require('loguix');
const clog = new LogType("YoutubeInformation");
const { Song } = require('../player/Song');
const { Playlist } = require('../playlists/Playlist');
const { getReadableDuration, getSecondsDuration } = require('../utils/TimeConverter');
const yts = require("yt-search")
const ytfps = require('ytfps');
async function getQuery(query, multiple) {
if (!query || typeof query !== 'string') {
clog.error("Impossible de rechercher une vidéo YouTube, car la requête est nulle");
return null;
}
try {
const limit = multiple ? 25 : 1;
const searchResults = await yts({ query: query, limit: limit });
const videos = searchResults.videos;
if (videos.length === 0) {
clog.error("Impossible de récupérer le lien de la vidéo YouTube à partir de la requête");
return null;
}
const songs = await Promise.all(videos.map(video => new Song().processYoutubeVideo(video)));
return multiple ? songs.filter(song => song !== null) : songs[0];
} catch (error) {
clog.error('Erreur lors de la recherche YouTube: ' + error);
return null;
}
}
async function getVideo(url) {
const videoId = url.match(/(?:youtu\.be\/|youtube\.com\/|music\.youtube\.com\/)(?:watch\?v=)?([a-zA-Z0-9_-]{11})/);
if (videoId === null) {
clog.error("Impossible de récupérer l'identifiant de la vidéo YouTube à partir de l'URL");
return null;
}
try {
const video = await yts({videoId: videoId[1]});
if (video) {
const songReturn = new Song();
await songReturn.processYoutubeVideo(video);
return songReturn;
} else {
clog.error("Impossible de récupérer la vidéo YouTube à partir de l'identifiant");
return null;
}
} catch (error) {
clog.error('Erreur lors de la recherche de la vidéo YouTube:' + error);
return null;
}
}
async function getPlaylist(url) {
if (url === null || typeof url !== 'string') {
clog.error("Impossible de rechercher une playlist YouTube, car la requête est nulle");
return null;
}
try {
// If watch?v= is present in the url with list?=, remove it and the code behind and transform it to playlist?list=
var playlistId;
// Get &list= in the url until the first & or ?
if (url.includes("list=")) {
playlistId = url.match(/(list=)([a-zA-Z0-9_-]+)/);
}
if (playlistId === null) {
clog.error("Impossible de récupérer l'identifiant de la playlist YouTube à partir de l'URL");
return null;
}
const playlistInfo = await yts({ listId: playlistId[2] });
if (!playlistInfo) {
clog.error("Impossible de récupérer la playlist YouTube à partir de l'identifiant");
return null;
}
const playlist = new Playlist();
playlist.type = "youtube";
playlist.author = playlistInfo.author.name;
playlist.authorId = playlistInfo.author.url;
playlist.authorAvatar = await getYouTubeProfilePicture(playlistInfo.author.url);
playlist.title = playlistInfo.title;
playlist.thumbnail = playlistInfo.thumbnail;
playlist.url = `https://www.youtube.com/playlist?list=${playlistId[2]}`;
playlist.id = playlistInfo.listId;
playlist.views = playlistInfo.views;
for (const video of playlistInfo.videos) {
const song = new Song();
await song.processYoutubeVideo(video);
playlist.duration += song.duration;
playlist.songs.push(song);
}
playlist.readduration = getReadableDuration(playlist.duration);
return playlist;
} catch (error) {
clog.error('Erreur lors de la recherche YouTube: ' + error);
return null;
}
}
async function getSecondsFromUrl(url) {
const videoId = url.match(/(?:youtu\.be\/|youtube\.com\/|music\.youtube\.com\/)(?:watch\?v=)?([a-zA-Z0-9_-]{11})/);
if (videoId === null) {
clog.error("Impossible de récupérer l'identifiant de la vidéo YouTube à partir de l'URL");
return null;
}
try {
const video = await yts({ videoId: videoId[1] });
if (video) {
return video.duration.seconds;
} else {
clog.error("Impossible de récupérer la vidéo YouTube à partir de l'identifiant");
return null;
}
} catch (error) {
clog.error('Erreur lors de la recherche de la vidéo YouTube:' + error);
return null;
}
}
async function getYouTubeProfilePicture(channelUrl) {
try {
const res = await fetch(channelUrl, {
headers: {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"
}
});
const html = await res.text();
// Match img with yt-spec-avatar-shape__image in class list
const imgRegex = /<img[^>]*(?:class="[^"]*\byt-spec-avatar-shape__image\b[^"]*"[^>]*|[^>]*class="[^"]*\byt-spec-avatar-shape__image\b[^"]*")[^>]*src="([^"]+)"/i;
const imgMatch = html.match(imgRegex);
if (imgMatch && imgMatch[1]) {
return imgMatch[1];
}
// Fallback: look for avatar in embedded JSON
const jsonRegex = /"avatar":\{"thumbnails":\[\{"url":"(.*?)"/;
const match = html.match(jsonRegex);
if (match && match[1]) {
return match[1].replace(/\\u0026/g, "&"); // Decode \u0026 to &
}
console.warn("Photo non trouvée pour :", channelUrl);
return null;
} catch (err) {
console.error("Erreur :", err);
return null;
}
}
module.exports = { getQuery, getVideo, getPlaylist, getSecondsFromUrl };

82
src/player/Finder.js Normal file
View File

@@ -0,0 +1,82 @@
const Resolver = require('../utils/Resolver');
const { QueryType } = require('../utils/QueryType');
const { Links } = require('../utils/Links');
const youtube = require("../media/YoutubeInformation")
const spotify = require("../media/SpotifyInformation")
const soundcloud = require("../media/SoundcloudInformation")
async function search(query, multiple, forceType) {
if(!query) return null
if(!multiple) multiple = false
if(!forceType) forceType = null
if(forceType == "PLAYLIST") {
if(query.includes("spotify")) {
return await spotify.getPlaylist(query)
} else if(query.includes("soundcloud")) {
return await soundcloud.getPlaylist(query)
} else if(query.includes("youtube")) {
return await youtube.getPlaylist(query)
} else {
return null
}
}
if(forceType == "SONG") {
if(query.includes("spotify")) {
return await spotify.getSong(query)
} else if(query.includes("soundcloud")) {
return await soundcloud.getTrack(query)
} else if(query.includes("youtube")) {
return await youtube.getQuery(query, multiple)
} else {
return null
}
}
if(forceType == "ALBUM") {
if(query.includes("spotify")) {
return await spotify.getAlbum(query)
} else if(query.includes("youtube")) {
return await youtube.getQuery(query, multiple)
} else {
return null
}
}
const type = Resolver.getQueryType(query)
if(type == QueryType.YOUTUBE_SEARCH) {
return await youtube.getQuery(query, multiple)
}
if(type == QueryType.YOUTUBE_VIDEO) {
return await youtube.getVideo(query)
}
if(type == QueryType.YOUTUBE_PLAYLIST) {
return await youtube.getPlaylist(query)
}
if(type == QueryType.SPOTIFY_SONG) {
return await youtube.getQuery(await spotify.getSong(query))
}
if(type == QueryType.SPOTIFY_ALBUM) {
return await spotify.getAlbum(query)
}
if(type == QueryType.SPOTIFY_PLAYLIST) {
return await spotify.getPlaylist(query)
}
if(type == QueryType.SOUNDCLOUD_TRACK) {
return await soundcloud.getTrack(query)
}
if(type == QueryType.SOUNDCLOUD_PLAYLIST) {
return await soundcloud.getPlaylist(query)
}
//MORELATER: Add more providers
}
module.exports = {search}

240
src/player/List.js Normal file
View File

@@ -0,0 +1,240 @@
const { Database } = require('../utils/Database/Database')
const { __glob } = require('../utils/GlobalVars')
const PreviousDB = new Database("previous", __glob.PREVIOUSFILE, {})
const {LogType} = require("loguix")
const clog = new LogType("List")
const { Song } = require('./Song')
const AllLists = new Map() // Map<guildId, List>
class List {
next;
current;
shuffle;
guildId;
constructor(guildId) {
if(guildId === null) {
clog.error("Impossible de créer une liste, car guildId est null")
return
}
if(AllLists.has(guildId)) {
return AllLists.get(guildId)
}
// Add PreviousDB.data[this.guildId]
if(PreviousDB.data[guildId] === undefined) {
PreviousDB.data[guildId] = new Array()
savePrevious()
}
AllLists.set(guildId, this)
this.next = new Array();
this.current = null;
this.shuffle = false;
this.guildId = guildId;
}
getNext() {
return this.next;
}
getNextSong() {
if(this.next.length > 0) {
return this.next[0];
} else {
return null;
}
}
nextSong() {
if(this.current != null) {
this.addPreviousSong(this.current)
}
var song = null;
if(!this.shuffle) {
song = this.next[0]
this.next.splice(0, 1)
} else {
const randomIndex = Math.floor(Math.random() * this.next.length);
song = this.next[randomIndex]
this.next.splice(randomIndex, 1)
}
this.setCurrent(song)
process.emit("PLAYERS_UPDATE")
//TODO: Check History and continuity
return song
}
clearNext() {
this.next = new Array();
process.emit("PLAYERS_UPDATE")
}
addNextSong(song) {
this.next.push(song)
process.emit("PLAYERS_UPDATE")
}
firstNext(song) {
this.next.unshift(song)
process.emit("PLAYERS_UPDATE")
}
removeNextByIndex(index) {
this.next.splice(index, 1)
process.emit("PLAYERS_UPDATE")
}
moveSongToUpNext(index) {
const song = this.next[index]
this.next.splice(index, 1)
this.next.unshift(song)
process.emit("PLAYERS_UPDATE")
}
getPrevious() {
const previousList = new Array()
for(const song of PreviousDB.data[this.guildId]) {
previousList.push(new Song(song))
}
return previousList;
}
getPreviousSong() {
if(PreviousDB.data[this.guildId].length > 0) {
return new Song(PreviousDB.data[this.guildId][0])
} else {
return null;
}
}
previousSong() {
if(this.current != null) {
this.firstNext(this.current)
}
if(PreviousDB.data[this.guildId].length > 0) {
const song = PreviousDB.data[this.guildId][0]
// Remove the song from the previous list
PreviousDB.data[this.guildId].splice(0, 1)
savePrevious()
return new Song(song)
} else {
return null;
}
}
clearPrevious() {
PreviousDB.data[this.guildId] = new Array();
savePrevious();
process.emit("PLAYERS_UPDATE")
}
addPreviousSongToNextByIndex(index) {
const song = PreviousDB.data[this.guildId][index]
this.next.push(song)
process.emit("PLAYERS_UPDATE")
}
addPreviousSong(song) {
PreviousDB.data[this.guildId].unshift(song)
savePrevious()
process.emit("PLAYERS_UPDATE")
}
getCurrent() {
return this.current;
}
setCurrent(value) {
this.current = value;
process.emit("PLAYERS_UPDATE")
}
destroy() {
this.clearNext();
this.current = null
this.shuffle = false;
AllLists.delete(this.guildId)
process.emit("PLAYERS_UPDATE")
}
setShuffle() {
this.shuffle = !this.shuffle;
}
isShuffle() {
return this.shuffle;
}
// Play the song with the index in the queue and delete it from the queue
playByIndex(index, typelist) {
var index = data[0]
var list = data[1]
if(typelist == ListType.NEXT) {
const song = this.next[index]
this.next.splice(index, 1)
return song
} else if(typelist == ListType.PREVIOUS) {
const song = PreviousDB.data[this.guildId][index]
return song
}
process.emit("PLAYERS_UPDATE")
}
addNextPlaylist(playlist, firstAlreadyPlayed) {
if(firstAlreadyPlayed) {
playlist.songs.shift()
}
for(const song of playlist.songs) {
this.addNextSong(song)
}
process.emit("PLAYERS_UPDATE")
}
moveNext(fromIndex, toIndex) {
// Check if fromIndex and toIndex are valid
if(fromIndex < 0 || fromIndex >= this.next.length + 1 || toIndex < 0 || toIndex >= this.next.length + 1) {
clog.error("Impossible de déplacer la musique, car l'index est invalide, GuildId : " + this.guildId)
return
}
if(fromIndex == toIndex) return;
const song = this.next[fromIndex]
this.next.splice(fromIndex, 1)
this.next.splice(toIndex, 0, song)
process.emit("PLAYERS_UPDATE")
}
}
const ListType = {
NEXT: "0",
PREVIOUS: "1"
}
function savePrevious() {
for(const guildId in PreviousDB.data) {
if(PreviousDB.data[guildId].length > 50) {
PreviousDB.data[guildId].splice(50, PreviousDB.data[guildId].length - 50)
}
}
PreviousDB.save();
}
module.exports = { List, ListType }

View File

@@ -0,0 +1,21 @@
const {createAudioResource, VoiceConnectionStatus, createAudioPlayer, StreamType} = require('@discordjs/voice');
const {LogType} = require('loguix')
const clog = new LogType("Media")
const plog = require("loguix").getInstance("Player")
const ffmpeg = require('fluent-ffmpeg')
async function getStream(song) {
try {
return song.url;
} catch(e) {
clog.error("Erreur lors de la lecture de la musique : " + song.title)
clog.error(e)
}
}
module.exports = {getStream}

View File

@@ -0,0 +1,25 @@
const {createAudioResource, VoiceConnectionStatus, createAudioPlayer, StreamType} = require('@discordjs/voice');
const {LogType} = require('loguix')
const clog = new LogType("Soundcloud-Stream")
const {Soundcloud} = require('soundcloud.ts')
const ffmpeg = require('fluent-ffmpeg')
const soundcloud = new Soundcloud();
async function getStream(song) {
try {
var stream = await soundcloud.util.streamTrack(song.url)
return stream
} catch(e) {
clog.error("Erreur lors de la récupération du stream : " + song.title)
clog.error(e)
}
}
module.exports = {getStream}

View File

@@ -0,0 +1,52 @@
const {createAudioResource, VoiceConnectionStatus, createAudioPlayer, StreamType} = require('@discordjs/voice');
const {LogType} = require('loguix')
const clog = new LogType("Youtube-Stream")
const ytdl = require('@distube/ytdl-core')
const { getRandomIPv6 } = require("@distube/ytdl-core/lib/utils");
const { __glob } = require('../../utils/GlobalVars');
const fs = require('fs');
async function getStream(song) {
// FIXME: Change youtube provider
try {
var cookies = await JSON.parse(await fs.readFileSync(__glob.COOKIES, 'utf-8'));
function cookiesArrayToHeaderString(cookies) {
return cookies.map(cookie => `${cookie.name}=${cookie.value}`).join('; ');
}
const headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) ' +
'AppleWebKit/537.36 (KHTML, like Gecko) ' +
'Chrome/116.0.5845.97 Safari/537.36',
'Accept-Language': 'en-US,en;q=0.9',
'Cookie': cookiesArrayToHeaderString(cookies)
};
let stream = ytdl(song.url, {
quality: 'highestaudio',
highWaterMark: 1 << 30,
liveBuffer: 20000,
dlChunkSize: 0,
bitrate: 128,
requestOptions: {
headers: headers,
}
});
return stream
} catch(e) {
clog.error("Erreur lors de la récupération du stream : " + song.title)
clog.error(e)
}
}
module.exports = {getStream}

435
src/player/Player.js Normal file
View File

@@ -0,0 +1,435 @@
const { joinVoiceChannel, getVoiceConnection, VoiceConnectionStatus, createAudioPlayer, AudioPlayerStatus, StreamType, createAudioResource } = require('@discordjs/voice');
const {List} = require('./List')
const {LogType} = require("loguix");
const songCheck = require('./SongCheck')
const ffmpeg = require('fluent-ffmpeg')
const fs = require('fs')
const { PassThrough } = require('stream');
const { Metric } = require('webmetrik')
const plog = new LogType("Player")
const clog = new LogType("Signal")
const media = require('./Method/Media');
const youtube = require('./Method/Youtube');
const soundcloud = require('./Method/Soundcloud');
const AllPlayers = new Map()
class Player {
connection;
connected = false;
player;
guildId;
channelId;
channelName;
queue;
currentResource;
loop = false;
constructor(guildId) {
if(this.guildId === null) {
clog.error("Impossible de créer un Player, car guildId est null")
return
}
if(AllPlayers.has(guildId)) {
return AllPlayers.get(guildId)
}
this.connection = null
this.player = null
this.guildId = guildId
this.queue = new List(guildId)
AllPlayers.set(guildId, this)
}
async join(channel) {
if(getVoiceConnection(channel.guild.id)) {
clog.log(`GUILD : ${this.guildId} - Une connexion existe déjà pour ce serveur`)
return
}
this.joinChannel(channel)
this.player = createAudioPlayer()
this.generatePlayerEvents()
}
isConnected() {
return this.connected
}
joinChannel(channel) {
this.channelId = channel.id
this.channelName = channel.name
this.connection = joinVoiceChannel({
channelId: channel.id,
guildId: channel.guild.id,
adapterCreator: channel.guild.voiceAdapterCreator,
selfDeaf: false,
selfMute: false
});
this.connection.on('stateChange', (oldState, newState) => {
clog.log(`GUILD : ${this.guildId} - [STATE] OLD : "${oldState.status}" NEW : "${newState.status}"`);
// Si la connection est fermée, on détruit le player
if(newState.status === VoiceConnectionStatus.Disconnected) {
this.leave()
}
});
this.connected = true
AllPlayers.set(this.guildId, this)
process.emit("PLAYERS_UPDATE")
}
generatePlayerEvents() {
const Activity = require('../discord/Activity');
this.player.on('error', error => {
plog.error(`GUILD : ${this.guildId} - Une erreur est survenue dans le player`);
plog.error(error);
console.error(error);
process.emit("PLAYERS_UPDATE")
});
this.player.on(AudioPlayerStatus.Idle, () => {
if(this.checkConnection()) return
// Si la musique est en boucle, on relance la musique
if(this.loop) {
this.play(this.queue.current)
return
}
// Si la musique n'est pas en boucle, on passe à la musique suivante
Activity.idleActivity()
this.queue.setCurrent(null)
if(this.queue.next.length > 0) {
this.play(this.queue.nextSong())
}
process.emit("PLAYERS_UPDATE")
});
this.player.on(AudioPlayerStatus.Playing, () => {
if(this.checkConnection()) return
plog.log(`GUILD : ${this.guildId} - Le player est en train de jouer le contenu suivant : ${this.queue.current.title}`);
Activity.setMusicActivity(this.queue.current.title, this.queue.current.author, this.queue.current.thumbnail)
process.emit("PLAYERS_UPDATE")
});
}
checkConnection() {
if(this.connection === null) {
clog.error(`GUILD : ${this.guildId} - La connection n'est pas définie`)
return true
}
if(this.player === null) {
plog.error(`GUILD : ${this.guildId} - Le player n'est pas défini`)
return true
}
}
getState() {
const playerStatus = this.player?.state?.status ?? false;
const connectionStatus = this.connection?.state?.status ?? false;
const state = {
current: this.queue.current,
next: this.queue.next,
previous: this.queue.getPrevious(),
loop: this.loop,
shuffle: this.queue.shuffle,
paused: playerStatus === AudioPlayerStatus.Paused,
playing: playerStatus === AudioPlayerStatus.Playing,
duration: this.getDuration(),
playerState: playerStatus,
connectionState: connectionStatus,
channelId: this.channelId,
channelName: this.channelName,
guildId: this.guildId,
}
return state
}
async setLoop() {
if(this.checkConnection()) return
this.loop = !this.loop
if(this.loop) {
plog.log(`GUILD : ${this.guildId} - La musique est en boucle`)
} else {
plog.log(`GUILD : ${this.guildId} - La musique n'est plus en boucle`)
}
process.emit("PLAYERS_UPDATE")
}
async setShuffle() {
if(this.checkConnection()) return
this.queue.shuffle = !this.queue.shuffle
if(this.queue.shuffle) {
plog.log(`GUILD : ${this.guildId} - La musique est en mode aléatoire`)
} else {
plog.log(`GUILD : ${this.guildId} - La musique n'est plus en mode aléatoire`)
}
process.emit("PLAYERS_UPDATE")
}
async play(song) {
if(!songCheck.checkSong(song)) return
if(this.checkConnection()) return
if(this.queue.current != null) {
this.player.stop()
}
var numberOfMusicPlayedPerServer = new Metric("numberOfMusicPlayed_" + this.guildId, "Nombre de musiques jouées sur le serveur : " + this.guildId)
numberOfMusicPlayedPerServer.setValue(numberOfMusicPlayedPerServer.getValue() + 1)
var numberOfSecondsPlayedPerServer = new Metric("numberOfSecondsPlayed_" + this.guildId, "Temps jouée sur le serveur : " + this.guildId)
numberOfSecondsPlayedPerServer.setValue(numberOfSecondsPlayedPerServer.getValue() + song.duration)
this.queue.setCurrent(song)
this.stream = await this.getStream(song)
if(this.stream === null) {
plog.error(`GUILD : ${this.guildId} - Impossible de lire la musique : ${song.title} avec le type : ${song.type}`)
return
}
this.playStream(this.stream)
plog.log(`GUILD : ${this.guildId} - Lecture de la musique : ${song.title} - Type : ${song.type}`)
}
async getStream(song) {
let stream = null
if(song.type == "attachment") {
stream = await media.getStream(song)
}
if(song.type == 'youtube') {
stream = await youtube.getStream(song)
}
if(song.type == "soundcloud") {
stream = await soundcloud.getStream(song)
}
return stream
}
async add(song) {
if(this.player?.state?.status == AudioPlayerStatus.Idle && this.queue.current === null && this.queue.next.length === 0) {
this.play(song)
return
}
this.queue.addNextSong(song)
plog.log(`GUILD : ${this.guildId} - La musique a été ajoutée à la liste de lecture : ${song.title}`)
}
async readPlaylist(playlist, now) {
if(this.player?.state?.status == AudioPlayerStatus.Idle && this.queue.current === null && this.queue.next.length === 0) {
this.play(playlist.songs[0])
this.queue.addNextPlaylist(playlist, true)
return
}
if(now) this.play(playlist.songs[0])
this.queue.addNextPlaylist(playlist, now)
plog.log(`GUILD : ${this.guildId} - La playlist a été ajoutée à la liste de lecture : ${playlist.title}`)
}
async pause() {
if(this.checkConnection()) return "no_music"
if(this.player.state.status == AudioPlayerStatus.Paused) {
this.player.unpause()
plog.log(`GUILD : ${this.guildId} - La musique a été reprise`)
process.emit("PLAYERS_UPDATE")
return false
} else {
this.player.pause()
plog.log(`GUILD : ${this.guildId} - La musique a été mise en pause`)
process.emit("PLAYERS_UPDATE")
return true
}
}
async leave() {
const Activity = require('../discord/Activity');
if(this.checkConnection()) return
if(this.queue.current != null) {
this.queue.addPreviousSong(this.queue.current)
}
// Détruit la connection et le player et l'enlève de la liste des
this.connection.destroy()
this.player.stop()
this.player = null
this.connection = null
this.channelId = null
this.channelName = null
this.connected = false
Activity.idleActivity()
this.queue.destroy()
AllPlayers.delete(this.guildId)
clog.log("Connection détruite avec le guildId : " + this.guildId)
plog.log("Player détruit avec le guildId : " + this.guildId)
process.emit("PLAYERS_UPDATE")
}
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) {
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);
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;
}
// Si stream est un lien, ouvrir le stream à partir du lien
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
plog.log(`GUILD : ${this.guildId} - Lecture déplacée à ${duration}s.`);
}
playStream(stream) {
if(this.checkConnection()) return
if(this.player !== null) this.player.stop();
this.player = createAudioPlayer()
this.generatePlayerEvents()
const resource = createAudioResource(stream, { inputType: StreamType.Arbitrary });
this.setCurrentResource(resource)
this.player.play(resource);
this.connection.subscribe(this.player);
process.emit("PLAYERS_UPDATE")
}
getDuration() {
// Return the duration of player
if(this.checkConnection()) return
if(this.queue.current == null) return
if(this.currentResource == null) return
return this.currentResource.playbackDuration / 1000
}
setCurrentResource(value) {
this.currentResource = value;
}
changeChannel(channel) {
if(this.checkConnection()) return
if(this.connection === null) return
if(this.connection.channelId === channel.id) return
this.connection.destroy()
this.joinChannel(channel)
// Si la musique est en cours de lecture, on la relance avec le bon timecode
if(this.player) {
this.connection.subscribe(this.player);
}
process.emit("PLAYERS_UPDATE")
}
async skip() {
if(this.checkConnection()) return "no_music"
if(this.queue.next.length === 0) {
return "no_music"
}
const songSkip = this.queue.nextSong()
this.play(songSkip)
process.emit("PLAYERS_UPDATE")
return songSkip
}
async previous() {
if(this.checkConnection()) return "no_music"
if(this.queue.getPrevious().length === 0) {
return "no_music"
}
const songPrevious = this.queue.previousSong()
this.play(songPrevious)
process.emit("PLAYERS_UPDATE")
return songPrevious
}
}
/**
*
* @param {string} guildId
* @returns {Player} player
*/
function getPlayer(guildId) {
if(AllPlayers.has(guildId)) {
return AllPlayers.get(guildId)
} else {
return new Player(guildId)
}
}
function getAllPlayers() {
const players = new Array()
AllPlayers.forEach((player) => {
players.push(player)
})
return players
}
function isPlayer(guildId) {
return AllPlayers.has(guildId)
}
module.exports = {Player, AllPlayers, getPlayer, isPlayer, getAllPlayers}
/*
You can access created connections elsewhere in your code without having to track the connections yourself. It is best practice to not track the voice connections yourself as you may forget to clean them up once they are destroyed, leading to memory leaks.
const connection = getVoiceConnection(myVoiceChannel.guild.id);
*/

70
src/player/Song.js Normal file
View File

@@ -0,0 +1,70 @@
const {LogType} = require('loguix')
const clog = new LogType("Song")
const MediaInformation = require('../media/MediaInformation')
const { getReadableDuration, getSecondsDuration } = require('../utils/TimeConverter');
class Song {
title = "Aucun titre";
id = "Aucun fichier";
author = "Auteur inconnu"
authorId;
url;
thumbnail = "https://radomisol.fr/wp-content/uploads/2016/08/cropped-note-radomisol-musique.png" ;
duration;
readduration;
form = "SONG";
type;
userAddedId;
constructor(properties) {
if(properties) {
this.type = properties.type ?? this.type
this.title = properties.title ?? this.title
this.id = properties.id ?? this.id
this.author = properties.author ?? this.author
this.url = properties.url ?? this.url
this.thumbnail = properties.thumbnail ?? this.thumbnail
this.duration = properties.duration ?? this.duration
this.readduration = properties.readduration ?? this.readduration
this.type = properties.type ?? this.type
this.authorId = properties.authorId ?? this.authorId
}
}
async processMedia(media, provider) {
if(provider) this.author = provider;
if(provider) this.authorId = provider;
// Check if media is a file or a link
if(media.attachment) {
this.url = media.attachment.url
this.id = media.attachment.name
this.type = "attachment"
// In face, duration is null, get the metadata of the file to get the duration
await MediaInformation.getMediaInformation(this, media)
} else {
clog.error("Impossible de traiter le média")
}
}
async processYoutubeVideo(video) {
this.title = video.title
this.author = video.author.name
this.authorId = video.author.url
this.thumbnail = video.thumbnail
this.url = "https://www.youtube.com/watch?v=" + video.videoId
this.type = "youtube"
this.id = video.videoId
this.duration = video.duration.seconds
this.readduration = getReadableDuration(this.duration)
return this
}
}
module.exports = {Song}

44
src/player/SongCheck.js Normal file
View File

@@ -0,0 +1,44 @@
const {LogType} = require("loguix")
const {Song} = require("./Song")
const slog = new LogType("SongCheck")
function checkSong(song) {
if(!(song instanceof Song)) {
slog.error("La musique n'est pas une instance de la classe Song")
// Check if the song is valid and if it has all the required properties
if(song.title && song.id && song.author && song.url && song.duration && song.readduration && song.type) {
slog.log("Acceptation de la musique : " + song.title)
return true
} else {
slog.error("La musique n'est pas valide")
return false
}
}
if(!song.url) {
slog.error("La musique n'a pas d'url")
return false
}
if(!song.title) {
slog.error("La musique n'a pas de titre")
return false
}
if(!song.author) {
slog.error("La musique n'a pas d'auteur")
return false
}
if(song.duration == null) {
slog.error("La musique n'a pas de durée")
return false
}
if(song.readduration == null) {
slog.error("La musique n'a pas de durée lisible")
return false
}
if(!song.type) {
slog.error("La musique n'a pas de type")
return false
}
return true
}
module.exports = {checkSong}

View File

@@ -0,0 +1,87 @@
const { LogType } = require('loguix');
const alog = new LogType("GoogleOAuth2");
const { google } = require('googleapis');
const config = require("../../utils/Database/Configuration");
const Users = require('../../server/auth/User');
const clientId = config.getYoutubeApiClientId();
const clientSecret = config.getYoutubeApiClientSecret();
const redirectUri = config.getWebsiteLink() + "/oauth2callback";
const oAuth2Map = new Map();
function createAuthUrl(userId) {
if(!checkCredientials()) return null;
var oAuth2Client;
const user = Users.getUserById(userId);
if (!user) {
alog.error(`User with ID ${userId} not found.`);
return null;
}
if (!clientId || !clientSecret) {
alog.error("YouTube API client ID or secret is not set in the configuration.");
} else {
oAuth2Client = new google.auth.OAuth2(
clientId,
clientSecret,
redirectUri
);
alog.log("Google OAuth2 client initialized successfully.");
}
if (!oAuth2Client) {
alog.error("OAuth2 client is not initialized. Please check your configuration.");
return null;
}
oAuth2Map.set(userId, oAuth2Client);
alog.log(`OAuth2 client created for user ${userId}.`);
return oAuth2Client.generateAuthUrl({
access_type: 'offline',
scope: SCOPES,
});
}
async function getAuthorization(userId, code) {
if(!checkCredientials()) return null;
try {
const user = Users.getUserById(userId);
if (!user) {
alog.error(`User with ID ${userId} not found.`);
return null;
}
oAuth2Client = oAuth2Map.get(userId);
if (!oAuth2Client) {
alog.error(`OAuth2 client for user ${userId} not found. Please create an OAuth2 client first.`);
return null;
}
const { tokens } = await oAuth2Client.getToken(code);
oAuth2Client.setCredentials(tokens);
alog.log(`OAuth2 client credentials set for user ${userId}.`);
return oAuth2Client;
} catch (error) {
alog.error(`Error during OAuth2 authorization for user ${userId}:`, error);
return null;
}
}
function checkCredientials() {
if (!clientId || !clientSecret) {
alog.error("YouTube API client ID or secret is not set in the configuration.");
return false;
}
return true;
}
module.exports = {
createAuthUrl,
getAuthorization,
getOAuth2Client: (userId) => oAuth2Map.get(userId),
oAuth2Map
};
const SCOPES = ['https://www.googleapis.com/auth/youtube.readonly'];

View File

@@ -0,0 +1,62 @@
const { google } = require('googleapis');
const { LogType } = require('loguix');
const alog = new LogType("YoutubeAPI");
const OAuth2 = require('./OAuth2');
const Users = require('../../server/auth/User');
async function getYoutubePlaylists(userId) {
const user = Users.getUserById(userId);
if (!user) {
alog.error(`User with ID ${userId} not found.`);
return null;
}
const oAuth2Client = OAuth2.getOAuth2Client(userId);
if (!oAuth2Client) {
alog.error(`OAuth2 client for user ${userId} not found. Please authenticate first.`);
return null;
}
const youtube = google.youtube({ version: 'v3', auth: oAuth2Client });
try {
const response = await youtube.playlists.list({
part: 'snippet,contentDetails',
mine: true,
maxResults: 50
});
alog.log(`Retrieved playlists for user ${userId}.`);
return response.data.items;
} catch (error) {
alog.error(`Error retrieving playlists for user ${userId}:`, error);
return null;
}
}
function getYoutubePlaylistSongs(playlistId, userId) {
const user = Users.getUserById(userId);
if (!user) {
alog.error(`User with ID ${userId} not found.`);
return null;
}
const oAuth2Client = OAuth2.getOAuth2Client(userId);
if (!oAuth2Client) {
alog.error(`OAuth2 client for user ${userId} not found. Please authenticate first.`);
return null;
}
const youtube = google.youtube({ version: 'v3', auth: oAuth2Client });
return youtube.playlistItems.list({
part: 'snippet',
playlistId: playlistId,
maxResults: 50
});
}
module.exports = {
getYoutubePlaylists,
getYoutubePlaylistSongs
};

54
src/playlists/History.js Normal file
View File

@@ -0,0 +1,54 @@
const {LogType} = require("loguix")
const hlog = new LogType("PersonalHistory")
const {__glob} = require("../utils/GlobalVars")
const { Database } = require("../utils/Database/Database")
const historyDb = new Database("history", __glob.HISTORY_DB, {})
historyDb.load()
/**
* @param {string} userId
* @returns {Array<Object>}
* @description Renvoie l'historique personnel de l'utilisateur
*/
function getPersonalHistory(userId) {
if (historyDb.data[userId]) {
return historyDb.data[userId];
} else {
hlog.log(`Création d'une clé pour l'utilisateur : ${userId}`);
historyDb.data[userId] = [];
historyDb.save();
return historyDb.data[userId];
}
}
/**
* @param {string} userId
* @param {Object} entry
* @description Ajoute une entrée à l'historique personnel de l'utilisateur
*/
function addToPersonalHistory(userId, entry) {
hlog.log(`Ajout d'une entrée à l'historique personnel de l'utilisateur : ${userId}`);
const history = getPersonalHistory(userId);
// Limit to 25 entries
if (history.length >= 25) {
history.shift();
}
history.push(entry)
historyDb.save();
}
/**
* @param {string} userId
* @description Vide l'historique personnel de l'utilisateur
*/
function clearPersonalHistory(userId) {
hlog.log(`Vidage de l'historique personnel de l'utilisateur : ${userId}`);
historyDb.data[userId] = [];
historyDb.save();
}
module.exports = {
getPersonalHistory,
addToPersonalHistory,
clearPersonalHistory
};

37
src/playlists/Playlist.js Normal file
View File

@@ -0,0 +1,37 @@
const { getReadableDuration } = require("../utils/TimeConverter");
class Playlist {
title = "Aucun titre";
id;
playlistId;
url;
author = "Auteur inconnu";
authorId;
songs = new Array();
thumbnail = "https://radomisol.fr/wp-content/uploads/2016/08/cropped-note-radomisol-musique.png" ;
duration = 0;
readduration;
description;
form = "PLAYLIST";
type;
constructor(title, url, author, authorId, songs, thumbnail, duration, readduration, description) {
this.title = title;
this.url = url;
this.author = author;
this.authorId = authorId;
this.songs = songs || new Array();
this.thumbnail = thumbnail;
// Make the some of durations of the songs
if(this.songs.length > 0) {
this.duration = this.songs.reduce((acc, song) => acc + song.duration, 0);
this.readduration = getReadableDuration(this.duration);
}
this.description = description;
if(!this.url) {
this.type = "playlist";
}
}
}
module.exports = {Playlist};

View File

@@ -0,0 +1,331 @@
const {Database} = require('../utils/Database/Database');
const {__glob} = require('../utils/GlobalVars');
const {Playlist} = require('./Playlist');
const {LogType} = require('loguix');
const clog = new LogType("PlaylistManager");
const Finder = require('../player/Finder');
const spotify = require('../media/SpotifyInformation');
const { getYoutubePlaylistSongs } = require('./Google/YoutubeList');
const { auth } = require('googleapis/build/src/apis/abusiveexperiencereport');
const { getReadableDuration } = require('../utils/TimeConverter');
const { getSecondsFromUrl } = require('../media/YoutubeInformation');
const playlistDB = new Database("Playlists", __glob.PLAYLISTFILE, {});
/**
* @param {string} id
* @param {string} name
* @returns {Array<Playlist>}
* @description Renvoie la liste des playlists de l'utilisateur
*/
function getPlaylistsOfUser(id) {
if (playlistDB.data[id]) {
return playlistDB.data[id];
} else {
// Creaete a key with the user id and an empty array
playlistDB.data[id] = new Array();
clog.log(`Création d'une clé pour l'utilisateur : ${id}`);
playlistDB.save();
return playlistDB.data[id];
}
}
/**
* @param {string} id
* @param {string} playlistId
* @returns {Playlist}
*/
function getPlaylistOfUser(id, playlistId) {
const playlists = getPlaylistsOfUser(id);
const playlist = playlists.find(p => p.playlistId === playlistId);
if (!playlist) {
clog.warn(`La playlist ${playlistId} n'existe pas pour l'utilisateur ${id}`);
return null;
}
return playlist;
}
async function addPlaylist(id, name, url, authorName, authorId, authorAvatar) {
const playlists = getPlaylistsOfUser(id);
var playlist = new Playlist(name, url);
let failed = false;
playlist.thumbnail = null
playlist.author = authorName;
playlist.authorAvatar = `https://cdn.discordapp.com/avatars/${authorId}/${authorAvatar}`;
playlist.views = null;
if(url) {
await Finder.search(url, false, "PLAYLIST").then(async (playlistFounded) => {
if(!playlistFounded) {
failed = true;
}
if(playlistFounded instanceof Playlist) {
playlist = playlistFounded;
}
if(playlist.type === "spotify") {
playlist.songs = await spotify.getTracks(playlist);
}
})
}
playlist.playlistId = new String(Date.now());
if(failed) {
clog.error(`Impossible de trouver la playlist ${name} pour l'utilisateur ${id}`);
return null;
}
playlists.push(playlist);
playlistDB.save();
clog.log(`Ajout de la playlist ${name} pour l'utilisateur ${id}`);
return playlist;
}
function removePlaylist(id, playlistId) {
const playlists = getPlaylistsOfUser(id);
const index = playlists.findIndex(p => p.playlistId === playlistId);
if (index === -1) {
clog.warn(`La playlist ${playlistId} n'existe pas pour l'utilisateur ${id}`);
return;
}
playlists.splice(index, 1);
playlistDB.save();
clog.log(`Suppression de la playlist ${playlistId} pour l'utilisateur ${id}`);
}
function getPlaylist(id, playlistId) {
const playlists = getPlaylistsOfUser(id);
const playlist = playlists.find(p => p.playlistId === playlistId);
if (!playlist) {
clog.warn(`La playlist ${playlistId} n'existe pas pour l'utilisateur ${id}`);
return null;
}
return playlist;
}
function copyPlaylist(fromId, toId, playlistId) {
const playlists = getPlaylistsOfUser(fromId);
const playlist = playlists.find(p => p.playlistId === playlistId);
if (!playlist) {
clog.warn(`La playlist ${playlistId} n'existe pas pour l'utilisateur ${fromId}`);
return null;
}
const toPlaylists = getPlaylistsOfUser(toId);
// Check if the playlist already exists in the target user
if (toPlaylists.find(p => p.title === playlist.title)) {
clog.warn(`La playlist ${playlist.title} existe déjà pour l'utilisateur ${toId}`);
return null;
}
toPlaylists.push(playlist);
playlistDB.save();
clog.log(`Copie de la playlist ${playlist.title} de l'utilisateur ${fromId} vers l'utilisateur ${toId}`);
return false;
}
function renamePlaylist(id, playlistId, newName) {
const playlists = getPlaylistsOfUser(id);
const playlist = playlists.find(p => p.playlistId === playlistId);
if (!playlist) {
clog.warn(`La playlist ${playlistId} n'existe pas pour l'utilisateur ${id}`);
return null;
}
playlist.title = newName;
playlistDB.save();
clog.log(`Renommage de la playlist ${playlistId} en ${newName} pour l'utilisateur ${id}`);
}
function addSong(id, playlistId, song) {
if(typeof song === "string") {
try {
song = JSON.parse(song)
} catch (e) {
clog.error(`La chanson ${song} n'est pas valide`);
return null;
}
}
// Check if the song is a valid object
if (typeof song !== 'object' || !song) {
clog.error(`La chanson ${song} n'est pas valide`);
return null;
}
const playlists = getPlaylistsOfUser(id);
const playlist = playlists.find(p => p.playlistId === playlistId);
if (!playlist) {
clog.warn(`La playlist ${playlistId} n'existe pas pour l'utilisateur ${id}`);
return null;
}
// Check the integrity of the song
if (!song.id || !song.title || !song.url) {
clog.error(`La chanson ${song.title} n'est pas valide`);
return null;
}
playlist.songs.push(song);
// Recalculate the songs duration and readduration
playlist.duration += song.duration;
playlist.readduration = getReadableDuration(playlist.duration);
playlistDB.save();
clog.log(`Ajout de la chanson ${song.title} à la playlist ${playlistId} pour l'utilisateur ${id}`);
}
function removeSong(id, playlistId, songId) {
const playlists = getPlaylistsOfUser(id);
const playlist = playlists.find(p => p.playlistId === playlistId);
if (!playlist) {
clog.warn(`La playlist ${playlistId} n'existe pas pour l'utilisateur ${id}`);
return null;
}
const index = playlist.songs.findIndex(s => s.id === songId);
if (index === -1) {
clog.warn(`La chanson ${songId} n'existe pas dans la playlist ${playlistId} pour l'utilisateur ${id}`);
return null;
}
playlist.duration -= playlist.songs[index].duration;
playlist.songs.splice(index, 1);
playlist.readduration = getReadableDuration(playlist.duration);
playlistDB.save();
clog.log(`Suppression de la chanson ${songId} de la playlist ${playlistId} pour l'utilisateur ${id}`);
}
async function processYoutubeData(userId, data) {
if (!data || data.length === 0) {
clog.warn(`Aucune donnée YouTube trouvée pour l'utilisateur ${userId}`);
return [];
}
const playlists = [];
for (const item of data) {
if (item.snippet && item.contentDetails) {
const playlist = new Playlist();
playlist.id = item.id;
playlist.title = item.snippet.title;
playlist.url = `https://www.youtube.com/playlist?list=${item.id}`;
playlist.description = item.snippet.description || "Aucune description disponible";
playlist.author = item.snippet.channelTitle;
playlist.thumbnail = item.snippet.thumbnails.default.url;
playlist.authorId = `https://www.youtube.com/channel/${item.snippet.channelId}`;
playlist.songs = []; // You can fetch songs later if needed
await getYoutubePlaylistSongs(item.id, userId).then(songsData => {
if (songsData && songsData.data && songsData.data.items) {
playlist.songs = songsData.data.items.map(song => ({
id: song.snippet.resourceId.videoId,
title: song.snippet.title,
author: song.snippet.videoOwnerChannelTitle,
authorId: `https://www.youtube.com/channel/${song.snippet.videoOwnerChannelId}`,
url: `https://www.youtube.com/watch?v=${song.snippet.resourceId.videoId}`,
thumbnail: song.snippet?.thumbnails?.default?.url || "https://radomisol.fr/wp-content/uploads/2016/08/cropped-note-radomisol-musique.png",
}));
// Add readduration for every items in songs
} else {
clog.warn(`Aucune chanson trouvée pour la playlist ${item.id}`);
}
}).catch(err => {
clog.error(`Erreur lors de la récupération des chansons pour la playlist ${item.id}:`, err);
});
for (const song of playlist.songs) {
// If authorId is not defined, delete the song
if (song.authorId == "https://www.youtube.com/channel/undefined") {
clog.warn(`L'auteur de la chanson ${song.title} (${song.id}) n'est pas défini. Suppression de la chanson.`);
playlist.songs.splice(playlist.songs.indexOf(song), 1);
continue; // Skip this song
}
song.duration = await getSecondsFromUrl(song.url);
if (song.duration === null) {
clog.warn(`Impossible de récupérer la durée de la chanson ${song.title} (${song.id})`);
song.duration = 0; // Set to 0 if duration cannot be fetched
} else {
song.readduration = getReadableDuration(song.duration);
playlist.duration += song.duration; // Initialize duration if not set
}
}
playlist.readduration = getReadableDuration(playlist.duration);
playlist.type = "youtube";
playlists.push(playlist);
} else {
clog.warn(`Données YouTube manquantes pour l'élément ${item.id}`);
}
};
clog.log(`Traitement des données YouTube pour l'utilisateur ${userId} terminé. Nombre de playlists trouvées : ${playlists.length}`);
// Save the playlists to the user's playlist collection
const userPlaylists = getPlaylistsOfUser(userId);
// Remove existing playlists with the same IDs to avoid duplicates
for (const playlist of playlists) {
const existingIndex = userPlaylists.findIndex(p => p.id === playlist.id);
if (existingIndex !== -1) {
userPlaylists.splice(existingIndex, 1); // Remove existing playlist with the same ID
}
}
userPlaylists.push(...playlists);
playlistDB.save();
clog.log(`Playlists ajoutées pour l'utilisateur ${userId}. Nombre total de playlists : ${userPlaylists.length}`);
return playlists;
}
async function deleteUserPlaylists(userId) {
// Delete all playlists of the user and the keys
if (!playlistDB.data[userId]) {
clog.warn(`Aucune playlist trouvée pour l'utilisateur ${userId}`);
return;
}
delete playlistDB.data[userId];
playlistDB.save();
}
async function refreshPlaylist(userId, playlistId) {
var playlist = getPlaylistOfUser(userId, playlistId);
if (!playlist) {
clog.warn(`Aucune playlist trouvée pour l'utilisateur ${userId} avec l'ID ${playlistId}`);
return null;
}
let failed = false;
// If playlistHasUrl, refresh the playlist
if (playlist.url) {
await Finder.search(playlist.url, false, "PLAYLIST").then(async (playlistFounded) => {
if(!playlistFounded) {
failed = true;
}
if(playlistFounded instanceof Playlist) {
playlist = playlistFounded;
}
if(playlist.type === "spotify") {
playlist.songs = await spotify.getTracks(playlist);
}
})
}
if(failed) {
clog.warn(`Échec de la mise à jour de la playlist ${playlistId} pour l'utilisateur ${userId}`);
return null;
}
playlist.playlistId = playlistId;
const playlists = getPlaylistsOfUser(userId);
// Remove the older one
// Push at the same index
const existingIndex = playlists.findIndex(p => p.playlistId === playlistId);
playlists.splice(existingIndex, 1, playlist);
playlistDB.save();
return playlist;
}
module.exports = {
getPlaylistsOfUser,
getPlaylistOfUser,
addPlaylist,
removePlaylist,
getPlaylist,
copyPlaylist,
renamePlaylist,
addSong,
removeSong,
processYoutubeData,
deleteUserPlaylists,
refreshPlaylist
}

1116
src/server/Server.js Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,126 @@
const { LogType } = require('loguix');
const dlog = new LogType("DiscordAuth");
const { getWebsiteLink, getClientSecret } = require('../../utils/Database/Configuration');
async function getDiscordUser(sessionId, auth_code) {
return new Promise((resolve, reject) => {
const discordBot = require("../../discord/Bot")
const discordBotClient = discordBot.getClient()
dlog.step.init("discord_auth_" + sessionId, "Authentification Discord de la session :" + sessionId);
dlog.log("Récupération de l'autorisation de récupération des informations de l'utilisateur Discord associé à la session : " + sessionId);
const params = new URLSearchParams();
params.append("client_id", discordBotClient.user.id);
params.append("client_secret", getClientSecret());
params.append("grant_type", "authorization_code");
params.append("code", auth_code);
params.append("redirect_uri", getWebsiteLink() + "/redirect");
params.append("scope", "identify guilds");
fetch("https://discord.com/api/oauth2/token", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: params,
}).then(accessTokenResp => accessTokenResp.json()).then(accessToken => {
if (accessToken.error) {
dlog.step.error("discord_auth_" + sessionId, "Erreur lors de la récupération du token d'accès Discord associé à la session : " + sessionId + " : " + accessToken.error + " : " + accessToken.error_description);
resolve("ACCESS_TOKEN_ERROR");
return;
}
dlog.log("Récupération réussi du token d'accès Discord associé à la session : " + sessionId);
fetch("https://discord.com/api/users/@me", {
headers: {
authorization: `${accessToken.token_type} ${accessToken.access_token}`,
},
}).then(userResp => userResp.json()).then(user => {
dlog.log("Récupération réussi des informations de l'utilisateur Discord associé à la session : " + sessionId + " avec le nom d'utilisateur : " + user.username + " (" + user.id + ")");
// Get the guilds of the user
fetch("https://discord.com/api/users/@me/guilds", {
headers: {
authorization: `${accessToken.token_type} ${accessToken.access_token}`,
},
}).then(guildsResp => guildsResp.json()).then(guilds => {
dlog.log("Récupération réussi des guildes de l'utilisateur Discord associé à la session : " + sessionId + " avec le nom d'utilisateur : " + user.username + " (" + user.id + ")");
dlog.step.end("discord_auth_" + sessionId)
const userData = {
auth: accessToken,
identity: user,
guilds: guilds,
}
resolve(userData);
}).catch(err => {
dlog.step.error("discord_auth_" + sessionId, "Erreur lors de la récupération des guildes de l'utilisateur Discord" + " avec le nom d'utilisateur : " + user.username + " (" + user.id + ")" + " associé à la session : " + sessionId + " : " + err);
resolve("GUILDS_ERROR");
});
}).catch(err => {
dlog.step.error("discord_auth_" + sessionId, "Erreur lors de la récupération des informations de l'utilisateur Discord associé à la session : " + sessionId + " : " + err);
resolve( "USER_INFO_ERROR");
})
}).catch(err => {
dlog.step.error("discord_auth_" + sessionId, "Erreur lors de la récupération du token d'accès Discord associé à la session : " + sessionId + " : " + err);
resolve("ACCESS_TOKEN_ERROR");
})
})
}
function refreshToken(refresh_token) {
return new Promise((resolve, reject) => {
const discordBot = require("../../discord/Bot")
const params = new URLSearchParams();
params.append("client_id", discordBot.getClient().user.id);
params.append("client_secret", getClientSecret());
params.append("grant_type", "refresh_token");
params.append("refresh_token", refresh_token);
params.append("scope", "identify guilds");
fetch("https://discord.com/api/oauth2/token", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: params,
}).then(accessTokenResp => accessTokenResp.json()).then(accessToken => {
if (accessToken.error) {
dlog.error("Erreur lors de la récupération du token d'accès Discord : " + accessToken.error + " : " + accessToken.error_description);
resolve(null);
return;
}
resolve(accessToken);
}).catch(err => {
dlog.error("Erreur lors de la récupération du token d'accès Discord : " + err);
resolve(null);
})
})
}
function getUserIdentity(accessToken) {
return new Promise((resolve, reject) => {
fetch("https://discord.com/api/users/@me", {
headers: {
authorization: `${accessToken.token_type} ${accessToken.access_token}`,
},
}).then(userResp => userResp.json()).then(user => {
if (user.error) {
dlog.error("Erreur lors de la récupération des informations de l'utilisateur Discord : " + user.error + " : " + user.error_description);
resolve(null);
return;
}
resolve(user);
}).catch(err => {
dlog.error("Erreur lors de la récupération des informations de l'utilisateur Discord : " + err);
resolve(null);
})
})
}
module.exports = {getDiscordUser, refreshToken, getUserIdentity}

View File

@@ -0,0 +1,33 @@
const { LogType } = require('loguix');
const { generateSessionId } = require('../../utils/TokenManager');
const clog = new LogType("Session");
const sessions = new Array();
function checkSession(sessionId) {
return sessions.includes(sessionId);
}
function addSession() {
const sessionId = generateSessionId();
if (checkSession(sessionId)) {
clog.warn(`Session ${sessionId} non trouvée dans la liste des sessions.`);
return addSession(); // Recursively generate a new session ID if it already exists
}
sessions.push(sessionId);
clog.log(`Nouvelle session ${sessionId} ajoutée.`);
return sessionId;
}
function removeSession(sessionId) {
const index = sessions.indexOf(sessionId);
if (index > -1) {
sessions.splice(index, 1);
clog.log(`Suppression de la session ${sessionId}.`);
} else {
clog.warn(`Session ${sessionId} non trouvée dans la liste des sessions.`);
}
}
module.exports = {checkSession, addSession, removeSession};

527
src/server/auth/User.js Normal file
View File

@@ -0,0 +1,527 @@
const { Database } = require('../../utils/Database/Database');
const { __glob } = require('../../utils/GlobalVars');
const { generateToken } = require('../../utils/TokenManager');
const { LogType } = require('loguix');
const clog = new LogType("User");
const discordAuth = require('./DiscordAuth');
const e = require('cors');
const UserDB = new Database("Users", __glob.USERFILE, []);
var userList = new Array();
class User {
auth;
identity;
tokens;
labels;
constructor(auth, identity, tokens, labels) {
this.auth = auth;
this.identity = identity;
this.tokens = tokens;
this.labels = labels;
}
setAdmin() {
const userInUserList = userList.find(user => user.identity.id === this.identity.id);
if (!userInUserList) {
clog.warn(`Utilisateur ${this.identity.username} non trouvé dans la liste des utilisateurs.`);
return null;
}
if (userInUserList.labels.includes("ADMIN")) {
userInUserList.labels.splice(userInUserList.labels.indexOf("ADMIN"), 1);
clog.log(`L'utilisateur ${this.identity.username} n'est plus admin.`);
} else {
userInUserList.labels.push("ADMIN");
clog.log(`L'utilisateur ${this.identity.username} est maintenant admin.`);
}
saveUsers()
}
setBan(guildId) {
const userInUserList = userList.find(user => user.identity.id === this.identity.id);
if (!userInUserList) {
clog.warn(`Utilisateur ${this.identity.username} non trouvé dans la liste des utilisateurs.`);
return null;
}
const banLabel = `BAN_${guildId}`;
if (userInUserList.labels.includes(banLabel)) {
userInUserList.labels.splice(userInUserList.labels.indexOf(banLabel), 1);
clog.log(`L'utilisateur ${this.identity.username} n'est plus banni du serveur ${guildId}.`);
} else {
userInUserList.labels.push(banLabel);
clog.log(`L'utilisateur ${this.identity.username} est maintenant banni du serveur ${guildId}.`);
}
saveUsers()
}
createToken() {
const token = generateToken(this.identity.id);
const userInUserList = userList.find(user => user.identity.id === this.identity.id);
if (!userInUserList) {
clog.warn(`Utilisateur ${this.identity.username} non trouvé dans la liste des utilisateurs.`);
return null;
}
userInUserList.tokens.push(token);
saveUsers();
clog.log(`Token créé pour l'utilisateur ${this.identity.username}.`);
return token;
}
removeToken(token) {
const userInUserList = userList.find(user => user.identity.id === this.identity.id);
if (!userInUserList) {
clog.warn(`Utilisateur ${this.identity.username} non trouvé dans la liste des utilisateurs.`);
return null;
}
const index = userInUserList.tokens.indexOf(token);
if (index > -1) {
userInUserList.tokens.splice(index, 1);
saveUsers();
clog.log(`Token supprimé pour l'utilisateur ${this.identity.username}.`);
} else {
clog.warn(`Token non trouvé pour l'utilisateur ${this.identity.username}.`);
}
}
clearToken() {
const userInUserList = userList.find(user => user.identity.id === this.identity.id);
if (!userInUserList) {
clog.warn(`Utilisateur ${this.identity.username} non trouvé dans la liste des utilisateurs.`);
return null;
}
userInUserList.tokens = [];
saveUsers();
clog.log(`Tous les tokens supprimés pour l'utilisateur ${this.identity.username}.`);
return userInUserList.tokens;
}
clearAuth() {
const userInUserList = userList.find(user => user.identity.id === this.identity.id);
if (!userInUserList) {
clog.warn(`Utilisateur ${this.identity.username} non trouvé dans la liste des utilisateurs.`);
return null;
}
userInUserList.auth = null;
saveUsers();
clog.log(`Authentification supprimée pour l'utilisateur ${this.identity.username}.`);
}
destroyAuth() {
const userInUserList = userList.find(user => user.identity.id === this.identity.id);
if (!userInUserList) {
clog.warn(`Utilisateur ${this.identity.username} non trouvé dans la liste des utilisateurs.`);
return null;
}
userInUserList.auth = null;
userInUserList.tokens = [];
saveUsers();
clog.log(`Authentification et tokens supprimés pour l'utilisateur ${this.identity.username}.`);
}
setFullBan() {
const userInUserList = userList.find(user => user.identity.id === this.identity.id);
if (!userInUserList) {
clog.warn(`Utilisateur ${this.identity.username} non trouvé dans la liste des utilisateurs.`);
return null;
}
if (userInUserList.labels.find(label => label == "BAN")) {
userInUserList.labels.splice(userInUserList.labels.indexOf("BAN"), 1);
clog.log(`L'utilisateur ${this.identity.username} n'est plus banni.`);
} else {
userInUserList.labels.push("BAN");
clog.log(`L'utilisateur ${this.identity.username} est maintenant banni.`);
}
saveUsers()
}
isBanned(guildId) {
const banLabel = `BAN_${guildId}`;
return this.labels.includes(banLabel);
}
isFullBanned() {
return this.labels.includes("BAN");
}
isAdmin() {
return this.labels.includes("ADMIN");
}
isMod(guildId) {
const modLabel = `MOD_${guildId}`;
return this.labels.includes(modLabel) || this.isAdmin();
}
justUpdated() {
const userInUserList = userList.find(user => user.identity.id === this.identity.id);
if (!userInUserList) {
clog.warn(`Utilisateur ${this.identity.username} non trouvé dans la liste des utilisateurs.`);
return false;
}
userInUserList.labels = userInUserList.labels.filter(label => !label.startsWith("UPDATED["));
userInUserList.labels.push("UPDATED[" + new Date().toISOString() + "]");
saveUsers();
}
needUpdate() {
const userInUserList = userList.find(user => user.identity.id === this.identity.id);
if (!userInUserList) {
clog.warn(`Utilisateur ${this.identity.username} non trouvé dans la liste des utilisateurs.`);
return false;
}
const lastUpdate = userInUserList.labels.find(label => label.startsWith("UPDATED["));
if (lastUpdate) {
const date = new Date(lastUpdate.replace("UPDATED[", "").replace("]", ""));
const now = new Date();
const diff = now - date;
// Check for 30 seconds
clog.log(`Dernière mise à jour de l'utilisateur ${this.identity.username} : ${date.toISOString()} (${diff} ms) - Besoin de mise à jour : ${diff > 30000}`);
// If the difference is greater than 30 seconds, we need to update
return diff > 30000; // 30 seconds
}
clog.log(`Aucune mise à jour n'a été effectuée pour l'utilisateur ${this.identity.username}.`);
return true;
}
}
//REFRESH USER
async function refreshAllUserInformation() {
await loadUsers();
clog.log("Récupération des informations de tous les utilisateurs...");
for (const user of userList) {
await updateCredientials(user.identity.id);
}
saveUsers();
}
async function updateCredientials(id) {
const user = getUserById(id);
if (!user) {
clog.warn(`Utilisateur ${id} non trouvé.`);
return null;
}
clog.log(`Mise à jour des informations d'authentification Discord de l'utilisateur ${user.identity.username} (${user.identity.id})...`);
if (user.auth) {
// Check if the token is expired
const auth = await discordAuth.refreshToken(user.auth.refresh_token);
if (auth) {
// Check Rate limit by checking if auth.message exists
if(typeof auth.message !== "undefined") {
clog.warn(`Erreur lors de la mise à jour des informations d'authentification de l'utilisateur ${user.identity.username} (${user.identity.id}) : ${auth.message}`);
return null;
}
user.auth = auth;
clog.log(`Mise à jour réussie des informations d'authentification de l'utilisateur ${user.identity.username} (${user.identity.id})`);
} else {
clog.warn(`Erreur lors de la mise à jour des informations d'authentification de l'utilisateur ${user.identity.username} (${user.identity.id})`);
}
// Update the user in the list
const userInUserList = userList.find(u => u.identity.id === user.identity.id);
if (userInUserList) {
userInUserList.auth = user.auth;
}
}
else {
clog.warn(`Aucune authentification trouvée pour l'utilisateur ${user.identity.username} (${user.identity.id})`);
}
saveUsers();
return user.auth;
}
async function updateIdentity(id) {
const user = getUserById(id);
if (!user) {
clog.warn(`Utilisateur ${id} non trouvé.`);
return null;
}
if(user.labels.includes("DELETED")) {
clog.warn(`L'utilisateur ${user.identity.username} (${user.identity.id}) est marqué comme supprimé, il ne peut pas être mis à jour.`);
return null;
}
clog.log(`Mise à jour de l'identité de l'utilisateur ${user.identity.username} (${user.identity.id})...`);
if (user.auth) {
const identity = await discordAuth.getUserIdentity(user.auth);
if(identity) {
// Check Rate limit by checking if identity.message exists
if(typeof identity.message !== "undefined") {
clog.warn(`Erreur lors de la mise à jour de l'identité de l'utilisateur ${user.identity.username} (${user.identity.id}) : ${identity.message}`);
return null;
}
user.identity = identity;
clog.log(`Mise à jour réussie de l'identité de l'utilisateur ${user.identity.username} (${user.identity.id})`);
}
else {
clog.warn(`Erreur lors de la mise à jour de l'identité de l'utilisateur ${user.identity.username} (${user.identity.id})`);
return null
}
// Update the user in the list
const userInUserList = userList.find(u => u.identity.id === user.identity.id);
if (userInUserList) {
userInUserList.auth = user.auth;
userInUserList.identity = user.identity;
}
} else {
clog.warn(`Aucune authentification trouvée pour l'utilisateur ${user.identity.username} (${user.identity.id})`);
return null;
}
saveUsers();
return user.identity;
}
// EDIT USER
/**
*
* @param {*} auth
* @param {*} identity
* @returns {User} user
*/
async function addUser(auth, identity) {
// Check if the user already exists
const existingUser = userList.find(user => user.identity.id === identity.id);
if (existingUser) {
clog.warn(`L'utilisateur ${identity.username} existe déjà.`);
if(existingUser.labels.includes("DELETED")) {
clog.warn(`L'utilisateur ${identity.username} est marqué comme supprimé, il sera réactivé.`);
existingUser.labels = existingUser.labels.filter(label => label !== "DELETED");
}
// Update the existing user with new information
existingUser.auth = auth;
existingUser.identity = identity;
existingUser.tokens = existingUser.tokens || []; // Ensure tokens array exists
existingUser.labels = existingUser.labels || []; // Ensure labels array exists
saveUsers();
clog.log(`Utilisateur ${identity.username} mis à jour.`);
return existingUser;
}
const newUser = new User(auth, identity, [], []);
userList.push(newUser);
await saveUsers();
return newUser;
}
function removeUser(id) {
const index = userList.findIndex(user => user.identity.id === id);
if (index > -1) {
userList.splice(index, 1);
saveUsers();
clog.log(`Utilisateur ${id} supprimé.`);
} else {
clog.warn(`Utilisateur ${id} non trouvé.`);
}
}
function removeToken(token) {
const user = getUserByToken(token);
if (user) {
const index = user.tokens.indexOf(token);
if (index > -1) {
user.tokens.splice(index, 1);
saveUsers();
clog.log(`Token ${token} supprimé pour l'utilisateur ${user.identity.username}.`);
} else {
clog.warn(`Token ${token} non trouvé pour l'utilisateur ${user.identity.username}.`);
}
} else {
clog.warn(`Utilisateur avec le token "${token}" non trouvé.`);
}
return user;
}
function addToken(id) {
const user = getUserById(id);
if (user) {
const token = generateToken();
user.tokens.push(token);
saveUsers();
clog.log(`Token "${token}" ajouté pour l'utilisateur ${user.identity.username}.`);
return token;
} else {
clog.warn(`Utilisateur ${id} non trouvé.`);
return null;
}
}
// GET
/**
* @param {string} id
* @returns {User} user
*/
function getUserById(id) {
return userList.find(user => user.identity.id === id) || null;
}
/**
*
* @param {string} token
* @returns {User} user
*/
function getUserByToken(token) {
return userList.find(user => user.tokens.includes(token)) || null;
}
function getUsers() {
return userList;
}
function getSimpleUsers() {
return userList.map(user => {
return {
identity: user.identity,
labels: user.labels,
};
});
}
function getSimpleUser(id) {
const user = getUserById(id);
if(user) {
return {
identity: user.identity,
labels: user.labels,
};
} else {
return null;
}
}
// SET LABELS
async function setAdmin(id) {
const user = getUserById(id);
if (user) {
await user.setAdmin();
await saveUsers();
} else {
clog.warn(`Utilisateur ${id} non trouvé.`);
}
}
async function setGuildMod(id, guildId) {
const user = getUserById(id);
if (user) {
const modLabel = `MOD_${guildId}`;
if (user.labels.includes(modLabel)) {
await user.labels.splice(user.labels.indexOf(modLabel), 1);
clog.log(`L'utilisateur ${user.identity.username} n'est plus modérateur du serveur ${guildId}.`);
} else {
await user.labels.push(modLabel);
clog.log(`L'utilisateur ${user.identity.username} est maintenant modérateur du serveur ${guildId}.`);
}
await saveUsers();
} else {
clog.warn(`Utilisateur ${id} non trouvé.`);
}
}
async function setGuildBan(id, guildId) {
const user = getUserById(id);
if (user) {
if(user.isAdmin()) {
clog.warn(`L'utilisateur ${user.identity.username} est admin, il ne peut pas être banni.`);
return;
}
await user.setBan(guildId);
await saveUsers();
} else {
clog.warn(`Utilisateur ${id} non trouvé.`);
}
}
async function setFullBan(id) {
const user = getUserById(id);
if (user) {
if(user.isAdmin()) {
clog.warn(`L'utilisateur ${user.identity.username} est admin, il ne peut pas être banni.`);
return;
}
await user.setFullBan();
await saveUsers();
} else {
clog.warn(`Utilisateur ${id} non trouvé.`);
}
}
function deleteAccount(id) {
const user = getUserById(id);
if (user) {
user.labels = user.labels.filter(label => label.includes('BAN'));
user.labels.push('DELETED'); // Add a deleted label
user.tokens = []; // Clear tokens
user.auth = null; // Clear authentication
user.identity = { id: user.identity.id, username: user.identity.username }; // Keep only identity information
saveUsers();
clog.log(`Suppression du compte de l'utilisateur ${user.identity.username}.`);
} else {
clog.warn(`Utilisateur ${id} non trouvé.`);
}
}
// USERS DB
function loadUsers() {
UserDB.load()
userList = new Array();
for (const user of UserDB.getData()) {
if(user?.labels?.includes("DELETED")) {
clog.log(`Utilisateur ${user.identity.id} marqué comme supprimé, ignoré.`);
userList.push(new User(null, user.identity, [], user.labels));
continue; // Skip deleted users
}
userList.push(new User(user.auth, user.identity, user.tokens, user.labels));
}
clog.log(`Chargement de ${userList.length} utilisateurs.`);
return userList;
}
function saveUsers() {
UserDB.data = userList.map(user => {
return {
auth: user.auth,
identity: user.identity,
tokens: user.tokens,
labels: user.labels,
};
});
UserDB.save()
clog.log(`Sauvegarde de ${userList.length} utilisateurs.`);
return loadUsers();
}
function clearNeedUpdateForUsers() {
userList.forEach(user => {
user.labels = user.labels.filter(label => !label.startsWith("UPDATED["));
});
saveUsers();
clog.log("Nettoyage des mises à jour nécessaires pour tous les utilisateurs.");
}
module.exports = {User}
module.exports = {
addUser,
setFullBan,
removeUser,
getUserByToken,
getUserById,
getUsers,
setAdmin,
setGuildMod,
setGuildBan,
addToken,
removeToken,
getSimpleUsers,
getSimpleUser,
updateCredientials,
refreshAllUserInformation,
updateIdentity,
clearNeedUpdateForUsers,
deleteAccount
};

View File

@@ -0,0 +1,31 @@
function isAudioFile(buffer) {
// Ensure the buffer is long enough to contain the magic number
if (!Buffer.isBuffer(buffer)) {
throw new TypeError('Expected a Buffer');
}
if (buffer.length < 2) {
return false;
}
// Convert the first few bytes to a hex string
const signature = buffer.subarray(0, 4).toString('hex').toUpperCase();
// Audio file signatures
const audioSignatures = [
'494433', // ID3 tag for MP3
'FFFB', // Another possible MP3 signature
'FFF3', // Another possible MP3 signature
'FFF2', // Another possible MP3 signature
'52494646', // RIFF header for WAV
'4F676753', // OGG container
'66747970', // MP4 container
// Add more audio file signatures as needed
];
// Check if the signature matches any known audio file type
return audioSignatures.some(magicNumber => signature.startsWith(magicNumber));
}
module.exports = {
isAudioFile
};

View File

@@ -0,0 +1,102 @@
const {Database} = require("./Database")
const {__glob} = require("../GlobalVars")
const {LogType} = require("loguix")
const path = require("path")
const { get } = require("http")
const clog = new LogType("Configuration")
const config = new Database("config", __glob.DATA + path.sep + "config.json", {
token: "",
client_secret: "",
report: {
channel : "",
contact : ""
},
api: {
youtube: {
clientId : "" ,
clientSecret: ""
},
spotify: {
clientId: "",
clientSecret: ""
}
},
website: "",
server_port: 5000,
media: {
guildId: "",
channelId: "",
}
})
function getToken() {
return config.data.token
}
function getReportChannel() {
return config.data.report.channel
}
function getReportContact() {
return config.data.report.contact
}
function getYoutubeApiClientId() {
return config.data.api.youtube.clientId
}
function getYoutubeApiClientSecret() {
return config.data.api.youtube.clientSecret
}
function getSpotifyClientId() {
return config.data.api.spotify.clientId
}
function getSpotifyClientSecret() {
return config.data.api.spotify.clientSecret
}
function getWebsiteLink() {
return config.data.website
}
function getPort() {
return config.data.server_port
}
function getClientSecret() {
return config.data.client_secret
}
function getMediaGuildId() {
return config.data.media.guildId
}
function getMediaChannelId() {
return config.data.media.channelId
}
if(getToken() == "") {
clog.error("Impossible de démarrer sans token valide")
process.exit(1)
}
module.exports = {
getToken,
getClientSecret,
getReportChannel,
getReportContact,
getYoutubeApiClientId,
getYoutubeApiClientSecret,
getSpotifyClientId,
getSpotifyClientSecret,
getWebsiteLink,
getPort,
getMediaGuildId,
getMediaChannelId
}

View File

@@ -61,6 +61,9 @@ class Database {
clog.error(`Erreur lors de la sauvegarde de la base de données '${this.name}'`)
clog.error(e)
}
// Assure that the database is up to date and reloaded
this.update()
}
@@ -80,6 +83,10 @@ class Database {
}
getData() {
return this.data
}
}

23
src/utils/GlobalVars.js Normal file
View File

@@ -0,0 +1,23 @@
const path = require("path")
const root = path.resolve(__dirname, '../../')
const version = JSON.parse(require('fs').readFileSync(root + path.sep + "package.json", "utf-8")).version
const __glob = {
PACKAGEINFO: root + path.sep + "package.json",
ROOT: root + + path.sep,
SRC: root + path.sep + "src",
LOGS: root + path.sep + "logs",
DATA: root + path.sep + "data",
COMMANDS: root + path.sep + "src" + path.sep + "discord" + path.sep + "Commands",
METRIC_FILE: root + path.sep + "data" + path.sep + "metrics.json",
PREVIOUSFILE: root + path.sep + "data" + path.sep + "previous.json",
USERFILE: root + path.sep + "data" + path.sep + "users.json",
PLAYLISTFILE: root + path.sep + "data" + path.sep + "playlists.json",
HISTORY_DB: root + path.sep + "data" + path.sep + "history.json",
MEDIA_DB: root + path.sep + "data" + path.sep + "media.json",
VERSION: version,
CHANGELOG_PATH: root + path.sep + "CHANGELOG.html",
COOKIES: root + path.sep + "data" + path.sep + "cookies.json"
}
module.exports = {__glob}

79
src/utils/Links.js Normal file
View File

@@ -0,0 +1,79 @@
const YoutubeLinks = [
"youtube.com",
"youtu.be",
"music.youtube.com",
"gaming.youtube.com",
"www.youtube.com",
"m.youtube.com"
]
var youtubePlaylistRegex = new RegExp(/^https?:\/\/(www.)?youtube.com\/playlist\?list=((PL|FL|UU|LL|RD|OL)[a-zA-Z0-9-_]{16,41})$/)
var youtubeVideoURLRegex = new RegExp(/^((?:https?:)?\/\/)?((?:www|m)\.)?((?:youtube\.com|youtu.be))(\/(?:watch\?v=))([\w-]{11})(\S+)?$/)
var youtubeVideoIdRegex = new RegExp(/^[a-zA-Z0-9-_]{11}$/)
const SpotifyLinks = [
"open.spotify.com",
"embed.spotify.com"
]
var spotifySongRegex = new RegExp(/^https?:\/\/(?:embed\.|open\.)(?:spotify\.com\/)(intl-([a-z]|[A-Z])+\/)?(?:track\/|\?uri=spotify:track:)((\w|-){22})(\?si=.+)?$/)
var spotifyPlaylistRegex = new RegExp(/^https?:\/\/(?:embed\.|open\.)(?:spotify\.com\/)(intl-([a-z]|[A-Z])+\/)?(?:playlist\/|\?uri=spotify:playlist:)((\w|-){22})(\?si=.+)?$/)
var spotifyAlbumRegex = new RegExp(/^https?:\/\/(?:embed\.|open\.)(?:spotify\.com\/)(intl-([a-z]|[A-Z])+\/)?(?:album\/|\?uri=spotify:album:)((\w|-){22})(\?si=.+)?$/)
const SoundcloudLinks = [
"soundcloud.com"
]
var soundcloudTrackRegex = new RegExp(/^https?:\/\/(m.|www.)?soundcloud.com\/(\w|-)+\/(\w|-)+(.+)?$/)
var soundcloudPlaylistRegex = new RegExp(/^https?:\/\/(m.|www.)?soundcloud.com\/(\w|-)+\/sets\/(\w|-)+(.+)?$/)
/**
* @typedef {Object} Links
* @property {Object} regex
* @property {Object} regex.youtube
* @property {RegExp} regex.youtube.playlist
* @property {RegExp} regex.youtube.videoURL
* @property {RegExp} regex.youtube.videoId
* @property {Object} regex.spotify
* @property {RegExp} regex.spotify.song
* @property {RegExp} regex.spotify.playlist
* @property {RegExp} regex.spotify.album
* @property {Object} regex.soundcloud
* @property {RegExp} regex.soundcloud.track
* @property {RegExp} regex.soundcloud.playlist
* @property {Object} types
* @property {Array<String>} types.youtube
* @property {Array<String>} types.spotify
* @property {Array<String>} types.soundcloud
*/
const Links = {
regex: {
youtube: {
playlist: youtubePlaylistRegex,
videoURL: youtubeVideoURLRegex,
videoId: youtubeVideoIdRegex
},
spotify: {
song: spotifySongRegex,
playlist: spotifyPlaylistRegex,
album: spotifyAlbumRegex
},
soundcloud: {
track: soundcloudTrackRegex,
playlist: soundcloudPlaylistRegex
}
},
types: {
youtube: YoutubeLinks,
spotify: SpotifyLinks,
soundcloud: SoundcloudLinks
}
}
module.exports = {Links}

11
src/utils/Maintenance.js Normal file
View File

@@ -0,0 +1,11 @@
const pm2 = require("pm2")
const { LogType } = require("loguix")
const clog = new LogType("Maintenance")
function restart(reason) {
clog.warn("Redémarrage du serveur Subsonics")
clog.warn(`Reason: ${reason}`)
pm2.restart("Subsonics")
}
module.exports = {restart}

16
src/utils/QueryType.js Normal file
View File

@@ -0,0 +1,16 @@
/**
* Enum for query types
* @readonly
*/
const QueryType = {
SPOTIFY_PLAYLIST: 'spotify_playlist',
SPOTIFY_ALBUM: 'spotify_album',
SPOTIFY_SONG: 'spotify_song',
YOUTUBE_PLAYLIST: 'youtube_playlist',
YOUTUBE_VIDEO: 'youtube_video',
SOUNDCLOUD_TRACK: 'soundcloud_track',
SOUNDCLOUD_PLAYLIST: 'soundcloud_playlist',
YOUTUBE_SEARCH: 'youtube_search',
}
module.exports = { QueryType };

32
src/utils/Resolver.js Normal file
View File

@@ -0,0 +1,32 @@
const {Links} = require('./Links')
const {QueryType} = require('./QueryType')
function getQueryType(url) {
// Check if it's string
if(typeof url !== "string") return "NOT_STRING"
// Check if it's a Youtube link
if(Links.regex.youtube.playlist.test(url)) return QueryType.YOUTUBE_PLAYLIST
if(Links.regex.youtube.videoURL.test(url)) return QueryType.YOUTUBE_VIDEO
// Check if it's a Spotify link
if(Links.regex.spotify.playlist.test(url)) return QueryType.SPOTIFY_PLAYLIST
if(Links.regex.spotify.album.test(url)) return QueryType.SPOTIFY_ALBUM
if(Links.regex.spotify.song.test(url)) return QueryType.SPOTIFY_SONG
// Check if it's a Soundcloud link
if(Links.regex.soundcloud.playlist.test(url)) return QueryType.SOUNDCLOUD_PLAYLIST
if(Links.regex.soundcloud.track.test(url)) return QueryType.SOUNDCLOUD_TRACK
return QueryType.YOUTUBE_SEARCH
}
module.exports = {getQueryType}

View File

@@ -0,0 +1,46 @@
function getReadableDuration(duration) {
var max = ""
duration *= 1000;
const maxhours = Math.floor(duration / 3600000);
var maxmin = Math.trunc(duration / 60000) - (Math.floor(duration / 60000 / 60) * 60);
var maxsec = Math.floor(duration / 1000) - (Math.floor(duration / 1000 / 60) * 60);
if (maxsec < 10) {
maxsec = `0${maxsec}`;
}
if(maxhours != 0) {
if (maxmin < 10) {
maxmin = `0${maxmin}`;
}
max = maxhours + "h" + maxmin + "m" + maxsec
} else {
max = maxmin + "m" + maxsec + "s"
}
return max
}
function getSecondsDuration(duration) {
// Duration is in format hh:mm:ss and can be just m:ss or mm:ss
var durationArray = duration.split(":");
var seconds = 0;
if(durationArray.length == 3) {
seconds = parseInt(durationArray[0]) * 3600 + parseInt(durationArray[1]) * 60 + parseInt(durationArray[2]);
} else if(durationArray.length == 2) {
seconds = parseInt(durationArray[0]) * 60 + parseInt(durationArray[1]);
} else {
seconds = parseInt(durationArray[0]);
}
return seconds;
}
module.exports = {getReadableDuration, getSecondsDuration}

18
src/utils/TokenManager.js Normal file
View File

@@ -0,0 +1,18 @@
function generateToken(userId) {
// Generate a token using the user ID with 32 random bytes
const crypto = require('crypto');
const token = userId + "_" + crypto.randomBytes(32).toString('hex');
return token;
}
function generateSessionId() {
// Generate a session ID using 32 random bytes
const crypto = require('crypto');
const sessionId = "SESSION" + "_" + crypto.randomBytes(32).toString('hex');
return sessionId;
}
module.exports = {generateToken, generateSessionId}