Merge pull request 'backend-0.2.0 => main' (#1) from backend-0.2.0 into main

Reviewed-on: #1
This commit is contained in:
2025-03-01 17:03:17 +00:00
40 changed files with 6668 additions and 23 deletions

4
.gitignore vendored
View File

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

4896
backend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "chopin-backend",
"version": "0.1.0",
"version": "0.3.0",
"description": "Discord Bot for music - Fetching everywhere !",
"main": "src/main.js",
"nodemonConfig": {
@@ -18,13 +18,25 @@
"author": "Raphix",
"license": "ISC",
"dependencies": {
"@discordjs/voice": "^0.18.0",
"@distube/ytdl-core": "^4.11.5",
"@distube/ytsr": "2.0.4",
"cors": "^2.8.5",
"discord-player": "^7.1.0",
"discord.js": "^14.18.0",
"express": "^4.21.2",
"ffmpeg-static": "^5.2.0",
"ffprobe": "^1.1.2",
"ffprobe-static": "^3.1.0",
"libsodium-wrappers": "^0.7.15",
"loguix": "^1.4.2",
"nodemon": "^3.1.9",
"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"
"webmetrik": "^0.1.4",
"ytfps": "^1.2.0"
}
}

View File

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

View File

@@ -5,9 +5,13 @@ 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 dlog = new LogType("Discord")
const membersVoices = new Map()
const timers = new Map()
const client = new Client({
intents:[GatewayIntentBits.Guilds, GatewayIntentBits.GuildVoiceStates, GatewayIntentBits.GuildMembers],
})
@@ -20,7 +24,7 @@ function getClient() {
function init() {
client.once('ready', () => {
dlog.log("Connexion au Bot Discord réussi ! Connecté en tant que : " + client.user.tag)
@@ -68,6 +72,44 @@ function init() {
}
})
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)
}
}, 10000))
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)
}
})
}
})
client.login(config.getToken())

View File

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

View File

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

View File

@@ -11,7 +11,7 @@ const command = new Command("about", "Affiche des informations sur le bot", (cli
const seconds = Math.floor(uptime % 60);
const embed = new Embed()
embed.setColor(0xb0f542)
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',"")

View File

@@ -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(" | ") +">"
}
})
}

View File

@@ -0,0 +1,28 @@
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 !").send(interaction)
const channel = interaction.member.voice.channel
var embed = new Embed()
if(AllPlayers.has(channel.guildId)) {
const player = AllPlayers.get(channel.guildId)
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")
} else {
embed = new EmbedError("Le bot n'est pas connecté à ce salon vocal")
}
embed.send(interaction)
})
module.exports = {command}

View File

@@ -0,0 +1,51 @@
const {Command} = require('../Command');
const {Embed, EmbedError} = require('../Embed');
const { Player } = require('../../player/Player');
const { Song } = require('../../player/Song');
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 !").send(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 !").send(interaction)
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)
const embed = new Embed()
embed.setColor(0x15e6ed)
if(now) {
player.play(song)
embed.setTitle('**Lecture immédiate**')
} else {
player.add(song)
embed.setTitle('**Ajout à liste de lecture**')
}
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(interaction)
}, [{type: "FILE", name: "media", description: "Fichier audio à lire", required: true},
{type:"BOOLEAN", name: "now", description: "Lire le média maintenant", required: false}]
)
module.exports = {command}

View File

