mirror of
https://github.com/informaticker/discord-jellyfin-bot.git
synced 2024-11-24 18:41:57 +01:00
✨ Add paged playlist (#106)
This commit is contained in:
parent
12065e6c90
commit
916969f07b
@ -21,8 +21,8 @@
|
||||
"test:e2e": "jest --config ./test/jest-e2e.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@discord-nestjs/common": "^5.2.0",
|
||||
"@discord-nestjs/core": "^5.3.0",
|
||||
"@discord-nestjs/common": "^5.2.1",
|
||||
"@discord-nestjs/core": "^5.3.3",
|
||||
"@discordjs/opus": "^0.9.0",
|
||||
"@discordjs/voice": "^0.14.0",
|
||||
"@jellyfin/sdk": "^0.7.0",
|
||||
|
@ -24,6 +24,9 @@ import { JellyfinClientModule } from './clients/jellyfin/jellyfin.module';
|
||||
JELLYFIN_AUTHENTICATION_USERNAME: Joi.string().required(),
|
||||
JELLYFIN_AUTHENTICATION_PASSWORD: Joi.string().required(),
|
||||
UPDATER_DISABLE_NOTIFICATIONS: Joi.boolean(),
|
||||
LOG_LEVEL: Joi.string()
|
||||
.valid('error', 'warn', 'log', 'debug', 'verbose')
|
||||
.default('log'),
|
||||
PORT: Joi.number().min(1),
|
||||
}),
|
||||
}),
|
||||
|
@ -22,7 +22,7 @@ import { JellyfinStreamBuilderService } from '../jellyfin/jellyfin.stream.builde
|
||||
import { JellyfinWebSocketService } from '../jellyfin/jellyfin.websocket.service';
|
||||
import { GenericTryHandler } from '../../models/generic-try-handler';
|
||||
import { PlaybackService } from '../../playback/playback.service';
|
||||
import { GenericTrack } from '../../models/shared/GenericTrack';
|
||||
import { Track } from '../../models/shared/Track';
|
||||
|
||||
import { DiscordMessageService } from './discord.message.service';
|
||||
|
||||
@ -41,7 +41,7 @@ export class DiscordVoiceService {
|
||||
) {}
|
||||
|
||||
@OnEvent('internal.audio.announce')
|
||||
handleOnNewTrack(track: GenericTrack) {
|
||||
handleOnNewTrack(track: Track) {
|
||||
const resource = createAudioResource(
|
||||
track.getStreamUrl(this.jellyfinStreamBuilder),
|
||||
);
|
||||
@ -69,7 +69,7 @@ export class DiscordVoiceService {
|
||||
success: false,
|
||||
reply: {
|
||||
embeds: [
|
||||
this.discordMessageService.buildErrorMessage({
|
||||
this.discordMessageService.buildMessage({
|
||||
title: 'Unable to join your channel',
|
||||
description:
|
||||
"I am unable to join your channel, because you don't seem to be in a voice channel. Connect to a channel first to use this command",
|
||||
|
@ -134,6 +134,10 @@ export class JellyfinSearchService {
|
||||
const api = this.jellyfinService.getApi();
|
||||
const remoteImageApi = getRemoteImageApi(api);
|
||||
|
||||
this.logger.verbose(
|
||||
`Searching for remote images of item '${id}' with limit of ${limit}`,
|
||||
);
|
||||
|
||||
try {
|
||||
const axiosReponse = await remoteImageApi.getRemoteImages({
|
||||
itemId: id,
|
||||
@ -151,6 +155,10 @@ export class JellyfinSearchService {
|
||||
TotalRecordCount: 0,
|
||||
};
|
||||
}
|
||||
|
||||
this.logger.verbose(
|
||||
`Retrieved ${axiosReponse.data.TotalRecordCount} remote images from Jellyfin`,
|
||||
);
|
||||
return axiosReponse.data;
|
||||
} catch (err) {
|
||||
this.logger.error(`Failed to retrieve remote images: ${err}`);
|
||||
|
@ -14,7 +14,7 @@ import {
|
||||
PlayNowCommand,
|
||||
SessionApiSendPlaystateCommandRequest,
|
||||
} from '../../types/websocket';
|
||||
import { GenericTrack } from '../../models/shared/GenericTrack';
|
||||
import { Track } from '../../models/shared/Track';
|
||||
|
||||
import { JellyfinSearchService } from './jellyfin.search.service';
|
||||
import { JellyfinService } from './jellyfin.service';
|
||||
|
@ -4,7 +4,7 @@ import { Module } from '@nestjs/common';
|
||||
import { DiscordClientModule } from '../clients/discord/discord.module';
|
||||
import { JellyfinClientModule } from '../clients/jellyfin/jellyfin.module';
|
||||
import { PlaybackModule } from '../playback/playback.module';
|
||||
import { PlaylistCommand } from './playlist.command';
|
||||
import { PlaylistCommand } from './playlist/playlist.command';
|
||||
import { DisconnectCommand } from './disconnect.command';
|
||||
import { HelpCommand } from './help.command';
|
||||
import { PausePlaybackCommand } from './pause.command';
|
||||
@ -14,6 +14,7 @@ import { SkipTrackCommand } from './next.command';
|
||||
import { StatusCommand } from './status.command';
|
||||
import { StopPlaybackCommand } from './stop.command';
|
||||
import { SummonCommand } from './summon.command';
|
||||
import { PlaylistInteractionCollector } from './playlist/playlist.interaction-collector';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@ -24,6 +25,7 @@ import { SummonCommand } from './summon.command';
|
||||
],
|
||||
controllers: [],
|
||||
providers: [
|
||||
PlaylistInteractionCollector,
|
||||
HelpCommand,
|
||||
StatusCommand,
|
||||
PlaylistCommand,
|
||||
|
@ -48,7 +48,7 @@ export class PlayItemCommand {
|
||||
@InteractionEvent(SlashCommandPipe) dto: TrackRequestDto,
|
||||
@IA() interaction: CommandInteraction,
|
||||
): Promise<InteractionReplyOptions | string> {
|
||||
await interaction.deferReply();
|
||||
await interaction.deferReply({ ephemeral: true });
|
||||
|
||||
const baseItems = TrackRequestDto.getBaseItemKinds(dto.type);
|
||||
|
||||
@ -72,6 +72,7 @@ export class PlayItemCommand {
|
||||
description: `- Check for any misspellings\n- Grant me access to your desired libraries\n- Avoid special characters`,
|
||||
}),
|
||||
],
|
||||
ephemeral: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
@ -96,20 +97,18 @@ export class PlayItemCommand {
|
||||
(sum, item) => sum + item.duration,
|
||||
0,
|
||||
);
|
||||
const enqueuedCount = this.playbackService
|
||||
.getPlaylistOrDefault()
|
||||
.enqueueTracks(tracks);
|
||||
|
||||
console.log(tracks);
|
||||
this.playbackService.getPlaylistOrDefault().enqueueTracks(tracks);
|
||||
|
||||
const remoteImage: RemoteImageInfo | undefined = tracks
|
||||
.map((x) => x.getRemoteImage())
|
||||
.flatMap((x) => x.getRemoteImages())
|
||||
.find((x) => true);
|
||||
|
||||
await interaction.followUp({
|
||||
embeds: [
|
||||
this.discordMessageService.buildMessage({
|
||||
title: `Added ${enqueuedCount} tracks to your playlist (${formatMillisecondsAsHumanReadable(
|
||||
title: `Added ${this.playbackService
|
||||
.getPlaylistOrDefault()
|
||||
.getLength()} tracks to your playlist (${formatMillisecondsAsHumanReadable(
|
||||
reducedDuration,
|
||||
)})`,
|
||||
mixin(embedBuilder) {
|
||||
@ -120,6 +119,7 @@ export class PlayItemCommand {
|
||||
},
|
||||
}),
|
||||
],
|
||||
ephemeral: true,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1,88 +0,0 @@
|
||||
import { Command, Handler, IA } from '@discord-nestjs/core';
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { CommandInteraction } from 'discord.js';
|
||||
|
||||
import { PlaybackService } from '../playback/playback.service';
|
||||
import { Constants } from '../utils/constants';
|
||||
import { formatMillisecondsAsHumanReadable } from '../utils/timeUtils';
|
||||
import { DiscordMessageService } from '../clients/discord/discord.message.service';
|
||||
import { trimStringToFixedLength } from '../utils/stringUtils/stringUtils';
|
||||
|
||||
@Injectable()
|
||||
@Command({
|
||||
name: 'playlist',
|
||||
description: 'Print the current track information',
|
||||
})
|
||||
export class PlaylistCommand {
|
||||
constructor(
|
||||
private readonly discordMessageService: DiscordMessageService,
|
||||
private readonly playbackService: PlaybackService,
|
||||
) {}
|
||||
|
||||
@Handler()
|
||||
async handler(@IA() interaction: CommandInteraction): Promise<void> {
|
||||
const playlist = this.playbackService.getPlaylistOrDefault();
|
||||
|
||||
if (playlist.isEmpty()) {
|
||||
await interaction.reply({
|
||||
embeds: [
|
||||
this.discordMessageService.buildMessage({
|
||||
title: 'Your Playlist',
|
||||
description:
|
||||
'You do not have any tracks in your playlist.\nUse the ``/play`` command to add new tracks to your playlist',
|
||||
}),
|
||||
],
|
||||
ephemeral: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const tracklist = playlist.tracks
|
||||
.map((track, index) => {
|
||||
const isCurrent = track === playlist.getActiveTrack();
|
||||
|
||||
let point = this.getListPoint(isCurrent, index);
|
||||
point += `**${trimStringToFixedLength(track.name, 30)}**`;
|
||||
|
||||
if (isCurrent) {
|
||||
point += ' :loud_sound:';
|
||||
}
|
||||
|
||||
point += '\n';
|
||||
point += Constants.Design.InvisibleSpace.repeat(2);
|
||||
point += formatMillisecondsAsHumanReadable(track.getDuration());
|
||||
|
||||
return point;
|
||||
})
|
||||
.slice(0, 10)
|
||||
.join('\n');
|
||||
const remoteImage = undefined;
|
||||
|
||||
await interaction.reply({
|
||||
embeds: [
|
||||
this.discordMessageService.buildMessage({
|
||||
title: 'Your Playlist',
|
||||
description: `${tracklist}\n\nUse the /skip and /previous command to select a track`,
|
||||
mixin(embedBuilder) {
|
||||
if (remoteImage === undefined) {
|
||||
return embedBuilder;
|
||||
}
|
||||
|
||||
return embedBuilder.setThumbnail(remoteImage.Url);
|
||||
},
|
||||
}),
|
||||
],
|
||||
ephemeral: true,
|
||||
});
|
||||
}
|
||||
|
||||
private getListPoint(isCurrent: boolean, index: number) {
|
||||
if (isCurrent) {
|
||||
return `${index + 1}. `;
|
||||
}
|
||||
|
||||
return `${index + 1}. `;
|
||||
}
|
||||
}
|
181
src/commands/playlist/playlist.command.ts
Normal file
181
src/commands/playlist/playlist.command.ts
Normal file
@ -0,0 +1,181 @@
|
||||
import { CollectorInterceptor, SlashCommandPipe } from '@discord-nestjs/common';
|
||||
import {
|
||||
AppliedCollectors,
|
||||
Command,
|
||||
Handler,
|
||||
IA,
|
||||
InteractionEvent,
|
||||
Param,
|
||||
ParamType,
|
||||
UseCollectors,
|
||||
} from '@discord-nestjs/core';
|
||||
|
||||
import { Injectable, Logger, UseInterceptors } from '@nestjs/common';
|
||||
|
||||
import {
|
||||
ActionRowBuilder,
|
||||
ButtonBuilder,
|
||||
ButtonInteraction,
|
||||
ButtonStyle,
|
||||
CommandInteraction,
|
||||
EmbedBuilder,
|
||||
InteractionCollector,
|
||||
InteractionReplyOptions,
|
||||
InteractionUpdateOptions,
|
||||
} from 'discord.js';
|
||||
|
||||
import { PlaybackService } from '../../playback/playback.service';
|
||||
import { chunkArray } from '../../utils/arrayUtils';
|
||||
import { Constants } from '../../utils/constants';
|
||||
import { formatMillisecondsAsHumanReadable } from '../../utils/timeUtils';
|
||||
import { DiscordMessageService } from '../../clients/discord/discord.message.service';
|
||||
import { Track } from '../../models/shared/Track';
|
||||
import { trimStringToFixedLength } from '../../utils/stringUtils/stringUtils';
|
||||
|
||||
import { PlaylistInteractionCollector } from './playlist.interaction-collector';
|
||||
|
||||
class PlaylistCommandDto {
|
||||
@Param({
|
||||
required: false,
|
||||
description: 'The page',
|
||||
type: ParamType.INTEGER,
|
||||
})
|
||||
page: number;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
@Command({
|
||||
name: 'playlist',
|
||||
description: 'Print the current track information',
|
||||
})
|
||||
@UseInterceptors(CollectorInterceptor)
|
||||
@UseCollectors(PlaylistInteractionCollector)
|
||||
export class PlaylistCommand {
|
||||
public pageData: Map<string, number> = new Map();
|
||||
private readonly logger = new Logger(PlaylistCommand.name);
|
||||
|
||||
constructor(
|
||||
private readonly discordMessageService: DiscordMessageService,
|
||||
private readonly playbackService: PlaybackService,
|
||||
) {}
|
||||
|
||||
@Handler()
|
||||
async handler(
|
||||
@InteractionEvent(SlashCommandPipe) dto: PlaylistCommandDto,
|
||||
@IA() interaction: CommandInteraction,
|
||||
@AppliedCollectors(0) collector: InteractionCollector<ButtonInteraction>,
|
||||
): Promise<void> {
|
||||
const page = dto.page ?? 0;
|
||||
|
||||
const response = await interaction.reply(
|
||||
this.getReplyForPage(page) as InteractionReplyOptions,
|
||||
);
|
||||
|
||||
this.pageData.set(response.id, page);
|
||||
this.logger.debug(
|
||||
`Added '${interaction.id}' as a message id for page storage`,
|
||||
);
|
||||
}
|
||||
|
||||
private getChunks() {
|
||||
const playlist = this.playbackService.getPlaylistOrDefault();
|
||||
return chunkArray(playlist.tracks, 10);
|
||||
}
|
||||
|
||||
public getReplyForPage(
|
||||
page: number,
|
||||
): InteractionReplyOptions | InteractionUpdateOptions {
|
||||
const chunks = this.getChunks();
|
||||
|
||||
if (page >= chunks.length) {
|
||||
return {
|
||||
embeds: [
|
||||
this.discordMessageService.buildMessage({
|
||||
title: 'Page does not exist',
|
||||
description: 'Please pass a valid page',
|
||||
}),
|
||||
],
|
||||
ephemeral: true,
|
||||
};
|
||||
}
|
||||
|
||||
const contentForPage = this.getContentForPage(chunks, page);
|
||||
|
||||
if (!contentForPage) {
|
||||
return {
|
||||
embeds: [
|
||||
this.discordMessageService.buildMessage({
|
||||
title: 'Your Playlist',
|
||||
description:
|
||||
'You do not have any tracks in your playlist.\nUse the ``/play`` command to add new tracks to your playlist',
|
||||
}),
|
||||
],
|
||||
ephemeral: true,
|
||||
};
|
||||
}
|
||||
|
||||
const hasPrevious = page;
|
||||
const hasNext = page + 1 < chunks.length;
|
||||
|
||||
const rowBuilder = new ActionRowBuilder<ButtonBuilder>().addComponents(
|
||||
new ButtonBuilder()
|
||||
.setDisabled(!hasPrevious)
|
||||
.setCustomId('playlist-controls-previous')
|
||||
.setEmoji('◀️')
|
||||
.setLabel('Previous')
|
||||
.setStyle(ButtonStyle.Secondary),
|
||||
new ButtonBuilder()
|
||||
.setDisabled(!hasNext)
|
||||
.setCustomId('playlist-controls-next')
|
||||
.setEmoji('▶️')
|
||||
.setLabel('Next')
|
||||
.setStyle(ButtonStyle.Secondary),
|
||||
);
|
||||
|
||||
return {
|
||||
embeds: [contentForPage.toJSON()],
|
||||
ephemeral: true,
|
||||
components: [rowBuilder],
|
||||
};
|
||||
}
|
||||
|
||||
private getContentForPage(
|
||||
chunks: Track[][],
|
||||
page: number,
|
||||
): EmbedBuilder | undefined {
|
||||
const playlist = this.playbackService.getPlaylistOrDefault();
|
||||
|
||||
if (page >= chunks.length || page < 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const content = chunks[page]
|
||||
.map((track, index) => {
|
||||
const isCurrent = track === playlist.getActiveTrack();
|
||||
|
||||
let point = this.getListPoint(isCurrent, index);
|
||||
point += `**${trimStringToFixedLength(track.name, 30)}**`;
|
||||
|
||||
if (isCurrent) {
|
||||
point += ' :loud_sound:';
|
||||
}
|
||||
|
||||
point += '\n';
|
||||
point += Constants.Design.InvisibleSpace.repeat(2);
|
||||
point += formatMillisecondsAsHumanReadable(track.getDuration());
|
||||
|
||||
return point;
|
||||
})
|
||||
.join('\n');
|
||||
|
||||
return new EmbedBuilder().setTitle('Your playlist').setDescription(content);
|
||||
}
|
||||
|
||||
private getListPoint(isCurrent: boolean, index: number) {
|
||||
if (isCurrent) {
|
||||
return `${index + 1}. `;
|
||||
}
|
||||
|
||||
return `${index + 1}. `;
|
||||
}
|
||||
}
|
78
src/commands/playlist/playlist.interaction-collector.ts
Normal file
78
src/commands/playlist/playlist.interaction-collector.ts
Normal file
@ -0,0 +1,78 @@
|
||||
import {
|
||||
Filter,
|
||||
InjectCauseEvent,
|
||||
InteractionEventCollector,
|
||||
On,
|
||||
} from '@discord-nestjs/core';
|
||||
|
||||
import { forwardRef, Inject, Injectable, Scope } from '@nestjs/common';
|
||||
import { Logger } from '@nestjs/common/services';
|
||||
|
||||
import {
|
||||
ButtonInteraction,
|
||||
ChatInputCommandInteraction,
|
||||
InteractionUpdateOptions,
|
||||
} from 'discord.js';
|
||||
|
||||
import { PlaylistCommand } from './playlist.command';
|
||||
|
||||
@Injectable({ scope: Scope.REQUEST })
|
||||
@InteractionEventCollector({ time: 15 * 1000 })
|
||||
export class PlaylistInteractionCollector {
|
||||
private readonly logger = new Logger(PlaylistInteractionCollector.name);
|
||||
|
||||
constructor(
|
||||
@Inject(forwardRef(() => PlaylistCommand))
|
||||
private readonly playlistCommand: PlaylistCommand,
|
||||
@InjectCauseEvent()
|
||||
private readonly causeInteraction: ChatInputCommandInteraction,
|
||||
) {}
|
||||
|
||||
@Filter()
|
||||
filter(interaction: ButtonInteraction): boolean {
|
||||
return this.causeInteraction.id === interaction.message.interaction.id;
|
||||
}
|
||||
|
||||
@On('collect')
|
||||
async onCollect(interaction: ButtonInteraction): Promise<void> {
|
||||
const targetPage = this.getInteraction(interaction);
|
||||
|
||||
if (targetPage === undefined) {
|
||||
await interaction.update({
|
||||
content: 'Unknown error',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.debug(
|
||||
`Updating current page for interaction ${this.causeInteraction.id} to ${targetPage}`,
|
||||
);
|
||||
this.playlistCommand.pageData.set(this.causeInteraction.id, targetPage);
|
||||
const reply = this.playlistCommand.getReplyForPage(targetPage);
|
||||
await interaction.update(reply as InteractionUpdateOptions);
|
||||
}
|
||||
|
||||
private getInteraction(interaction: ButtonInteraction): number | null {
|
||||
const current = this.playlistCommand.pageData.get(this.causeInteraction.id);
|
||||
|
||||
this.logger.debug(
|
||||
`Retrieved current page from command using id '${
|
||||
this.causeInteraction.id
|
||||
}' in list of ${
|
||||
Object.keys(this.playlistCommand.pageData).length
|
||||
}: ${current}`,
|
||||
);
|
||||
|
||||
switch (interaction.customId) {
|
||||
case 'playlist-controls-next':
|
||||
return current + 1;
|
||||
case 'playlist-controls-previous':
|
||||
return current - 1;
|
||||
default:
|
||||
this.logger.error(
|
||||
`Unable to map button interaction from collector to target page`,
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
18
src/main.ts
18
src/main.ts
@ -4,11 +4,21 @@ import { NestFactory } from '@nestjs/core';
|
||||
import { AppModule } from './app.module';
|
||||
|
||||
function getLoggingLevels(): LogLevel[] {
|
||||
if (process.env.DEBUG) {
|
||||
return ['log', 'error', 'warn', 'debug'];
|
||||
switch (process.env.LOG_LEVEL.toLowerCase()) {
|
||||
case 'error':
|
||||
return ['error'];
|
||||
case 'warn':
|
||||
return ['error', 'warn'];
|
||||
case 'log':
|
||||
return ['error', 'warn', 'log'];
|
||||
case 'debug':
|
||||
return ['error', 'warn', 'log', 'debug'];
|
||||
case 'verbose':
|
||||
return ['error', 'warn', 'log', 'debug', 'verbose'];
|
||||
default:
|
||||
console.log(`failed to process log level ${process.env.LOG_LEVEL}`);
|
||||
return ['error', 'warn', 'log'];
|
||||
}
|
||||
|
||||
return ['log', 'error', 'warn'];
|
||||
}
|
||||
|
||||
async function bootstrap() {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { SearchHint as JellyfinSearchHint } from '@jellyfin/sdk/lib/generated-client/models';
|
||||
|
||||
import { GenericTrack } from '../shared/GenericTrack';
|
||||
import { Track } from '../shared/Track';
|
||||
import { JellyfinSearchService } from '../../clients/jellyfin/jellyfin.search.service';
|
||||
|
||||
import { SearchHint } from './SearchHint';
|
||||
@ -16,7 +16,7 @@ export class AlbumSearchHint extends SearchHint {
|
||||
|
||||
override async toTracks(
|
||||
searchService: JellyfinSearchService,
|
||||
): Promise<GenericTrack[]> {
|
||||
): Promise<Track[]> {
|
||||
const albumItems = await searchService.getAlbumItems(this.id);
|
||||
const tracks = albumItems.map(async (x) =>
|
||||
(await x.toTracks(searchService)).find((x) => x !== null),
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { SearchHint as JellyfinSearchHint } from '@jellyfin/sdk/lib/generated-client/models';
|
||||
|
||||
import { GenericTrack } from '../shared/GenericTrack';
|
||||
import { Track } from '../shared/Track';
|
||||
import { JellyfinSearchService } from '../../clients/jellyfin/jellyfin.search.service';
|
||||
|
||||
import { SearchHint } from './SearchHint';
|
||||
@ -20,7 +20,7 @@ export class PlaylistSearchHint extends SearchHint {
|
||||
|
||||
override async toTracks(
|
||||
searchService: JellyfinSearchService,
|
||||
): Promise<GenericTrack[]> {
|
||||
): Promise<Track[]> {
|
||||
const playlistItems = await searchService.getPlaylistitems(this.id);
|
||||
const tracks = playlistItems.map(async (x) =>
|
||||
(await x.toTracks(searchService)).find((x) => x !== null),
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { SearchHint as JellyfinSearchHint } from '@jellyfin/sdk/lib/generated-client/models';
|
||||
|
||||
import { GenericTrack } from '../shared/GenericTrack';
|
||||
import { Track } from '../shared/Track';
|
||||
import { JellyfinSearchService } from '../../clients/jellyfin/jellyfin.search.service';
|
||||
|
||||
export class SearchHint {
|
||||
@ -14,17 +14,10 @@ export class SearchHint {
|
||||
return `🎵 ${this.name}`;
|
||||
}
|
||||
|
||||
async toTracks(
|
||||
searchService: JellyfinSearchService,
|
||||
): Promise<GenericTrack[]> {
|
||||
async toTracks(searchService: JellyfinSearchService): Promise<Track[]> {
|
||||
const remoteImages = await searchService.getRemoteImageById(this.id);
|
||||
return [
|
||||
new GenericTrack(
|
||||
this.id,
|
||||
this.name,
|
||||
this.runtimeInMilliseconds,
|
||||
remoteImages,
|
||||
),
|
||||
new Track(this.id, this.name, this.runtimeInMilliseconds, remoteImages),
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
|
||||
import { GenericTrack } from './GenericTrack';
|
||||
import { Track } from './Track';
|
||||
|
||||
export class GenericPlaylist {
|
||||
tracks: GenericTrack[];
|
||||
export class Playlist {
|
||||
tracks: Track[];
|
||||
activeTrackIndex?: number;
|
||||
|
||||
constructor(private readonly eventEmitter: EventEmitter2) {
|
||||
@ -23,7 +23,7 @@ export class GenericPlaylist {
|
||||
* Checks if the active track is out of bounds
|
||||
* @returns active track or undefined if there's none
|
||||
*/
|
||||
getActiveTrack(): GenericTrack | undefined {
|
||||
getActiveTrack(): Track | undefined {
|
||||
if (this.isActiveTrackOutOfSync()) {
|
||||
return undefined;
|
||||
}
|
||||
@ -40,6 +40,10 @@ export class GenericPlaylist {
|
||||
);
|
||||
}
|
||||
|
||||
getLength() {
|
||||
return this.tracks.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Go to the next track in the playlist
|
||||
* @returns if the track has been changed successfully
|
||||
@ -79,13 +83,18 @@ export class GenericPlaylist {
|
||||
* @param tracks the tracks that should be added
|
||||
* @returns the new lendth of the tracks in the playlist
|
||||
*/
|
||||
enqueueTracks(tracks: GenericTrack[]) {
|
||||
enqueueTracks(tracks: Track[]) {
|
||||
this.eventEmitter.emit('controls.playlist.tracks.enqueued', {
|
||||
count: tracks.length,
|
||||
activeTrack: this.activeTrackIndex,
|
||||
});
|
||||
const length = this.tracks.push(...tracks);
|
||||
this.announceTrackChange();
|
||||
|
||||
// emit a track change if there is no item
|
||||
if (!this.activeTrackIndex) {
|
||||
this.announceTrackChange();
|
||||
}
|
||||
|
||||
return length;
|
||||
}
|
||||
|
@ -1,8 +1,11 @@
|
||||
import { RemoteImageInfo, RemoteImageResult } from '@jellyfin/sdk/lib/generated-client/models';
|
||||
import {
|
||||
RemoteImageInfo,
|
||||
RemoteImageResult,
|
||||
} from '@jellyfin/sdk/lib/generated-client/models';
|
||||
|
||||
import { JellyfinStreamBuilderService } from '../../clients/jellyfin/jellyfin.stream.builder.service';
|
||||
|
||||
export class GenericTrack {
|
||||
export class Track {
|
||||
/**
|
||||
* The identifier of this track, structured as a UID.
|
||||
* This id can be used to build a stream url and send more API requests to Jellyfin
|
||||
@ -44,7 +47,7 @@ export class GenericTrack {
|
||||
return streamBuilder.buildStreamUrl(this.id, 96000);
|
||||
}
|
||||
|
||||
getRemoteImage(): RemoteImageInfo | undefined {
|
||||
return this.remoteImages.Images.find((x) => true);
|
||||
getRemoteImages(): RemoteImageInfo[] {
|
||||
return this.remoteImages.Images;
|
||||
}
|
||||
}
|
@ -1,21 +1,21 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
|
||||
import { GenericPlaylist } from '../models/shared/GenericPlaylist';
|
||||
import { Playlist } from '../models/shared/Playlist';
|
||||
|
||||
@Injectable()
|
||||
export class PlaybackService {
|
||||
private readonly logger = new Logger(PlaybackService.name);
|
||||
private playlist: GenericPlaylist | undefined = undefined;
|
||||
private playlist: Playlist | undefined = undefined;
|
||||
|
||||
constructor(private readonly eventEmitter: EventEmitter2) {}
|
||||
|
||||
getPlaylistOrDefault(): GenericPlaylist {
|
||||
getPlaylistOrDefault(): Playlist {
|
||||
if (this.playlist) {
|
||||
return this.playlist;
|
||||
}
|
||||
|
||||
this.playlist = new GenericPlaylist(this.eventEmitter);
|
||||
this.playlist = new Playlist(this.eventEmitter);
|
||||
return this.playlist;
|
||||
}
|
||||
}
|
||||
|
4
src/utils/arrayUtils.ts
Normal file
4
src/utils/arrayUtils.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export const chunkArray = <T>(a: T[], size): T[][] =>
|
||||
Array.from(new Array(Math.ceil(a.length / size)), (_, i) =>
|
||||
a.slice(i * size, i * size + size),
|
||||
);
|
@ -1,11 +1,17 @@
|
||||
import { formatDuration, intervalToDuration } from 'date-fns';
|
||||
|
||||
export const formatMillisecondsAsHumanReadable = (milliseconds: number) => {
|
||||
export const formatMillisecondsAsHumanReadable = (
|
||||
milliseconds: number,
|
||||
format = ['years', 'months', 'weeks', 'days', 'hours', 'minutes', 'seconds'],
|
||||
) => {
|
||||
const duration = formatDuration(
|
||||
intervalToDuration({
|
||||
start: milliseconds,
|
||||
end: 0,
|
||||
}),
|
||||
{
|
||||
format: format,
|
||||
},
|
||||
);
|
||||
return duration;
|
||||
};
|
||||
|
16
yarn.lock
16
yarn.lock
@ -359,19 +359,19 @@
|
||||
dependencies:
|
||||
"@jridgewell/trace-mapping" "0.3.9"
|
||||
|
||||
"@discord-nestjs/common@^5.2.0":
|
||||
version "5.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@discord-nestjs/common/-/common-5.2.0.tgz#3bdf25eadf8372d81110e2aeefbb31e707e75554"
|
||||
integrity sha512-aXp6P7XyDk/Zoz9zpe5DLqGFBfZrz1fu6Vc8oMz2RggVxBm8k8P5bH5iOcIvI0jWsjbZ3pVCVB3SmCpjBFItRA==
|
||||
"@discord-nestjs/common@^5.2.1":
|
||||
version "5.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@discord-nestjs/common/-/common-5.2.1.tgz#113a6a67481c9bb5d2e7a0ee76ee61dea555c489"
|
||||
integrity sha512-6JP53oA6Fysh1Xj3i30zaJTQIZWoPiigqbHjjzPFOMUSjKbaIEX0/75gZm0JBHCPw9oUnVBGq8taV200pyiosg==
|
||||
dependencies:
|
||||
"@nestjs/mapped-types" "1.2.2"
|
||||
class-transformer "0.5.1"
|
||||
class-validator "0.14.0"
|
||||
|
||||
"@discord-nestjs/core@^5.3.0":
|
||||
version "5.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@discord-nestjs/core/-/core-5.3.0.tgz#8e93b0310e8cc2c0cde74c6317d949d6e9d28d2d"
|
||||
integrity sha512-eHVzuPCu3EbQyTln+ZEH0/Jwe0xPG7Z1eZV655jZoCSpq7RmvxWcTG/REf3XMSgtYFN27HaXa9vPCsgUOnp9xQ==
|
||||
"@discord-nestjs/core@^5.3.3":
|
||||
version "5.3.3"
|
||||
resolved "https://registry.yarnpkg.com/@discord-nestjs/core/-/core-5.3.3.tgz#0e0af8cfc7b1c6df0dd9668573a51b44c3940033"
|
||||
integrity sha512-R3duQIUU9qQiKEIyleG2swdDdGp3FXaXHbgooVieyEJVx8tvIulH5BryE0lnYiUdPEvXbZrBF+w476PATiDWMQ==
|
||||
dependencies:
|
||||
class-transformer "0.5.1"
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user