diff --git a/backend/package-lock.json b/backend/package-lock.json index e3ccb57..fb7330f 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -24,9 +24,11 @@ "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" } }, "node_modules/@derhuerst/http-basic": { @@ -65,6 +67,16 @@ "spotify-url-info": "^3.2.6" } }, + "node_modules/@discord-player/extractor/node_modules/soundcloud.ts": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/soundcloud.ts/-/soundcloud.ts-0.5.5.tgz", + "integrity": "sha512-bygjhC1w/w26Nk0Y+4D4cWSEJ1TdxLaE6+w4pCazFzPF+J4mzuB62ggWmFa7BiwnirzNf9lgPbjzrQYGege4Ew==", + "license": "MIT", + "peer": true, + "dependencies": { + "undici": "^6.17.0" + } + }, "node_modules/@discord-player/ffmpeg": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/@discord-player/ffmpeg/-/ffmpeg-7.1.0.tgz", @@ -1560,6 +1572,32 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "license": "MIT" }, + "node_modules/axios": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.1.tgz", + "integrity": "sha512-NN+fvwH/kV01dYUQ3PTOZns4LWtWhOFCAhQ/pHb88WQ1hNe5V/dvFwc4VJcDL11LT9xSX0QtsR8sWUuyOuOq7g==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/axios/node_modules/form-data": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", + "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -5569,14 +5607,10 @@ "license": "MIT" }, "node_modules/soundcloud.ts": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/soundcloud.ts/-/soundcloud.ts-0.5.5.tgz", - "integrity": "sha512-bygjhC1w/w26Nk0Y+4D4cWSEJ1TdxLaE6+w4pCazFzPF+J4mzuB62ggWmFa7BiwnirzNf9lgPbjzrQYGege4Ew==", - "license": "MIT", - "peer": true, - "dependencies": { - "undici": "^6.17.0" - } + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/soundcloud.ts/-/soundcloud.ts-0.6.3.tgz", + "integrity": "sha512-Ri5bO0jQKKACijGP1/OVbWXhHREDST2T6QUSAPWlzQjUScXVyh+7YJfN1mTnyuAA7vZjKyZ1FMlWC2hKd7jmHQ==", + "license": "MIT" }, "node_modules/source-map": { "version": "0.6.1", @@ -6559,6 +6593,15 @@ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "license": "ISC" + }, + "node_modules/ytfps": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/ytfps/-/ytfps-1.2.0.tgz", + "integrity": "sha512-DLcW0opwT0zO+4C5YqcCgPiOIzAtge6q6q3nDW0gCBy4kPufEdyxmjd1O9GUV4WeAFxfA2XNhZLmaohrGKV1WA==", + "license": "MIT", + "dependencies": { + "axios": "^1.7.2" + } } } } diff --git a/backend/package.json b/backend/package.json index f3648e1..8eb90ec 100644 --- a/backend/package.json +++ b/backend/package.json @@ -33,8 +33,10 @@ "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" } } diff --git a/backend/src/discord/Bot.js b/backend/src/discord/Bot.js index 0cb9378..c4b07f2 100644 --- a/backend/src/discord/Bot.js +++ b/backend/src/discord/Bot.js @@ -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], }) @@ -68,7 +72,42 @@ function init() { } }) - // TODO: Implement the disconnect event for the bot + 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) + } + }) + + } + + + }) diff --git a/backend/src/discord/Commands/Play.js b/backend/src/discord/Commands/Play.js index 73b9332..62ba938 100644 --- a/backend/src/discord/Commands/Play.js +++ b/backend/src/discord/Commands/Play.js @@ -3,6 +3,7 @@ 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) => { @@ -11,7 +12,7 @@ const command = new Command("play", "Jouer une musique à partir d'un lien dans 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((song) => { + 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) @@ -22,35 +23,23 @@ const command = new Command("play", "Jouer une musique à partir d'un lien dans // Check if song is playlist if(song instanceof Playlist) { - - if(now) { - player.readPlaylist(song, true) - embed.setTitle('**Lecture immédiate**') - } else { - player.readPlaylist(song) - embed.setTitle('**Ajout à la liste de lecture**') - } + 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('**Durée : **', song.readduration) + 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 { - - 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) @@ -60,9 +49,42 @@ const command = new Command("play", "Jouer une musique à partir d'un lien dans } + 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) + } + + } + - embed.send(interaction) + }) }, [{type: "STRING", name: "url", description: "Recherche / Lien audio (Youtube / Soundclound / Spotify)", required: true}, diff --git a/backend/src/media/SoundcloudInformation.js b/backend/src/media/SoundcloudInformation.js index e69de29..bd3cf52 100644 --- a/backend/src/media/SoundcloudInformation.js +++ b/backend/src/media/SoundcloudInformation.js @@ -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} \ No newline at end of file diff --git a/backend/src/media/SpotifyInformation.js b/backend/src/media/SpotifyInformation.js index eea84d6..d8727eb 100644 --- a/backend/src/media/SpotifyInformation.js +++ b/backend/src/media/SpotifyInformation.js @@ -27,33 +27,83 @@ async function getSong(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) { - console.error('Erreur lors de la récupération des données :', 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(albumId) { +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 - const data = await spotifyApi.clientCredentialsGrant(); - spotifyApi.setAccessToken(data.body['access_token']); + + 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]; - spotifyApi.getPlaylist(playlistId) - .then(function(data) { + 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; @@ -62,51 +112,68 @@ async function getPlaylist(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; - const tracks = info.tracks.items; - tracks.forEach(async function(track) { - - var trackName = track.track.name; - var artistName = track.track.artists[0].name; - var queryForYoutube = `${trackName} - ${artistName}`; + } catch (error) { + + clog.error("Impossible de récupérer les informations de l'album Spotify à partir de l'URL"); + clog.error(error); + return null; + } + +} - 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); +async function getTracks(playlist) { - 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.track.name; - song.author = track.track.artists[0].name; - song.url = urlYoutubeFounded; - song.thumbnail = track.track.album.images[0].url; - song.id = track.track.id; - song.duration = track.track.duration_ms / 1000; - song.readduration = getReadableDuration(track.track.duration_ms); + const tracks = playlist.songs + playlistSongs = []; + for(const track of tracks) { - playlist.duration += track.track.duration_ms; - - - playlist.songs.push(song); - } + 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; - - }, function(err) { - clog.error('Une erreur s\'est produite lors de la récupération de la playlist'); - clog.error(err); - }); } -module.exports = {getSong, getAlbum, getPlaylist} \ No newline at end of file +module.exports = {getSong, getAlbum, getPlaylist, getTracks} \ No newline at end of file diff --git a/backend/src/media/YoutubeInformation.js b/backend/src/media/YoutubeInformation.js index 650eb26..ccb06b9 100644 --- a/backend/src/media/YoutubeInformation.js +++ b/backend/src/media/YoutubeInformation.js @@ -1,35 +1,28 @@ -const {LogType} = require('loguix'); +const { LogType } = require('loguix'); const clog = new LogType("YoutubeInformation"); -const config = require('../utils/Database/Configuration'); -const YOUTUBE_API_KEY = config.getYoutubeApiKey() 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) { - // Check Query not null and a string - if(query === null && typeof query !== 'string') { +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; } - // * Fetch + try { - const response = await fetch(`https://www.googleapis.com/youtube/v3/search?part=snippet&type=video&q=${encodeURIComponent(query)}&key=${YOUTUBE_API_KEY}`); - const data = await response.json(); - - var videoLink = null - const videoId = data.items[0]?.id.videoId; - if(videoId) videoLink = `https://www.youtube.com/watch?v=${videoId}`; - if(videoLink === null) { + 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(videoLink); - + + const song = await getVideo(video.url); return song; - } catch (error) { clog.error('Erreur lors de la recherche YouTube: ' + error); return null; @@ -37,103 +30,72 @@ getQuery(query) { } async function getVideo(url) { - // Extract video ID from URL if it exists and is valid (11 characters) and if not return "NOT_VALID" - // Extract id from youtu.be youtube.com and music.youtube.com const videoId = url.match(/(?:youtu\.be\/|youtube\.com\/|music\.youtube\.com\/)(?:watch\?v=)?([a-zA-Z0-9_-]{11})/); - if(videoId === null) { + if (videoId === null) { clog.error("Impossible de récupérer l'identifiant de la vidéo YouTube à partir de l'URL"); return null; } - // Fetch video information - try { - const response = await fetch(`https://www.googleapis.com/youtube/v3/videos?part=snippet&id=${videoId[1]}&key=${YOUTUBE_API_KEY}`); - const data = await response.json(); - - const video = data.items[0]; - if(video) { - const songReturn = new Song() + 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); + clog.error('Erreur lors de la recherche de la vidéo YouTube:' + error); return null; } - - - - } async function getPlaylist(url) { - // Check Query not null and a string - if(url === null && typeof url !== 'string') { + if (url === null || typeof url !== 'string') { clog.error("Impossible de rechercher une playlist YouTube, car la requête est nulle"); return null; } - // * Fetch + try { - // For Playlist - 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 vidéo YouTube à partir de l'URL"); + 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 response = await fetch(`https://www.googleapis.com/youtube/v3/search?part=snippet&type=playlist&q=${encodeURIComponent(playlistId[1])}&key=${YOUTUBE_API_KEY}`); - const data = await response.json(); - if(data.items.length === 0) { + 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 = data.items[0].snippet.channelTitle - playlist.authorId = data.items[0].snippet.channelId - playlist.title = data.items[0].snippet.title - playlist.thumbnail = data.items[0].snippet.thumbnails.high.url - playlist.description = data.items[0].snippet.description - playlist.url = `https://www.youtube.com/playlist?list=${playlistId[1]}` - playlist.id = playlistId[1] + 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]; - - - - // Get all songs from playlist - - const responsePlaylist = await fetch(`https://www.googleapis.com/youtube/v3/playlistItems?part=snippet&playlistId=${playlistId[1]}&key=${YOUTUBE_API_KEY}&maxResults=100`); - const dataPlaylist = await responsePlaylist.json(); - - if(dataPlaylist.items.length === 0) { - clog.error("Impossible de récupérer les vidéos de la playlist YouTube à partir de l'identifiant ou la playlist est vide"); - return null; + for (const video of playlistInfo.videos) { + const song = new Song(); + await song.processYoutubeVideo(video, true); + playlist.duration += song.duration; + playlist.songs.push(song); } - - for (const video of dataPlaylist.items) { - const song = new Song() - video.id = video.snippet.resourceId.videoId - await song.processYoutubeVideo(video) - //? Add seconds to playlist duration - playlist.duration += song.duration - playlist.songs.push(song) - } - playlist.readduration = getReadableDuration(playlist.duration) + 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} \ No newline at end of file +module.exports = { getQuery, getVideo, getPlaylist }; diff --git a/backend/src/player/Finder.js b/backend/src/player/Finder.js index 46e02d5..4efed39 100644 --- a/backend/src/player/Finder.js +++ b/backend/src/player/Finder.js @@ -3,6 +3,7 @@ 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) { @@ -25,16 +26,18 @@ async function search(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 diff --git a/backend/src/player/Method/Media.js b/backend/src/player/Method/Media.js index 511a25f..26130a6 100644 --- a/backend/src/player/Method/Media.js +++ b/backend/src/player/Method/Media.js @@ -9,11 +9,11 @@ async function play(instance, song) { instance.player = createAudioPlayer() instance.generatePlayerEvents() const player = instance.player - song.resource = await createAudioResource(song.url, { + var resource = await createAudioResource(song.url, { inputType: StreamType.Arbitrary }) // Remplace par ton fichier audio - player.play(song.resource); + player.play(resource); instance.connection.subscribe(player); clog.log(`GUILD : ${instance.guildId} - Lecture de la musique (Media): ${song.title} - id : ${song.id}`) diff --git a/backend/src/player/Method/Soundcloud.js b/backend/src/player/Method/Soundcloud.js new file mode 100644 index 0000000..0cde29b --- /dev/null +++ b/backend/src/player/Method/Soundcloud.js @@ -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} \ No newline at end of file diff --git a/backend/src/player/Method/Youtube.js b/backend/src/player/Method/Youtube.js index aaa4c45..077db93 100644 --- a/backend/src/player/Method/Youtube.js +++ b/backend/src/player/Method/Youtube.js @@ -20,13 +20,13 @@ async function play(instance, song) { }); // Add compressor to the audio resource - song.resource = createAudioResource(stream); + var resource = createAudioResource(stream); - player.play(song.resource); + player.play(resource); instance.connection.subscribe(player); - clog.log(`GUILD : ${instance.guildId} - Lecture de la musique (Media): ${song.title} - id : ${song.id}`) + 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) diff --git a/backend/src/player/Player.js b/backend/src/player/Player.js index 39a6588..e7e2046 100644 --- a/backend/src/player/Player.js +++ b/backend/src/player/Player.js @@ -7,7 +7,7 @@ const clog = new LogType("Signal") const media = require('./Method/Media'); const youtube = require('./Method/Youtube'); -const Activity = require('../discord/Activity'); +const soundcloud = require('./Method/Soundcloud'); const AllPlayers = new Map() @@ -15,6 +15,7 @@ class Player { connection; player; guildId; + channelId; queue; constructor(guildId) { if(this.guildId === null) { @@ -45,6 +46,8 @@ class Player { selfMute: false }); + this.channelId = channel.id + this.player = createAudioPlayer() this.generatePlayerEvents() @@ -62,6 +65,8 @@ class Player { 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); @@ -109,7 +114,9 @@ class Player { 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 } @@ -151,6 +158,7 @@ class Player { } async leave() { + const Activity = require('../discord/Activity'); if(this.checkConnection()) return if(this.queue.current != null) { this.queue.addPreviousSong(this.queue.current) @@ -158,6 +166,9 @@ class Player { // 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) diff --git a/backend/src/player/Song.js b/backend/src/player/Song.js index ff5bb2c..3f3404d 100644 --- a/backend/src/player/Song.js +++ b/backend/src/player/Song.js @@ -2,8 +2,7 @@ const {LogType} = require('loguix') const clog = new LogType("Song") const MediaInformation = require('../media/MediaInformation') -const YoutubeDuration = require('../utils/YoutubeDuration'); -const { getReadableDuration } = require('../utils/TimeConverter'); +const { getReadableDuration, getSecondsDuration } = require('../utils/TimeConverter'); class Song { title = "Aucun titre"; @@ -15,7 +14,6 @@ class Song { duration; readduration; type; - resource; constructor(properties) { if(properties) { @@ -50,20 +48,33 @@ class Song { } - async processYoutubeVideo(video) { - this.title = video.snippet.title - this.author = video.snippet.channelTitle - this.authorId = video.snippet.channelId - this.thumbnail = video.snippet.thumbnails.standard.url - this.url = `https://www.youtube.com/watch?v=${video.id}` + 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 = await YoutubeDuration.getDurationVideo(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 - } + } } diff --git a/backend/src/utils/Resolver.js b/backend/src/utils/Resolver.js index 2a57ff9..9c664aa 100644 --- a/backend/src/utils/Resolver.js +++ b/backend/src/utils/Resolver.js @@ -17,10 +17,10 @@ function getQueryType(url) { if(Links.regex.spotify.song.test(url)) return QueryType.SPOTIFY_SONG // Check if it's a Soundcloud link - - if(Links.regex.soundcloud.track.test(url)) return QueryType.SOUNDCLOUD_TRACK - if(Links.regex.soundcloud.playlist.test(url)) return QueryType.SOUNDCLOUD_PLAYLIST + 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 diff --git a/backend/src/utils/TimeConverter.js b/backend/src/utils/TimeConverter.js index 52f1087..73e2d3e 100644 --- a/backend/src/utils/TimeConverter.js +++ b/backend/src/utils/TimeConverter.js @@ -29,4 +29,18 @@ function getReadableDuration(duration) { return max } -module.exports = {getReadableDuration} \ No newline at end of file +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} \ No newline at end of file diff --git a/backend/src/utils/YoutubeDuration.js b/backend/src/utils/YoutubeDuration.js deleted file mode 100644 index b3f496a..0000000 --- a/backend/src/utils/YoutubeDuration.js +++ /dev/null @@ -1,45 +0,0 @@ -const config = require('../utils/Database/Configuration'); -const YOUTUBE_API_KEY = config.getYoutubeApiKey() - -async function getDurationVideo(videoId) { - const clog = require("loguix").getInstance("YoutubeInformation"); - // Check videoId if valid - if(videoId === null && typeof videoId !== 'string') { - clog.error("Impossible de récupérer la durée de la vidéo YouTube, car l'identifiant est nul ou n'est pas une chaîne de caractères"); - return null; - } - - // Fetch video information - try { - const response = await fetch(`https://www.googleapis.com/youtube/v3/videos?part=contentDetails&id=${videoId}&key=${YOUTUBE_API_KEY}`); - const data = await response.json(); - const video = data.items[0]; - if(video) { - - if(video.contentDetails.duration == "P0D") return "LIVE"; - const duration = video.contentDetails.duration; - //Convert ISO 8601 duration to seconds - return parseDuration(duration); - - - } else { - clog.error("Impossible de récupérer la durée de la vidéo YouTube à partir de l'identifiant"); - return null; - } - } catch (error) { - clog.error('Erreur lors de la recherche de la durée de la vidéo YouTube:', error); - return null; - } - - -} - -function parseDuration(duration) { - const match = duration.match(/PT(\d+H)?(\d+M)?(\d+S)?/); - const hours = parseInt(match[1]) || 0; - const minutes = parseInt(match[2]) || 0; - const seconds = parseInt(match[3]) || 0; - return hours * 3600 + minutes * 60 + seconds; -} - -module.exports = {getDurationVideo} \ No newline at end of file