@@ -0,0 +1,44 @@
const {Command} = require("../Command")
const {Embed, EmbedError} = require("../Embed")
const { Player } = require("../../player/Player")
const command = new Command("pause", "Mettre en pause / Reprendre la musique en cours", (client, interaction) => {
if(!interaction.member.voice.channel) return new EmbedError("Vous devez rejoindre un salon vocal pour mettre en pause la musique !").send(interaction)
const channel = interaction.member.voice.channel
const player = new Player(channel.guildId)
const result = player.pause()
var embed = new Embed()
embed.setColor(0x03ff2d)
result.then((pause) => {
if(pause == "no_music") {
embed = new EmbedError("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(interaction)
})
// Réponse en embed
})
module.exports = {command}

View File

@@ -0,0 +1,94 @@
const { Command } = require("../Command");
const { Embed, EmbedError } = require("../Embed");
const { Player } = require("../../player/Player");
const Finder = require("../../player/Finder");
const { Playlist } = require("../../player/Playlist");
const spotify = require("../../media/SpotifyInformation");
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 !").send(interaction)
const url = interaction.options.get("url")
const channel = interaction.member.voice.channel
const now = interaction.options.getBoolean("now") || false
await Finder.search(url.value).then(async (song) => {
if(!song) return new EmbedError("Impossible de trouver la musique à partir du lien donné ou des mots clés donnés").send(interaction)
const player = new Player(channel.guildId)
player.join(channel)
const embed = new Embed()
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.setThumbnail(song.thumbnail)
}
if(now) {
embed.setTitle("Lecture immédiate")
} else {
embed.setTitle("Ajoutée à la file d'attente")
}
embed.send(interaction)
if(song instanceof Playlist) {
if(song.type == "spotify") {
song = await spotify.getTracks(song)
}
if(now) {
player.readPlaylist(song, true)
} else {
player.readPlaylist(song)
}
} else {
if(now) {
player.play(song)
} else {
player.add(song)
}
}
})
}, [{type: "STRING", name: "url", description: "Recherche / Lien audio (Youtube / Soundclound / Spotify)", required: true},
{type:"BOOLEAN", name: "now", description: "Lire le média maintenant", required: false}]
)
module.exports = {command}

View File

@@ -0,0 +1,52 @@
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 !").send(interaction)
const channel = interaction.member.voice.channel
if(AllPlayers.has(channel.guildId)) {
const player = new Player(channel.guildId)
const result = player.previous()
var embed = new Embed()
embed.setColor(0x15e6ed)
result.then((song) => {
if(song == "no_music") {
embed = new EmbedError("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(interaction)
})
} else {
return new EmbedError("Le bot n'est pas connecté").send(interaction)
}
})
module.exports = {command}

View File

@@ -0,0 +1,42 @@
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
if(AllPlayers.has(channel.guildId)) {
const player = new Player(channel.guildId)
const queue = player.queue.getNext()
var embed = new Embed()
embed.setColor(0x15e6ed)
if(queue.length == 0) {
embed = new EmbedError("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(interaction)
} else {
return new EmbedError("Le bot n'est pas connecté").send(interaction)
}
})
module.exports = {command}

View File

@@ -0,0 +1,20 @@
const {Embed} = require("../Embed")
const {Command} = require("../Command")
const {restart} = require("../../utils/Maintenance")
// Nécéssite une raison pour redémarrer le bot
const command = new Command("restart", "Redémarre le bot", (client, interaction) => {
const reason = interaction.options.getString("reason")
restart(reason)
const embed = new Embed()
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(interaction)
},
[{type: "STRING", name: "reason", description: "Raison du redémarrage", required: true}]
)
module.exports = {command}

View File

@@ -0,0 +1,49 @@
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 !").send(interaction)
const channel = interaction.member.voice.channel
if(AllPlayers.has(channel.guildId)) {
const player = new Player(channel.guildId)
const result = player.skip()
var embed = new Embed()
embed.setColor(0x15e6ed)
result.then((song) => {
if(song == "no_music") {
embed = new EmbedError("Il n'y a pas de musique en file d'attente")
} 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(interaction)
})
} else {
return new EmbedError("Le bot n'est pas connecté").send(interaction)
}
})
module.exports = {command}

View File

