Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/classes/Cobalt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ export class Cobalt {
body: JSON.stringify({
url,
downloadMode: 'audio',
audioFormat: 'mp3',
audioFormat: 'wav',
}),
});

Expand Down
82 changes: 82 additions & 0 deletions src/classes/DirectDownloader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { Readable } from 'stream';
import { AudioResource, createAudioResource, demuxProbe } from '@discordjs/voice';
import { stream as playStream } from 'play-dl';
import { JukebotGlobals } from '../global';
import { FFmpegProcessor } from './FfmpegProcessor';
import { MusicDisc } from './MusicDisc';

export class DirectDownloader {
private readonly _disc: MusicDisc;

public constructor(disc: MusicDisc) {
this._disc = disc;
}

public async getResource(controller: AbortController): Promise<AudioResource<MusicDisc> | null> {
try {
// Use play-dl to get the best audio stream directly from the source.
// Use a typed alias for the awaited return so we can narrow safely.
type PlayDLStreamInfo = Awaited<ReturnType<typeof playStream>>;

const streamInfo = (await playStream(this._disc._url, { quality: 2 })) as unknown as PlayDLStreamInfo;

// play-dl may return either a Readable directly or an object with a
// `.stream` property containing the Readable. Narrow both cases.
let inputStream: Readable | null = null;

if ((streamInfo as unknown as Readable).readable !== undefined) {
inputStream = streamInfo as unknown as Readable;
} else if (typeof streamInfo === 'object' && streamInfo !== null && 'stream' in streamInfo) {
inputStream = (streamInfo as { stream?: Readable }).stream ?? null;
}

if (!inputStream) {
this._disc._requestedIn
.send({ content: `Direct download failed: no stream for ${this._disc._url}` })
.catch(() => null);
return null;
}

// Ensure the stream is aborted if the controller signals
if (controller) {
const abortHandler = () => {
try {
inputStream.destroy(new Error('Aborted'));
} catch (e) {
/* ignore */
}
};

if (controller.signal.aborted) abortHandler();
controller.signal.addEventListener('abort', abortHandler);
}

const processed = FFmpegProcessor.process(
inputStream,
this._disc.playbackSpeed,
this._disc.isPitchChangedOnPlaybackSpeed,
this._disc.isReversed,
this._disc.isEcho,
controller,
);

const { stream: probedStream, type } = await demuxProbe(processed);

const resource = createAudioResource<MusicDisc>(probedStream, {
inputType: type,
metadata: this._disc,
inlineVolume: JukebotGlobals.config.volumeModifier !== 1,
});

if (JukebotGlobals.config.volumeModifier !== 1 && resource.volume !== undefined) {
resource.volume.setVolume(JukebotGlobals.config.volumeModifier);
}

return resource;
} catch (error) {
const msg = error instanceof Error ? error.message : String(error);
this._disc._requestedIn.send({ content: `DirectDownloader error:\n\`${msg}\`` }).catch(() => null);
return null;
}
}
}
18 changes: 8 additions & 10 deletions src/classes/MusicDisc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { YouTubeVideo } from 'play-dl';
import { JukebotGlobals } from '../global';
import { DiscImage } from '../types';
import { awaitOrTimeout } from '../util';
import { Cobalt } from './Cobalt';
import { DirectDownloader } from './DirectDownloader';

/** A track that is ready to be prepared and then played. */
export class MusicDisc {
Expand Down Expand Up @@ -91,15 +91,13 @@ export class MusicDisc {

const controller = new AbortController();

const resourcePromise = new Promise<AudioResource<MusicDisc>>((resolve, reject) => {
new Cobalt(this).getResource(controller).then((res) => {
if (res !== null) {
resolve(res);
} else {
reject();
}
});
});
// Use the direct downloader (play-dl) exclusively — cobalt has a hard
// 1-minute limit for some YouTube sources in your environment.
const resourcePromise = (async (): Promise<AudioResource<MusicDisc>> => {
const direct = await new DirectDownloader(this).getResource(controller);
if (direct !== null) return direct;
throw new Error('Unable to create audio resource via direct downloader');
})();

this._resource = await awaitOrTimeout(
resourcePromise,
Expand Down