Merge branch 'codetheweb:master' into master

This commit is contained in:
Oliver Traber 2023-05-28 00:28:53 +02:00 committed by GitHub
commit 162dfe2d52
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 70 additions and 98 deletions

View file

@ -18,11 +18,11 @@ export default class FileCacheProvider {
}
/**
* Returns path to cached file if it exists, otherwise throws an error.
* Returns path to cached file if it exists, otherwise returns null.
* Updates the `accessedAt` property of the cached file.
* @param hash lookup key
*/
async getPathFor(hash: string): Promise<string> {
async getPathFor(hash: string): Promise<string | null> {
const model = await prisma.fileCache.findUnique({
where: {
hash,
@ -30,7 +30,7 @@ export default class FileCacheProvider {
});
if (!model) {
throw new Error('File is not cached');
return null;
}
const resolvedPath = path.join(this.config.CACHE_DIR, hash);
@ -44,7 +44,7 @@ export default class FileCacheProvider {
},
});
throw new Error('File is not cached');
return null;
}
await prisma.fileCache.update({
@ -76,19 +76,15 @@ export default class FileCacheProvider {
const stats = await fs.stat(tmpPath);
if (stats.size !== 0) {
try {
await fs.rename(tmpPath, finalPath);
await fs.rename(tmpPath, finalPath);
await prisma.fileCache.create({
data: {
hash,
accessedAt: new Date(),
bytes: stats.size,
},
});
} catch (error) {
debug('Errored when moving a finished cache file:', error);
}
await prisma.fileCache.create({
data: {
hash,
accessedAt: new Date(),
bytes: stats.size,
},
});
}
await this.evictOldestIfNecessary();

View file

@ -1,7 +1,7 @@
import {VoiceChannel, Snowflake} from 'discord.js';
import {Readable} from 'stream';
import hasha from 'hasha';
import ytdl from 'ytdl-core';
import ytdl, {videoFormat} from 'ytdl-core';
import {WriteStream} from 'fs-capacitor';
import ffmpeg from 'fluent-ffmpeg';
import shuffle from 'array-shuffle';
@ -56,6 +56,8 @@ export interface PlayerEvents {
statusChange: (oldStatus: STATUS, newStatus: STATUS) => void;
}
type YTDLVideoFormat = videoFormat & {loudnessDb?: number};
export default class {
public voiceConnection: VoiceConnection | null = null;
public status = STATUS.PAUSED;
@ -408,30 +410,22 @@ export default class {
private async getStream(song: QueuedSong, options: {seek?: number; to?: number} = {}): Promise<Readable> {
if (song.source === MediaSource.HLS) {
return this.createReadStream(song.url);
return this.createReadStream({url: song.url, cacheKey: song.url});
}
let ffmpegInput = '';
let ffmpegInput: string | null;
const ffmpegInputOptions: string[] = [];
let shouldCacheVideo = false;
let format: ytdl.videoFormat | undefined;
let format: YTDLVideoFormat | undefined;
try {
ffmpegInput = await this.fileCache.getPathFor(this.getHashForCache(song.url));
ffmpegInput = await this.fileCache.getPathFor(this.getHashForCache(song.url));
if (options.seek) {
ffmpegInputOptions.push('-ss', options.seek.toString());
}
if (options.to) {
ffmpegInputOptions.push('-to', options.to.toString());
}
} catch {
if (!ffmpegInput) {
// Not yet cached, must download
const info = await ytdl.getInfo(song.url);
const {formats} = info;
const formats = info.formats as YTDLVideoFormat[];
const filter = (format: ytdl.videoFormat): boolean => format.codecs === 'opus' && format.container === 'webm' && format.audioSampleRate !== undefined && parseInt(format.audioSampleRate, 10) === 48000;
@ -465,12 +459,16 @@ export default class {
}
}
debug('Using format', format);
ffmpegInput = format.url;
// Don't cache livestreams or long videos
const MAX_CACHE_LENGTH_SECONDS = 30 * 60; // 30 minutes
shouldCacheVideo = !info.player_response.videoDetails.isLiveContent && parseInt(info.videoDetails.lengthSeconds, 10) < MAX_CACHE_LENGTH_SECONDS && !options.seek;
debug(shouldCacheVideo ? 'Caching video' : 'Not caching video');
ffmpegInputOptions.push(...[
'-reconnect',
'1',
@ -479,17 +477,23 @@ export default class {
'-reconnect_delay_max',
'5',
]);
if (options.seek) {
ffmpegInputOptions.push('-ss', options.seek.toString());
}
if (options.to) {
ffmpegInputOptions.push('-to', options.to.toString());
}
}
return this.createReadStream(ffmpegInput, {ffmpegInputOptions, cache: shouldCacheVideo});
if (options.seek) {
ffmpegInputOptions.push('-ss', options.seek.toString());
}
if (options.to) {
ffmpegInputOptions.push('-to', options.to.toString());
}
return this.createReadStream({
url: ffmpegInput,
cacheKey: song.url,
ffmpegInputOptions,
cache: shouldCacheVideo,
volumeAdjustment: format?.loudnessDb ? `${-format.loudnessDb}dB` : undefined,
});
}
private startTrackingPosition(initalPosition?: number): void {
@ -546,23 +550,24 @@ export default class {
}
}
private async createReadStream(url: string, options: {ffmpegInputOptions?: string[]; cache?: boolean} = {}): Promise<Readable> {
private async createReadStream(options: {url: string; cacheKey: string; ffmpegInputOptions?: string[]; cache?: boolean; volumeAdjustment?: string}): Promise<Readable> {
return new Promise((resolve, reject) => {
const capacitor = new WriteStream();
if (options?.cache) {
const cacheStream = this.fileCache.createWriteStream(this.getHashForCache(url));
const cacheStream = this.fileCache.createWriteStream(this.getHashForCache(options.cacheKey));
capacitor.createReadStream().pipe(cacheStream);
}
const returnedStream = capacitor.createReadStream();
let hasReturnedStreamClosed = false;
const stream = ffmpeg(url)
const stream = ffmpeg(options.url)
.inputOptions(options?.ffmpegInputOptions ?? ['-re'])
.noVideo()
.audioCodec('libopus')
.outputFormat('webm')
.addOutputOption(['-filter:a', `volume=${options?.volumeAdjustment ?? '1'}`])
.on('error', error => {
if (!hasReturnedStreamClosed) {
reject(error);