Use download queue for spotify downloads
This commit is contained in:
parent
a8b52e9192
commit
6226c01ca2
@ -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 (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;
|
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({
|
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;
|
@ -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,36 +91,61 @@ class SpotifyDownloader extends Downloader
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.logger.info(`Queueing song for download: ${url}`);
|
||||||
return new Promise((resolve, reject) =>
|
return new Promise((resolve, reject) =>
|
||||||
{
|
{
|
||||||
childProcess.exec(`"${this.#executable}" --root-path "${this.downloadDir}" ${this.#options.join(' ')} ${url}`, (error, stdout, stderr) =>
|
this.#queue.push({
|
||||||
{
|
resolve, reject, id, url
|
||||||
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.#processQueue();
|
||||||
// child.stdout?.on('data', (chunk => this.logger.debug('child stdout:', chunk)));
|
// 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)
|
#getSongData (id: string)
|
||||||
{
|
{
|
||||||
const songIdsPath = path.join(this.downloadDir!, '.song_ids');
|
const songIdsPath = path.join(this.downloadDir!, '.song_ids');
|
||||||
|
Loading…
Reference in New Issue
Block a user