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

@ -6,6 +6,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
## [2.3.0] - 2023-05-13
### Added
- Muse now normalizes playback volume across tracks. Thanks to @UniversalSuperBox for sponsoring this feature!
### Fixed
- Fixed a bug where tracks wouldn't be cached
## [2.2.4] - 2023-04-17
### Fixed
- Bumped ytdl-core
## [2.2.3] - 2023-04-04 ## [2.2.3] - 2023-04-04
- Updated ytsr dependency to fix (reading 'reelPlayerHeaderRenderer') error - Updated ytsr dependency to fix (reading 'reelPlayerHeaderRenderer') error
## [2.2.2] - 2023-03-18 ## [2.2.2] - 2023-03-18
@ -231,7 +242,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
- Initial release - Initial release
[unreleased]: https://github.com/codetheweb/muse/compare/v2.2.3...HEAD [unreleased]: https://github.com/codetheweb/muse/compare/v2.3.0...HEAD
[2.3.0]: https://github.com/codetheweb/muse/compare/v2.2.4...v2.3.0
[2.2.4]: https://github.com/codetheweb/muse/compare/v2.2.3...v2.2.4
[2.2.3]: https://github.com/codetheweb/muse/compare/v2.2.2...v2.2.3 [2.2.3]: https://github.com/codetheweb/muse/compare/v2.2.2...v2.2.3
[2.2.2]: https://github.com/codetheweb/muse/compare/v2.2.1...v2.2.2 [2.2.2]: https://github.com/codetheweb/muse/compare/v2.2.1...v2.2.2
[2.2.1]: https://github.com/codetheweb/muse/compare/v2.2.0...v2.2.1 [2.2.1]: https://github.com/codetheweb/muse/compare/v2.2.0...v2.2.1

View file

@ -19,6 +19,7 @@ Muse is a **highly-opinionated midwestern self-hosted** Discord music bot **that
- ↔️ Autoconverts playlists / artists / albums / songs from Spotify - ↔️ Autoconverts playlists / artists / albums / songs from Spotify
- ↗️ Users can add custom shortcuts (aliases) - ↗️ Users can add custom shortcuts (aliases)
- 1⃣ Muse instance supports multiple guilds - 1⃣ Muse instance supports multiple guilds
- 🔊 Normalizes volume across tracks
- ✍️ Written in TypeScript, easily extendable - ✍️ Written in TypeScript, easily extendable
- ❤️ Loyal Packers fan - ❤️ Loyal Packers fan

View file

@ -1,6 +1,6 @@
{ {
"name": "muse", "name": "muse",
"version": "2.2.3", "version": "2.3.0",
"description": "🎧 a self-hosted Discord music bot that doesn't suck ", "description": "🎧 a self-hosted Discord music bot that doesn't suck ",
"repository": "git@github.com:codetheweb/muse.git", "repository": "git@github.com:codetheweb/muse.git",
"author": "Max Isom <hi@maxisom.me>", "author": "Max Isom <hi@maxisom.me>",
@ -111,7 +111,7 @@
"sync-fetch": "^0.3.1", "sync-fetch": "^0.3.1",
"tsx": "3.8.2", "tsx": "3.8.2",
"xbytes": "^1.7.0", "xbytes": "^1.7.0",
"ytdl-core": "^4.11.2", "ytdl-core": "^4.11.4",
"ytsr": "^3.8.1" "ytsr": "^3.8.2"
} }
} }

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

View file

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

View file

