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 fs from 'node:fs';
import { ActivityType, ChannelType, Collection, Guild, GuildTextBasedChannel, VoiceChannel } from 'discord.js'; import { ActivityType, ChannelType, Collection, Guild, GuildTextBasedChannel } from 'discord.js';
import { AudioPlayer, AudioPlayerStatus, AudioResource, PlayerSubscription, VoiceConnection, createAudioPlayer, createAudioResource, joinVoiceChannel } from '@discordjs/voice'; import { AudioPlayer, AudioPlayerStatus, AudioResource, PlayerSubscription, VoiceConnection, VoiceConnectionState, VoiceConnectionStatus, createAudioPlayer, createAudioResource, entersState, joinVoiceChannel } from '@discordjs/voice';
import { LoggerClient } from '@navy.gif/logger'; import { LoggerClient } from '@navy.gif/logger';
import Initialisable from '../../interfaces/Initialisable.js'; import Initialisable from '../../interfaces/Initialisable.js';
@ -56,6 +56,7 @@ class MusicPlayer implements Initialisable
this.#currentSong = null; this.#currentSong = null;
this.#player.on(AudioPlayerStatus.Idle, this.playNext.bind(this)); this.#player.on(AudioPlayerStatus.Idle, this.playNext.bind(this));
this.#player.on('error', (err) => this.#logger.error(err));
} }
get ready () get ready ()
@ -226,13 +227,13 @@ class MusicPlayer implements Initialisable
this.#shuffleList = this.#library.getShufflePlaylist(); this.#shuffleList = this.#library.getShufflePlaylist();
this.initialiseVoiceChannels(); this.#initialiseVoiceChannels();
this.#ready = true; this.#ready = true;
this.playNext(); this.playNext();
} }
initialiseVoiceChannels () #initialiseVoiceChannels ()
{ {
for (const config of this.#options.guilds) 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}`); this.#logger.warn(`Invalid id pair given: Invalid guild ${id}`);
continue; continue;
} }
const channel = guild.channels.resolve(voiceChannel);
if (!this.#joinVoiceChannel(guild, voiceChannel, textOutput))
continue;
}
}
#joinVoiceChannel (guild: Guild, channelId: string, output?: string)
{
const channel = guild.channels.resolve(channelId);
if (channel?.type !== ChannelType.GuildVoice || !channel.joinable) if (channel?.type !== ChannelType.GuildVoice || !channel.joinable)
{ {
this.#logger.warn(`Invalid id pair given: Invalid channel ${id}`); this.#logger.warn(`Invalid voice channel given for guild ${guild.id}: ${channelId}`);
continue; return false;
} }
if (!this.joinVoiceChannel(guild, channel, textOutput))
continue;
}
}
joinVoiceChannel (guild: Guild, channel: VoiceChannel, output?: string)
{
const connection = joinVoiceChannel({ const connection = joinVoiceChannel({
channelId: channel.id, channelId: channel.id,
guildId: guild.id, guildId: guild.id,
adapterCreator: guild.voiceAdapterCreator adapterCreator: guild.voiceAdapterCreator
}); });
const subscription = connection.subscribe(this.#player); const subscription = connection.subscribe(this.#player);
if (!subscription) if (!subscription)
{ {
@ -269,6 +272,11 @@ class MusicPlayer implements Initialisable
return false; 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; let textChannel = null;
if (output) if (output)
{ {
@ -284,6 +292,36 @@ class MusicPlayer implements Initialisable
return true; 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; export default MusicPlayer;

View File

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