@@ -0,0 +1,34 @@
const {Command} = require("../Command")
const {Embed, EmbedError} = require("../Embed")
const {Player} = require("../../player/Player")
const command = new Command("state", "Affiche la musique en cours", (client, interaction) => {
const channel = interaction.member.voice.channel
const player = new Player(channel.guildId)
const song = player.queue.getCurrent()
var embed = new Embed()
embed.setColor(0x15e6ed)
if(!song) {
embed = new EmbedError("Il n'y a pas de musique en cours de lecture")
} else if(song) {
// 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(interaction)
})
module.exports = {command}

View File

@@ -81,9 +81,23 @@ class Embed {
return this.embed
}
send(interaction) {
interaction.reply({embeds: [this.build()]})
send(interaction, ephemeral) {
if(ephemeral) {
interaction.reply({embeds: [this.build()], ephemeral: true})
} else {
interaction.reply({embeds: [this.build()]})
}
}
}
module.exports = {Embed}
class EmbedError extends Embed {
constructor(message) {
super()
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)
}
}
module.exports = {Embed, EmbedError}

View File

@@ -18,7 +18,7 @@ metric.publishMetrics("8001", "raphraph")
setup();
function setup() {
async function setup() {
const DiscordBot = require("./discord/Bot")
DiscordBot.init()
}

View File

@@ -0,0 +1,32 @@
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;
} catch (err) {
clog.error("Impossible de récupérer les informations de la musique : " + media.attachment.name)
clog.error(err)
return null;
}
}
module.exports = {getMediaInformation}

View File

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

View File

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

View File

@@ -0,0 +1,101 @@
const { LogType } = require('loguix');
const clog = new LogType("YoutubeInformation");
const { Song } = require('../player/Song');
const { Playlist } = require('../player/Playlist');
const { getReadableDuration } = require('../utils/TimeConverter');
const ytsr = require('@distube/ytsr');
const ytfps = require('ytfps');
async function getQuery(query) {
if (query === null || typeof query !== 'string') {
clog.error("Impossible de rechercher une vidéo YouTube, car la requête est nulle");
return null;
}
try {
const searchResults = await ytsr(query, { limit: 1 });
const video = searchResults.items.find(item => item.type === 'video');
if (!video) {
clog.error("Impossible de récupérer le lien de la vidéo YouTube à partir de la requête");
return null;
}
const song = await getVideo(video.url);
return song;
} 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 searchResults = await ytsr(videoId[1], { limit: 1 });
const video = searchResults.items.find(item => item.type === 'video');
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 {
const playlistId = url.match(/(?:youtu\.be\/|youtube\.com\/|music\.youtube\.com\/)(?:playlist\?list=)?([a-zA-Z0-9_-]{34})/);
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 ytfps(playlistId[1]);
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.title = playlistInfo.title;
playlist.thumbnail = playlistInfo.thumbnail_url;
playlist.description = playlistInfo.description;
playlist.url = `https://www.youtube.com/playlist?list=${playlistId[1]}`;
playlist.id = playlistId[1];
for (const video of playlistInfo.videos) {
const song = new Song();
await song.processYoutubeVideo(video, true);
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;
}
}
module.exports = { getQuery, getVideo, getPlaylist };

View File

@@ -0,0 +1,46 @@
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) {
const type = Resolver.getQueryType(query)
if(type == QueryType.YOUTUBE_SEARCH) {
return await youtube.getQuery(query)
}
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)
}
// TODO: Add more providers
}
module.exports = {search}

213
backend/src/player/List.js Normal file
View File

