diff --git a/package.json b/package.json index 9d0bc7f..08fd8ad 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "merge-options": "^2.0.0", "node-fetch": "^2.6.0", "simple-youtube-api": "^5.2.1", + "soundcloud-downloader": "0.0.9", "ytsr": "^0.1.15" }, "devDependencies": { diff --git a/src/Player.js b/src/Player.js index 3a2341d..adaabaa 100644 --- a/src/Player.js +++ b/src/Player.js @@ -1,6 +1,7 @@ const ytdl = require('discord-ytdl-core') const Discord = require('discord.js') const ytsr = require('ytsr') +const scdl = require('soundcloud-downloader') const Queue = require('./Queue') const Track = require('./Track') @@ -43,6 +44,7 @@ const filters = { * @property {boolean} [leaveOnEnd=true] Whether the bot should leave the current voice channel when the queue ends. * @property {boolean} [leaveOnStop=true] Whether the bot should leave the current voice channel when the stop() function is used. * @property {boolean} [leaveOnEmpty=true] Whether the bot should leave the voice channel if there is no more member in it. + * @property {string} [soundcloudClientID=""] The client ID for Soundcloud, optional */ /** @@ -53,7 +55,8 @@ const filters = { const defaultPlayerOptions = { leaveOnEnd: true, leaveOnStop: true, - leaveOnEmpty: true + leaveOnEmpty: true, + soundcloudClientID: '' } class Player { @@ -164,6 +167,21 @@ class Player { const matchURL = query.match(/(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/\s]{11})/) if (matchURL) { query = matchURL[1] + } else if (this.options.soundcloudClientID) { + const scRegex = /^https?:\/\/(soundcloud\.com)\/(.*)$/ + if (query.match(scRegex) && query.match(scRegex)[2]) { + const info = await scdl.getInfo(query, this.options.soundcloudClientID) + const details = { + title: info.title, + link: info.uri, + duration: info.duration, + description: info.description, + thumbnail: info.artwork_url, + views: info.playback_count, + author: { name: info.user.username } + } + return resolve([new Track(details, null, null)]) + } } ytsr(query, (err, results) => { if (err) return [] @@ -762,6 +780,51 @@ class Player { }) } + /** + * Play a Soundcloud stream in a channel + * @ignore + * @private + * @param {Queue} queue The queue to play + * @param {*} updateFilter Whether this method is called to update some ffmpeg filters + * @returns {Promise} + */ + _playSouncloudStream (queue, updateFilter) { + return new Promise(async (resolve) => { + const currentStreamTime = updateFilter ? queue.voiceConnection.dispatcher.streamTime / 1000 : undefined + const encoderArgsFilters = [] + Object.keys(queue.filters).forEach((filterName) => { + if (queue.filters[filterName]) { + encoderArgsFilters.push(filters[filterName]) + } + }) + let encoderArgs + if (encoderArgsFilters.length < 1) { + encoderArgs = [] + } else { + encoderArgs = ['-af', encoderArgsFilters.join(',')] + } + + const info = await scdl.getInfo(queue.playing.url, this.options.soundcloudClientID) + const opus = scdl.filterMedia(info.media.transcodings, { format: scdl.FORMATS.OPUS }) + const newStream = await scdl.downloadFromURL(opus[0].url, this.options.soundcloudClientID) + setTimeout(() => { + queue.voiceConnection.play(newStream, { + type: 'ogg/opus' + }) + queue.voiceConnection.dispatcher.setVolumeLogarithmic(queue.volume / 200) + // When the track starts + queue.voiceConnection.dispatcher.on('start', () => { + resolve() + }) + // When the track ends + queue.voiceConnection.dispatcher.on('finish', () => { + // Play the next track + return this._playTrack(queue.guildID, false) + }) + }, 1000) + }) + } + /** * Start playing a track in a guild * @ignore @@ -790,6 +853,16 @@ class Player { const nowPlaying = queue.playing = queue.repeatMode ? wasPlaying : queue.tracks.shift() // Reset lastSkipped state queue.lastSkipped = false + + if (queue.playing.url.includes('soundcloud.com')) { + this._playSouncloudStream(queue, false).then(() => { + // Emit trackChanged event + if (!firstPlay) { + queue.emit('trackChanged', nowPlaying, wasPlaying, queue.lastSkipped, queue.repeatMode) + } + }) + return + } this._playYTDLStream(queue, false).then(() => { // Emit trackChanged event if (!firstPlay) {