Use download queue for spotify downloads

This commit is contained in:
Erik 2024-03-28 08:57:25 +02:00
parent a8b52e9192
commit 6226c01ca2
2 changed files with 111 additions and 36 deletions

View File

@ -1,7 +1,7 @@
import fs from 'node:fs';
import { ActivityType, ChannelType, Collection, Guild, GuildTextBasedChannel, VoiceChannel } from 'discord.js';
import { AudioPlayer, AudioPlayerStatus, AudioResource, PlayerSubscription, VoiceConnection, createAudioPlayer, createAudioResource, joinVoiceChannel } from '@discordjs/voice';
import { ActivityType, ChannelType, Collection, Guild, GuildTextBasedChannel } from 'discord.js';
import { AudioPlayer, AudioPlayerStatus, AudioResource, PlayerSubscription, VoiceConnection, VoiceConnectionState, VoiceConnectionStatus, createAudioPlayer, createAudioResource, entersState, joinVoiceChannel } from '@discordjs/voice';
import { LoggerClient } from '@navy.gif/logger';
import Initialisable from '../../interfaces/Initialisable.js';
@ -56,6 +56,7 @@ class MusicPlayer implements Initialisable
this.#currentSong = null;
this.#player.on(AudioPlayerStatus.Idle, this.playNext.bind(this));
this.#player.on('error', (err) => this.#logger.error(err));
}
get ready ()
@ -226,13 +227,13 @@ class MusicPlayer implements Initialisable
this.#shuffleList = this.#library.getShufflePlaylist();
this.initialiseVoiceChannels();
this.#initialiseVoiceChannels();
this.#ready = true;
this.playNext();
}
initialiseVoiceChannels ()
#initialiseVoiceChannels ()
{
for (const config of this.#options.guilds)
{
@ -243,25 +244,27 @@ class MusicPlayer implements Initialisable
this.#logger.warn(`Invalid id pair given: Invalid guild ${id}`);
continue;
}
const channel = guild.channels.resolve(voiceChannel);
if (channel?.type !== ChannelType.GuildVoice || !channel.joinable)
{
this.#logger.warn(`Invalid id pair given: Invalid channel ${id}`);
continue;
}
if (!this.joinVoiceChannel(guild, channel, textOutput))
if (!this.#joinVoiceChannel(guild, voiceChannel, textOutput))
continue;
}
}
joinVoiceChannel (guild: Guild, channel: VoiceChannel, output?: string)
#joinVoiceChannel (guild: Guild, channelId: string, output?: string)
{
const channel = guild.channels.resolve(channelId);
if (channel?.type !== ChannelType.GuildVoice || !channel.joinable)
{
this.#logger.warn(`Invalid voice channel given for guild ${guild.id}: ${channelId}`);
return false;
}
const connection = joinVoiceChannel({
channelId: channel.id,
guildId: guild.id,
adapterCreator: guild.voiceAdapterCreator
});
const subscription = connection.subscribe(this.#player);
if (!subscription)
{
@ -269,6 +272,11 @@ class MusicPlayer implements Initialisable
return false;
}
connection.on('error', error => this.#logger.error('Voice connection error:', error));
connection.on(VoiceConnectionStatus.Disconnected, (...args) => this.#handleDisconnect(...args, guild));
if (this.#player.state.status === AudioPlayerStatus.AutoPaused)
this.#player.unpause();
let textChannel = null;
if (output)
{
@ -284,6 +292,36 @@ class MusicPlayer implements Initialisable
return true;
}
async #handleDisconnect (oldState: VoiceConnectionState, newState: VoiceConnectionState, guild: Guild): Promise<void>
{
this.#logger.debug(`Voice connection in ${guild.name} changed state from ${oldState.status} to ${newState.status}`);
const connectionData = this.#connections.get(guild.id);
if (!connectionData)
return;
const { connection } = connectionData;
try
{
await Promise.race([
entersState(connection, VoiceConnectionStatus.Signalling, 5_000),
entersState(connection, VoiceConnectionStatus.Connecting, 5_000)
]);
}
catch
{
this.#logger.info(`Connection in ${guild.name} was terminated, attempting reconnect`);
// this.#logger.error('Promise error:', err as Error);
connection.destroy();
this.#connections.delete(guild.id);
const details = this.#options.guilds.find((conf) => conf.id === guild.id);
if (!details)
return;
this.#joinVoiceChannel(guild, details.voiceChannel, details.textOutput);
}
// if (newState.status !== VoiceConnectionStatus.Disconnected)
// return;
}
}
export default MusicPlayer;

View File

@ -7,11 +7,20 @@ import DiscordClient from '../../DiscordClient.js';
import path from 'node:path';
import MusicPlayerError from '../../../errors/MusicPlayerError.js';
type QueueEntry = {
resolve: (result: DownloaderResult) => void,
reject: (error: Error) => void,
id: string,
url: string
}
class SpotifyDownloader extends Downloader
{
#executable;
#available;
#options: string[];
#queue: QueueEntry[];
#processing: boolean;
constructor (client: DiscordClient)
{
super(client, {
@ -25,12 +34,15 @@ class SpotifyDownloader extends Downloader
'--download-quality very_high',
'--download-real-time true',
'--md-allgenres true',
'--skip-previously-downloaded true',
// '--skip-previously-downloaded true',
'--output "{artist} --- {song_name} --- {album}.{ext}"',
'--print-downloads true',
];
if (process.env.CREDENTIAL_PATH)
this.#options.push('--credentials-location ' + process.env.CREDENTIAL_PATH);
this.#queue = [];
this.#processing = false;
}
override test (): void | Promise<void>
@ -79,36 +91,61 @@ class SpotifyDownloader extends Downloader
});
}
this.logger.info(`Queueing song for download: ${url}`);
return new Promise((resolve, reject) =>
{
childProcess.exec(`"${this.#executable}" --root-path "${this.downloadDir}" ${this.#options.join(' ')} ${url}`, (error, stdout, stderr) =>
{
if (error)
return reject(error);
const data = this.#getSongData(id);
if (!data)
throw new Error('Failed to find file reference');
const { fileName, artist, song, album, ext } = data;
const filePath = path.join(this.downloadDir!, fileName);
if (stderr && !stderr.includes('charmap'))
this.logger.debug('stderr', stderr);
if (stdout)
this.logger.debug('stdout', stdout);
resolve({
filePath,
artist,
song,
album,
ext
});
this.#queue.push({
resolve, reject, id, url
});
this.#processQueue();
// child.stdout?.on('data', (chunk => this.logger.debug('child stdout:', chunk)));
});
}
async #processQueue ()
{
if (this.#processing)
return;
const entry = this.#queue.shift();
if (!entry)
return;
this.#processing = true;
const { resolve, reject, id, url } = entry;
this.logger.info(`Processing queue, downloading ${url}`);
childProcess.exec(`"${this.#executable}" --root-path "${this.downloadDir}" ${this.#options.join(' ')} ${url}`, (error, stdout, stderr) =>
{
if (error)
return reject(error);
if (stderr) // && !stderr.includes('charmap')
this.logger.debug('stderr', stderr);
if (stdout)
this.logger.debug('stdout', stdout);
const data = this.#getSongData(id);
if (!data)
throw new Error('Failed to find file reference');
const { fileName, artist, song, album, ext } = data;
const filePath = path.join(this.downloadDir!, fileName);
resolve({
filePath,
artist,
song,
album,
ext
});
this.#processing = false;
if (this.#queue.length)
process.nextTick(this.#processQueue);
});
}
#getSongData (id: string)
{
const songIdsPath = path.join(this.downloadDir!, '.song_ids');