Compare commits
16 Commits
frontend-0
...
59ea576181
Author | SHA1 | Date | |
---|---|---|---|
59ea576181
|
|||
8b2728622c
|
|||
e313d4228c | |||
98cdae97c0 | |||
a59d7a66db | |||
e686edb0e6 | |||
3aa4201dd2 | |||
812d5c72fa | |||
12c4e2740a | |||
407d9d6b9a | |||
2a934d14ae | |||
c8c8fd71be | |||
a060d00599 | |||
f99fc24aa9 | |||
5e722e9dbc | |||
6f3847138b |
61
.gitea/workflows/deploy.yml
Normal file
61
.gitea/workflows/deploy.yml
Normal file
@@ -0,0 +1,61 @@
|
||||
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
|
||||
ssh-keyscan raphix.fr >> ~/.ssh/known_hosts
|
||||
|
||||
- name: Deploy Subsonics
|
||||
run: |
|
||||
ssh -o StrictHostKeyChecking=no raphix@raphix.fr << 'EOF'
|
||||
sudo su - gitlab-ci << 'INNER_EOF'
|
||||
set -e
|
||||
echo "[Subsonics-Deploy] - Stage - Déploiement - START "
|
||||
echo "[Subsonics-Deploy] - Arrêt de Subsonics : Processing"
|
||||
cd /home/gitlab-ci
|
||||
whoami
|
||||
pm2 stop "Subsonics" || true
|
||||
pm2 delete "Subsonics" || true
|
||||
echo "[Subsonics-Deploy] - Arrêt de Subsonics : Success"
|
||||
|
||||
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.git
|
||||
echo "[Subsonics-Deploy] - Installation de Subsonics : Success"
|
||||
|
||||
echo "[Subsonics-Deploy] - Installation des dépendances : Processing"
|
||||
cd /home/gitlab-ci/backend
|
||||
rm -r /home/gitlab-ci/backend/data || true
|
||||
mv /home/gitlab-ci/tempdata/ /home/gitlab-ci/backend/data || true
|
||||
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"
|
||||
INNER_EOF
|
||||
EOF
|
3
.gitignore
vendored
3
.gitignore
vendored
@@ -142,3 +142,6 @@ docs/_book
|
||||
test/
|
||||
|
||||
data/
|
||||
|
||||
__DEBUG.js
|
||||
__TEST.js
|
13
CHANGELOG.html
Normal file
13
CHANGELOG.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<div class="changelog-version changelog-actual">
|
||||
<h2>Chopin - Version /*1.0.0*/</h2>
|
||||
<p class="changelog-date">*_Date de sortie_*: *-XX/XX/XXXX-*</p>
|
||||
<ul>
|
||||
<li>[FRONTEND] Sortie de la version 1.0.0 de Chopin, le bot Discord pour SubSonics.</li>
|
||||
<li>[BACKEND] Ajout de la fonctionnalité de gestion des utilisateurs avec un fichier JSON.</li>
|
||||
<li>[DISCORD] Ajout de la fonctionnalité de gestion des utilisateurs avec un fichier JSON.</li>
|
||||
<li>[PLAYER] Ajout de la fonctionnalité de gestion des utilisateurs avec un fichier JSON.</li>
|
||||
<li>[AUTRE] Ajout de la fonctionnalité de gestion des utilisateurs avec un fichier JSON.</li>
|
||||
<li>[FIX] Ajout de la fonctionnalité de gestion des utilisateurs avec un fichier JSON.</li>
|
||||
<li>[AJOUT] Ajout de la fonctionnalité de gestion des utilisateurs avec un fichier JSON.</li>
|
||||
</ul>
|
||||
</div>
|
@@ -2,9 +2,9 @@
|
||||
|
||||
> 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)*
|
||||
@@ -14,7 +14,6 @@
|
||||
> - 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)
|
3
TODOS.md
Normal file
3
TODOS.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# List
|
||||
|
||||
TODO: Récupération des recommendations, playlists Youtube et Spotify
|
1717
backend/package-lock.json
generated
1717
backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
}
|
@@ -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}
|
||||
|
||||
|
@@ -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}
|
@@ -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}
|
@@ -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}
|
17
changelog.md
17
changelog.md
@@ -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
7124
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
46
package.json
Normal file
46
package.json
Normal file
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"name": "chopin-backend",
|
||||
"version": "1.1.1",
|
||||
"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"
|
||||
}
|
||||
}
|
@@ -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}`)
|
||||
}
|
232
src/discord/Bot.js
Normal file
232
src/discord/Bot.js
Normal file
@@ -0,0 +1,232 @@
|
||||
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 = [
|
||||
'CreateInstantInvite', 'AddReactions',
|
||||
'Stream', 'ViewChannel',
|
||||
'SendMessages', 'SendTTSMessages',
|
||||
'EmbedLinks', 'AttachFiles',
|
||||
'ReadMessageHistory', 'UseExternalEmojis',
|
||||
'Connect', 'Speak',
|
||||
'UseVAD', 'ChangeNickname',
|
||||
'UseApplicationCommands', 'RequestToSpeak',
|
||||
'CreatePublicThreads', 'CreatePrivateThreads',
|
||||
'UseExternalStickers', 'SendMessagesInThreads',
|
||||
'UseEmbeddedActivities', 'UseSoundboard',
|
||||
'UseExternalSounds', 'SendVoiceMessages',
|
||||
'SendPolls', 'UseExternalApps'
|
||||
]
|
||||
return requiredPermissions.filter(permission => !guildMember.permissions.has(permission));
|
||||
}
|
||||
|
||||
module.exports = {init, getClient, getGuilds, getMembersVoices, getChannel, getGuildMembers, isReady}
|
||||
|
||||
|
26
src/discord/Button.js
Normal file
26
src/discord/Button.js
Normal 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 };
|
@@ -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)}}
|
||||
|
||||
}
|
@@ -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!
|
@@ -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()
|
||||
|
||||
|
||||
})
|
@@ -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}
|
20
src/discord/Commands/Invite.js
Normal file
20
src/discord/Commands/Invite.js
Normal 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}
|
34
src/discord/Commands/Leave.js
Normal file
34
src/discord/Commands/Leave.js
Normal 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}
|
55
src/discord/Commands/Media.js
Normal file
55
src/discord/Commands/Media.js
Normal 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}
|
44
src/discord/Commands/Pause.js
Normal file
44
src/discord/Commands/Pause.js
Normal 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}
|
93
src/discord/Commands/Play.js
Normal file
93
src/discord/Commands/Play.js
Normal 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}
|
51
src/discord/Commands/Previous.js
Normal file
51
src/discord/Commands/Previous.js
Normal 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}
|
43
src/discord/Commands/Queue.js
Normal file
43
src/discord/Commands/Queue.js
Normal 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}
|
@@ -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()
|
||||
})
|
||||
|
||||
|
27
src/discord/Commands/Restart.js
Normal file
27
src/discord/Commands/Restart.js
Normal 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}
|
48
src/discord/Commands/Skip.js
Normal file
48
src/discord/Commands/Skip.js
Normal 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}
|
34
src/discord/Commands/State.js
Normal file
34
src/discord/Commands/State.js
Normal 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}
|
20
src/discord/Commands/Web.js
Normal file
20
src/discord/Commands/Web.js
Normal 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}
|
@@ -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
158
src/discord/MediaBase.js
Normal 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
|
||||
}
|
@@ -46,6 +46,7 @@ class Report {
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
module.exports = {Report}
|
58
src/lyrics/Lyrics.js
Normal file
58
src/lyrics/Lyrics.js
Normal 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
4
src/lyrics/urls.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"urlSearch": "http://api.deezer.com/search?limit=5&q=",
|
||||
"urlGet": "https://api.lyrics.ovh/v1/"
|
||||
}
|
@@ -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();
|
||||
|
||||
function setup() {
|
||||
async function setup() {
|
||||
const DiscordBot = require("./discord/Bot")
|
||||
DiscordBot.init()
|
||||
await DiscordBot.init()
|
||||
const Server = require("./server/Server")
|
||||
await Server.init()
|
||||
}
|
58
src/media/MediaInformation.js
Normal file
58
src/media/MediaInformation.js
Normal 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};
|
85
src/media/SoundcloudInformation.js
Normal file
85
src/media/SoundcloudInformation.js
Normal 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}
|
179
src/media/SpotifyInformation.js
Normal file
179
src/media/SpotifyInformation.js
Normal 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}
|
164
src/media/YoutubeInformation.js
Normal file
164
src/media/YoutubeInformation.js
Normal 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
82
src/player/Finder.js
Normal 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
240
src/player/List.js
Normal 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 }
|
21
src/player/Method/Media.js
Normal file
21
src/player/Method/Media.js
Normal 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}
|
25
src/player/Method/Soundcloud.js
Normal file
25
src/player/Method/Soundcloud.js
Normal 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}
|
30
src/player/Method/Youtube.js
Normal file
30
src/player/Method/Youtube.js
Normal file
@@ -0,0 +1,30 @@
|
||||
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 ffmpeg = require('fluent-ffmpeg')
|
||||
const { getRandomIPv6 } = require("@distube/ytdl-core/lib/utils");
|
||||
|
||||
async function getStream(song) {
|
||||
try {
|
||||
|
||||
let stream = ytdl(song.url, {
|
||||
quality: 'highestaudio',
|
||||
highWaterMark: 1 << 30,
|
||||
liveBuffer: 20000,
|
||||
dlChunkSize: 0,
|
||||
bitrate: 128,
|
||||
|
||||
});
|
||||
|
||||
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
435
src/player/Player.js
Normal 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
70
src/player/Song.js
Normal 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
44
src/player/SongCheck.js
Normal 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}
|
87
src/playlists/Google/OAuth2.js
Normal file
87
src/playlists/Google/OAuth2.js
Normal 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'];
|
||||
|
62
src/playlists/Google/YoutubeList.js
Normal file
62
src/playlists/Google/YoutubeList.js
Normal 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
54
src/playlists/History.js
Normal 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
37
src/playlists/Playlist.js
Normal 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};
|
||||
|
331
src/playlists/PlaylistManager.js
Normal file
331
src/playlists/PlaylistManager.js
Normal 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
1116
src/server/Server.js
Normal file
File diff suppressed because it is too large
Load Diff
126
src/server/auth/DiscordAuth.js
Normal file
126
src/server/auth/DiscordAuth.js
Normal 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}
|
33
src/server/auth/Session.js
Normal file
33
src/server/auth/Session.js
Normal 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
527
src/server/auth/User.js
Normal 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
|
||||
};
|
31
src/utils/AudioBufferCheck.js
Normal file
31
src/utils/AudioBufferCheck.js
Normal 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
|
||||
};
|
102
src/utils/Database/Configuration.js
Normal file
102
src/utils/Database/Configuration.js
Normal 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
|
||||
|
||||
}
|
@@ -62,6 +62,9 @@ class Database {
|
||||
clog.error(e)
|
||||
}
|
||||
|
||||
// Assure that the database is up to date and reloaded
|
||||
this.update()
|
||||
|
||||
}
|
||||
|
||||
update() {
|
||||
@@ -80,6 +83,10 @@ class Database {
|
||||
|
||||
}
|
||||
|
||||
getData() {
|
||||
return this.data
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
22
src/utils/GlobalVars.js
Normal file
22
src/utils/GlobalVars.js
Normal file
@@ -0,0 +1,22 @@
|
||||
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",
|
||||
}
|
||||
|
||||
module.exports = {__glob}
|
79
src/utils/Links.js
Normal file
79
src/utils/Links.js
Normal 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
11
src/utils/Maintenance.js
Normal 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
16
src/utils/QueryType.js
Normal 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
32
src/utils/Resolver.js
Normal 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}
|
46
src/utils/TimeConverter.js
Normal file
46
src/utils/TimeConverter.js
Normal 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
18
src/utils/TokenManager.js
Normal 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}
|
Reference in New Issue
Block a user