@@ -0,0 +1,213 @@
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)
return song
}
clearNext() {
this.next = new Array();
}
addNextSong(song) {
this.next.push(song)
}
firstNext(song) {
this.next.unshift(song)
}
removeNextByIndex(index) {
this.next.splice(index, 1)
}
moveSongToUpNext(index) {
const song = this.next[index]
this.next.splice(index, 1)
this.next.unshift(song)
}
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();
}
addPreviousSongToNextByIndex(index) {
const song = PreviousDB.data[this.guildId][index]
this.next.push(song)
}
addPreviousSong(song) {
PreviousDB.data[this.guildId].unshift(song)
savePrevious()
}
getCurrent() {
return this.current;
}
setCurrent(value) {
this.current = value;
}
destroy() {
this.clearNext();
this.current = null
this.shuffle = false;
AllLists.delete(this.guildId)
}
setShuffle(value) {
this.shuffle = value;
}
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
}
}
addNextPlaylist(playlist, firstAlreadyPlayed) {
if(firstAlreadyPlayed) {
playlist.songs.shift()
}
for(const song of playlist.songs) {
this.addNextSong(song)
}
}
}
const ListType = {
NEXT: "0",
PREVIOUS: "1"
}
function savePrevious() {
for(const guildId in PreviousDB.data) {
if(PreviousDB.data[guildId].length > 50) {
PreviousDB.data[guildId].splice(50, PreviousDB.data[guildId].length - 50)
}
}
PreviousDB.save();
}
module.exports = { List, ListType }

View File

@@ -0,0 +1,28 @@
const {createAudioResource, VoiceConnectionStatus, createAudioPlayer, StreamType} = require('@discordjs/voice');
const {LogType} = require('loguix')
const clog = new LogType("Media")
const plog = require("loguix").getInstance("Player")
async function play(instance, song) {
try {
instance.player = createAudioPlayer()
instance.generatePlayerEvents()
const player = instance.player
var resource = await createAudioResource(song.url, {
inputType: StreamType.Arbitrary
}) // Remplace par ton fichier audio
player.play(resource);
instance.connection.subscribe(player);
clog.log(`GUILD : ${instance.guildId} - Lecture de la musique (Media): ${song.title} - id : ${song.id}`)
} catch(e) {
clog.error("Erreur lors de la lecture de la musique : " + song.title)
clog.error(e)
}
}
module.exports = {play}

View File

@@ -0,0 +1,31 @@
const {createAudioResource, VoiceConnectionStatus, createAudioPlayer, StreamType} = require('@discordjs/voice');
const {LogType} = require('loguix')
const clog = new LogType("Soundcloud")
const plog = require("loguix").getInstance("Player")
const {Soundcloud} = require('soundcloud.ts')
const soundcloud = new Soundcloud();
async function play(instance, song) {
try {
instance.player = createAudioPlayer()
instance.generatePlayerEvents()
const player = instance.player
const stream = await soundcloud.util.streamTrack(song.url)
var resource = await createAudioResource(stream)
player.play(resource);
instance.connection.subscribe(player);
clog.log(`GUILD : ${instance.guildId} - Lecture de la musique (Soundcloud): ${song.title} - id : ${song.id}`)
} catch(e) {
clog.error("Erreur lors de la lecture de la musique : " + song.title)
clog.error(e)
}
}
module.exports = {play}

View File

@@ -0,0 +1,37 @@
const {createAudioResource, VoiceConnectionStatus, createAudioPlayer, StreamType} = require('@discordjs/voice');
const {LogType} = require('loguix')
const clog = new LogType("Youtube")
const plog = require("loguix").getInstance("Player")
const ytdl = require('@distube/ytdl-core')
async function play(instance, song) {
try {
instance.player = createAudioPlayer()
instance.generatePlayerEvents()
const player = instance.player
const stream = ytdl(song.url, {
quality: 'highestaudio',
highWaterMark: 1 << 30,
liveBuffer: 20000,
dlChunkSize: 0,
bitrate: 128,
});
// Add compressor to the audio resource
var resource = createAudioResource(stream);
player.play(resource);
instance.connection.subscribe(player);
clog.log(`GUILD : ${instance.guildId} - Lecture de la musique (Youtube): ${song.title} - id : ${song.id}`)
} catch(e) {
clog.error("Erreur lors de la lecture de la musique : " + song.title)
clog.error(e)
}
}
module.exports = {play}

View File

