diff options
| author | Charlignon <[email protected]> | 2024-04-28 23:13:25 +0200 |
|---|---|---|
| committer | GitHub <[email protected]> | 2024-04-28 16:13:25 -0500 |
| commit | a27598c50abaa920feb68741c27e915d559c0e7a (patch) | |
| tree | e518177428fe0d16050d011778d6fc532901ab78 /src/services | |
| parent | 8e089192060d889910e71a2e6715237a46d264c7 (diff) | |
| download | muse-a27598c50abaa920feb68741c27e915d559c0e7a.tar.xz muse-a27598c50abaa920feb68741c27e915d559c0e7a.zip | |
Add SponsorBlock support (#1013)
Diffstat (limited to 'src/services')
| -rw-r--r-- | src/services/add-query-to-queue.ts | 87 | ||||
| -rw-r--r-- | src/services/config.ts | 4 | ||||
| -rw-r--r-- | src/services/player.ts | 2 |
3 files changed, 90 insertions, 3 deletions
diff --git a/src/services/add-query-to-queue.ts b/src/services/add-query-to-queue.ts index fe72893..317a0f9 100644 --- a/src/services/add-query-to-queue.ts +++ b/src/services/add-query-to-queue.ts @@ -5,15 +5,32 @@ import {inject, injectable} from 'inversify'; import shuffle from 'array-shuffle'; import {TYPES} from '../types.js'; import GetSongs from '../services/get-songs.js'; -import {SongMetadata, STATUS} from './player.js'; +import {MediaSource, SongMetadata, STATUS} from './player.js'; import PlayerManager from '../managers/player.js'; import {buildPlayingMessageEmbed} from '../utils/build-embed.js'; import {getMemberVoiceChannel, getMostPopularVoiceChannel} from '../utils/channels.js'; import {getGuildSettings} from '../utils/get-guild-settings.js'; +import {SponsorBlock} from 'sponsorblock-api'; +import Config from './config'; +import KeyValueCacheProvider from './key-value-cache'; +import {ONE_HOUR_IN_SECONDS} from '../utils/constants'; @injectable() export default class AddQueryToQueue { - constructor(@inject(TYPES.Services.GetSongs) private readonly getSongs: GetSongs, @inject(TYPES.Managers.Player) private readonly playerManager: PlayerManager) { + private readonly sponsorBlock?: SponsorBlock; + private sponsorBlockDisabledUntil?: Date; + private readonly sponsorBlockTimeoutDelay; + private readonly cache: KeyValueCacheProvider; + + constructor(@inject(TYPES.Services.GetSongs) private readonly getSongs: GetSongs, + @inject(TYPES.Managers.Player) private readonly playerManager: PlayerManager, + @inject(TYPES.Config) private readonly config: Config, + @inject(TYPES.KeyValueCache) cache: KeyValueCacheProvider) { + this.sponsorBlockTimeoutDelay = config.SPONSORBLOCK_TIMEOUT; + this.sponsorBlock = config.ENABLE_SPONSORBLOCK + ? new SponsorBlock('muse-sb-integration') // UserID matters only for submissions + : undefined; + this.cache = cache; } public async addToQueue({ @@ -118,6 +135,10 @@ export default class AddQueryToQueue { newSongs = shuffle(newSongs); } + if (this.config.ENABLE_SPONSORBLOCK) { + newSongs = await Promise.all(newSongs.map(this.skipNonMusicSegments.bind(this))); + } + newSongs.forEach(song => { player.add({ ...song, @@ -167,4 +188,66 @@ export default class AddQueryToQueue { await interaction.editReply(`u betcha, **${firstSong.title}** and ${newSongs.length - 1} other songs were added to the queue${extraMsg}`); } } + + private async skipNonMusicSegments(song: SongMetadata) { + if (!this.sponsorBlock + || (this.sponsorBlockDisabledUntil && new Date() < this.sponsorBlockDisabledUntil) + || song.source !== MediaSource.Youtube + || !song.url) { + return song; + } + + try { + const segments = await this.cache.wrap( + async () => this.sponsorBlock?.getSegments(song.url, ['music_offtopic']), + { + key: song.url, // Value is too short for hashing + expiresIn: ONE_HOUR_IN_SECONDS, + }, + ) ?? []; + const skipSegments = segments + .sort((a, b) => a.startTime - b.startTime) + .reduce((acc: Array<{startTime: number; endTime: number}>, {startTime, endTime}) => { + const previousSegment = acc[acc.length - 1]; + // If segments overlap merge + if (previousSegment && previousSegment.endTime > startTime) { + acc[acc.length - 1].endTime = endTime; + } else { + acc.push({startTime, endTime}); + } + + return acc; + }, []); + + const intro = skipSegments[0]; + const outro = skipSegments.at(-1); + if (outro && outro?.endTime >= song.length - 2) { + song.length -= outro.endTime - outro.startTime; + } + + if (intro?.startTime <= 2) { + song.offset = Math.floor(intro.endTime); + song.length -= song.offset; + } + + return song; + } catch (e) { + if (!(e instanceof Error)) { + console.error('Unexpected event occurred while fetching skip segments : ', e); + return song; + } + + if (!e.message.includes('404')) { + // Don't log 404 response, it just means that there are no segments for given video + console.warn(`Could not fetch skip segments for "${song.url}" :`, e); + } + + if (e.message.includes('504')) { + // Stop fetching SponsorBlock data when servers are down + this.sponsorBlockDisabledUntil = new Date(new Date().getTime() + (this.sponsorBlockTimeoutDelay * 60_000)); + } + + return song; + } + } } diff --git a/src/services/config.ts b/src/services/config.ts index 1cafd3c..b6b9aea 100644 --- a/src/services/config.ts +++ b/src/services/config.ts @@ -22,6 +22,8 @@ const CONFIG_MAP = { BOT_ACTIVITY_TYPE: process.env.BOT_ACTIVITY_TYPE ?? 'LISTENING', BOT_ACTIVITY_URL: process.env.BOT_ACTIVITY_URL ?? '', BOT_ACTIVITY: process.env.BOT_ACTIVITY ?? 'music', + ENABLE_SPONSORBLOCK: process.env.ENABLE_SPONSORBLOCK === 'true', + SPONSORBLOCK_TIMEOUT: process.env.ENABLE_SPONSORBLOCK ?? 5, } as const; const BOT_ACTIVITY_TYPE_MAP = { @@ -45,6 +47,8 @@ export default class Config { readonly BOT_ACTIVITY_TYPE!: Exclude<ActivityType, ActivityType.Custom>; readonly BOT_ACTIVITY_URL!: string; readonly BOT_ACTIVITY!: string; + readonly ENABLE_SPONSORBLOCK!: boolean; + readonly SPONSORBLOCK_TIMEOUT!: number; constructor() { for (const [key, value] of Object.entries(CONFIG_MAP)) { diff --git a/src/services/player.ts b/src/services/player.ts index 72c7604..2ec177c 100644 --- a/src/services/player.ts +++ b/src/services/player.ts @@ -34,7 +34,7 @@ export interface QueuedPlaylist { export interface SongMetadata { title: string; artist: string; - url: string; + url: string; // For YT, it's the video ID (not the full URI) length: number; offset: number; playlist: QueuedPlaylist | null; |