@ -714,13 +714,6 @@ asynckit@^0.4.0:
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= integrity sha1-x57Zf380y48robyXkLzDZkdLS3k=
axios@^0.19.0:
version "0.19.2"
resolved "https://registry.yarnpkg.com/axios/-/axios-0.19.2.tgz#3ea36c5d8818d0d5f8a8a97a6d36b86cdc00cb27"
integrity sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==
dependencies:
follow-redirects "1.5.10"
balanced-match@^1.0.0: balanced-match@^1.0.0:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
@ -1069,13 +1062,6 @@ debug@4, debug@4.3.4, debug@^4.0.1, debug@^4.1.1, debug@^4.3.1, debug@^4.3.3:
dependencies: dependencies:
ms "2.1.2" ms "2.1.2"
debug@=3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==
dependencies:
ms "2.0.0"
decode-uri-component@^0.2.0: decode-uri-component@^0.2.0:
version "0.2.0" version "0.2.0"
resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545"
@ -1775,13 +1761,6 @@ fluent-ffmpeg@^2.1.2:
async ">=0.2.9" async ">=0.2.9"
which "^1.1.1" which "^1.1.1"
follow-redirects@1.5.10:
version "1.5.10"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.10.tgz#7b7a9f9aea2fdff36786a94ff643ed07f4ff5e2a"
integrity sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==
dependencies:
debug "=3.1.0"
form-data-encoder@1.7.1: form-data-encoder@1.7.1:
version "1.7.1" version "1.7.1"
resolved "https://registry.yarnpkg.com/form-data-encoder/-/form-data-encoder-1.7.1.tgz#ac80660e4f87ee0d3d3c3638b7da8278ddb8ec96" resolved "https://registry.yarnpkg.com/form-data-encoder/-/form-data-encoder-1.7.1.tgz#ac80660e4f87ee0d3d3c3638b7da8278ddb8ec96"
@ -2869,11 +2848,6 @@ mkdirp@^1.0.3:
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
ms@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=
ms@2.1.2: ms@2.1.2:
version "2.1.2" version "2.1.2"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
@ -4374,35 +4348,18 @@ yocto-queue@^1.0.0:
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.0.0.tgz#7f816433fb2cbc511ec8bf7d263c3b58a1a3c251" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.0.0.tgz#7f816433fb2cbc511ec8bf7d263c3b58a1a3c251"
integrity sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g== integrity sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==
youtube.ts@^0.2.9: ytdl-core@^4.11.4:
version "0.2.9" version "4.11.4"
resolved "https://registry.yarnpkg.com/youtube.ts/-/youtube.ts-0.2.9.tgz#f66f210ede81ae8116dc614788d90f6016e269c2" resolved "https://registry.yarnpkg.com/ytdl-core/-/ytdl-core-4.11.4.tgz#0ee2bd04d8effa7b8762a3ba0e3d038e37dc10f2"
integrity sha512-s7udygKcvNpjGM4AieWQ84+H0McJIKCWCTBjxKOJsBm5jtk8D6eRtwHxbAmDgOQ9X+mykMEopDK1jdfI8VpExg== integrity sha512-tsVvqt++B5LSTMnCKQb4H/PFBewKj7gGPJ6KIM5gOFGMKNZj4qglGAl4QGFG8cNPP6wY54P80FDID5eN2di0GQ==
dependencies:
axios "^0.19.0"
ytdl-core "^4.11.0"
ytdl-core@^4.11.0:
version "4.11.0"
resolved "https://registry.yarnpkg.com/ytdl-core/-/ytdl-core-4.11.0.tgz#79a3ea94d9d662b4b3acecdb1372ed3f1a9ea9db"
integrity sha512-Q3hCLiUA9AOGQXzPvno14GN+HgF9wsO1ZBHlj0COTcyxjIyFpWvMfii0UC4/cAbVaIjEdbWB71GdcGuc4J1Lmw==
dependencies: dependencies:
m3u8stream "^0.8.6" m3u8stream "^0.8.6"
miniget "^4.2.2" miniget "^4.2.2"
sax "^1.1.3" sax "^1.1.3"
ytdl-core@^4.11.2: ytsr@^3.8.2:
version "4.11.2" version "3.8.2"
resolved "https://registry.yarnpkg.com/ytdl-core/-/ytdl-core-4.11.2.tgz#c2341916b9757171741a2fa118b6ffbb4ffd92f7" resolved "https://registry.yarnpkg.com/ytsr/-/ytsr-3.8.2.tgz#10a60d0c1adcc3522b0810368c18dff49e875ba7"
integrity sha512-D939t9b4ZzP3v0zDvehR2q+KgG97UTgrTKju3pOPGQcXtl4W6W5z0EpznzcJFu+OOpl7S7Ge8hv8zU65QnxYug== integrity sha512-J+t+a1Ic6jL0Hd0zGX8eFn3uEKtXTf6naa96KO0q7H00GKBfCG8aXW55NAMnaBeUi9Hni6u1xKnf8xZF2F0E/A==
dependencies:
m3u8stream "^0.8.6"
miniget "^4.2.2"
sax "^1.1.3"
ytsr@^3.8.0:
version "3.8.0"
resolved "https://registry.yarnpkg.com/ytsr/-/ytsr-3.8.0.tgz#49a8e5dc413f41515fc3d79d93ee3e073d10e772"
integrity sha512-R+RfYXvBBMAr2e4OxrQ5SBv5x/Mdhmcj1Q8TH0f2HK5d2jbhHOtK4BdzPvLriA6MDoMwqqX04GD8Rpf9UNtSTg==
dependencies: dependencies:
miniget "^4.2.2" miniget "^4.2.2"