aboutsummaryrefslogtreecommitdiff
path: root/src/services
diff options
context:
space:
mode:
authorCharlignon <[email protected]>2024-04-28 23:13:25 +0200
committerGitHub <[email protected]>2024-04-28 16:13:25 -0500
commita27598c50abaa920feb68741c27e915d559c0e7a (patch)
treee518177428fe0d16050d011778d6fc532901ab78 /src/services
parent8e089192060d889910e71a2e6715237a46d264c7 (diff)
downloadmuse-a27598c50abaa920feb68741c27e915d559c0e7a.tar.xz
muse-a27598c50abaa920feb68741c27e915d559c0e7a.zip
Add SponsorBlock support (#1013)
Diffstat (limited to 'src/services')
-rw-r--r--src/services/add-query-to-queue.ts87
-rw-r--r--src/services/config.ts4
-rw-r--r--src/services/player.ts2
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;