@@ -0,0 +1,213 @@
const { joinVoiceChannel, getVoiceConnection, entersState, VoiceConnectionStatus, createAudioPlayer, AudioPlayerStatus } = require('@discordjs/voice');
const {List} = require('./List')
const {LogType} = require("loguix");
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;
player;
guildId;
channelId;
queue;
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.connection = joinVoiceChannel({
channelId: channel.id,
guildId: channel.guild.id,
adapterCreator: channel.guild.voiceAdapterCreator,
selfDeaf: false,
selfMute: false
});
this.channelId = channel.id
this.player = createAudioPlayer()
this.generatePlayerEvents()
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()
}
});
}
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);
});
this.player.on(AudioPlayerStatus.Idle, () => {
Activity.idleActivity()
this.queue.setCurrent(null)
if(this.queue.next.length > 0) {
this.play(this.queue.nextSong())
}
});
this.player.on(AudioPlayerStatus.Playing, () => {
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)
});
}
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
}
}
async play(song) {
if(this.checkConnection()) return
if(this.queue.current != null) {
this.player.stop()
}
this.queue.setCurrent(song)
if(song.type == "attachment") {
media.play(this, song)
}
if(song.type == 'youtube') {
youtube.play(this, song)
}
if(song.type == "soundcloud") {
soundcloud.play(this, song)
}
// TODO: Créer une méthode pour les autres types de médias
}
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])
}
if(now) {
this.play(playlist.songs[0])
this.queue.addNextPlaylist(playlist, true)
} else {
this.queue.addNextPlaylist(playlist)
}
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`)
return false
} else {
this.player.pause()
plog.log(`GUILD : ${this.guildId} - La musique a été mise en pause`)
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
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)
}
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)
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)
return songPrevious
}
}
module.exports = {Player, AllPlayers}
/*
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);
*/

View File

@@ -0,0 +1,16 @@
class Playlist {
title = "Aucun titre";
id;
url;
author = "Auteur inconnu";
authorId;
songs = [];
thumbnail = "https://radomisol.fr/wp-content/uploads/2016/08/cropped-note-radomisol-musique.png" ;
duration = 0;
readduration;
description;
type;
}
module.exports = {Playlist};

View File

@@ -0,0 +1,81 @@
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;
type;
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, playlist) {
if(playlist) {
this.title = video.title
this.author = video.author.name
this.authorId = video.author.channel_url
this.thumbnail = video.thumbnail_url
this.url = video.url
this.type = "youtube"
this.id = video.id
this.duration = video.milis_length / 1000
this.readduration = getReadableDuration(this.duration)
} else {
this.title = video.name
this.author = video.author.name
this.authorId = video.author.url
this.thumbnail = video.thumbnail
this.url = video.url
this.type = "youtube"
this.id = video.id
this.duration = getSecondsDuration(video.duration)
this.readduration = getReadableDuration(this.duration)
}
return this
}
}
module.exports = {Song}

View File

@@ -10,6 +10,13 @@ const config = new Database("config", __glob.DATA + path.sep + "config.json", {
report: {
channel : "",
contact : ""
},
api: {
youtube: "",
spotify: {
clientId: "",
clientSecret: ""
}
}
})
@@ -25,9 +32,22 @@ function getReportContact() {
return config.data.report.contact
}
function getYoutubeApiKey() {
return config.data.api.youtube
}
function getSpotifyClientId() {
return config.data.api.spotify.clientId
}
function getSpotifyClientSecret() {
return config.data.api.spotify.clientSecret
}
if(getToken() == "") {
clog.error("Impossible de démarrer sans token valide")
process.exit(1)
}
module.exports = {getToken, getReportChannel, getReportContact}
module.exports = {getToken, getReportChannel, getReportContact, getYoutubeApiKey, getSpotifyClientId, getSpotifyClientSecret}

View File

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

View File

@@ -8,7 +8,8 @@ const __glob = {
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"
METRIC_FILE: root + path.sep + "data" + path.sep + "metrics.json",
PREVIOUSFILE: root + path.sep + "data" + path.sep + "previous.json",
}
module.exports = {__glob}

View File

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

View File

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

View File

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

View File

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

View File

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