mirror of
https://github.com/informaticker/discord-jellyfin-bot.git
synced 2024-11-25 02:51:57 +01:00
✨ Add time tracking
This commit is contained in:
parent
831e03a77f
commit
d1fc61c6fe
@ -43,7 +43,8 @@
|
|||||||
"rimraf": "^4.4.1",
|
"rimraf": "^4.4.1",
|
||||||
"rxjs": "^7.2.0",
|
"rxjs": "^7.2.0",
|
||||||
"uuid": "^9.0.0",
|
"uuid": "^9.0.0",
|
||||||
"ws": "^8.13.0"
|
"ws": "^8.13.0",
|
||||||
|
"zod": "^3.21.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nestjs/cli": "^9.3.0",
|
"@nestjs/cli": "^9.3.0",
|
||||||
|
@ -14,6 +14,7 @@ import {
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { Logger } from '@nestjs/common/services';
|
import { Logger } from '@nestjs/common/services';
|
||||||
import { EventEmitter2, OnEvent } from '@nestjs/event-emitter';
|
import { EventEmitter2, OnEvent } from '@nestjs/event-emitter';
|
||||||
|
import { Interval } from '@nestjs/schedule';
|
||||||
|
|
||||||
import { GuildMember } from 'discord.js';
|
import { GuildMember } from 'discord.js';
|
||||||
|
|
||||||
@ -224,7 +225,7 @@ export class DiscordVoiceService {
|
|||||||
|
|
||||||
if (this.audioPlayer === undefined) {
|
if (this.audioPlayer === undefined) {
|
||||||
this.logger.debug(
|
this.logger.debug(
|
||||||
"Initialized new instance of AudioPlayer because it has not been defined yet",
|
'Initialized new instance of AudioPlayer because it has not been defined yet',
|
||||||
);
|
);
|
||||||
this.audioPlayer = createAudioPlayer({
|
this.audioPlayer = createAudioPlayer({
|
||||||
debug: process.env.DEBUG?.toLowerCase() === 'true',
|
debug: process.env.DEBUG?.toLowerCase() === 'true',
|
||||||
@ -291,7 +292,7 @@ export class DiscordVoiceService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.debug("Audio player finished playing old resource");
|
this.logger.debug('Audio player finished playing old resource');
|
||||||
|
|
||||||
const playlist = this.playbackService.getPlaylistOrDefault();
|
const playlist = this.playbackService.getPlaylistOrDefault();
|
||||||
const finishedTrack = playlist.getActiveTrack();
|
const finishedTrack = playlist.getActiveTrack();
|
||||||
@ -308,11 +309,43 @@ export class DiscordVoiceService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!hasNextTrack) {
|
if (!hasNextTrack) {
|
||||||
this.logger.debug("Reached the end of the playlist");
|
this.logger.debug('Reached the end of the playlist');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.playbackService.getPlaylistOrDefault().setNextTrackAsActiveTrack();
|
this.playbackService.getPlaylistOrDefault().setNextTrackAsActiveTrack();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Interval(500)
|
||||||
|
private checkAudioResourcePlayback() {
|
||||||
|
if (!this.audioResource) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const progress = this.audioResource.playbackDuration;
|
||||||
|
|
||||||
|
const playlist = this.playbackService.getPlaylistOrDefault();
|
||||||
|
|
||||||
|
if (!playlist) {
|
||||||
|
this.logger.error(
|
||||||
|
`Failed to update ellapsed audio time because playlist was unexpectitly undefined`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeTrack = playlist.getActiveTrack();
|
||||||
|
|
||||||
|
if (!activeTrack) {
|
||||||
|
this.logger.error(
|
||||||
|
`Failed to update ellapsed audio time because active track was unexpectitly undefined`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
activeTrack.updatePlaybackProgress(progress);
|
||||||
|
this.logger.verbose(
|
||||||
|
`Reporting progress: ${progress} on track ${activeTrack.id}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,7 @@ import { getSessionApi } from '@jellyfin/sdk/lib/utils/api/session-api';
|
|||||||
|
|
||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import { OnEvent } from '@nestjs/event-emitter';
|
import { OnEvent } from '@nestjs/event-emitter';
|
||||||
|
import { Interval } from '@nestjs/schedule';
|
||||||
import { Track } from '../../models/shared/Track';
|
import { Track } from '../../models/shared/Track';
|
||||||
|
|
||||||
import { PlaybackService } from '../../playback/playback.service';
|
import { PlaybackService } from '../../playback/playback.service';
|
||||||
@ -52,6 +53,7 @@ export class JellyinPlaystateService {
|
|||||||
await this.playstateApi.reportPlaybackStart({
|
await this.playstateApi.reportPlaybackStart({
|
||||||
playbackStartInfo: {
|
playbackStartInfo: {
|
||||||
ItemId: track.id,
|
ItemId: track.id,
|
||||||
|
PositionTicks: 0,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -60,7 +62,7 @@ export class JellyinPlaystateService {
|
|||||||
private async onPlaybackFinished(track: Track) {
|
private async onPlaybackFinished(track: Track) {
|
||||||
if (!track) {
|
if (!track) {
|
||||||
this.logger.error(
|
this.logger.error(
|
||||||
"Unable to report playback because finished track was undefined",
|
'Unable to report playback because finished track was undefined',
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -68,6 +70,7 @@ export class JellyinPlaystateService {
|
|||||||
await this.playstateApi.reportPlaybackStopped({
|
await this.playstateApi.reportPlaybackStopped({
|
||||||
playbackStopInfo: {
|
playbackStopInfo: {
|
||||||
ItemId: track.id,
|
ItemId: track.id,
|
||||||
|
PositionTicks: track.playbackProgress * 10000,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -78,7 +81,7 @@ export class JellyinPlaystateService {
|
|||||||
|
|
||||||
if (!track) {
|
if (!track) {
|
||||||
this.logger.error(
|
this.logger.error(
|
||||||
"Unable to report changed playstate to Jellyfin because no track was active",
|
'Unable to report changed playstate to Jellyfin because no track was active',
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -87,7 +90,28 @@ export class JellyinPlaystateService {
|
|||||||
playbackProgressInfo: {
|
playbackProgressInfo: {
|
||||||
IsPaused: paused,
|
IsPaused: paused,
|
||||||
ItemId: track.id,
|
ItemId: track.id,
|
||||||
|
PositionTicks: track.playbackProgress * 10000,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Interval(1000)
|
||||||
|
private async onPlaybackProgress() {
|
||||||
|
const track = this.playbackService.getPlaylistOrDefault().getActiveTrack();
|
||||||
|
|
||||||
|
if (!track) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.playstateApi.reportPlaybackProgress({
|
||||||
|
playbackProgressInfo: {
|
||||||
|
ItemId: track.id,
|
||||||
|
PositionTicks: track.playbackProgress * 10000,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.logger.verbose(
|
||||||
|
`Reported playback progress ${track.playbackProgress} to Jellyfin for item ${track.id}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -70,7 +70,8 @@ export class PlayItemCommand {
|
|||||||
embeds: [
|
embeds: [
|
||||||
this.discordMessageService.buildMessage({
|
this.discordMessageService.buildMessage({
|
||||||
title: 'No results found',
|
title: 'No results found',
|
||||||
description: "- Check for any misspellings\n- Grant me access to your desired libraries\n- Avoid special characters",
|
description:
|
||||||
|
'- Check for any misspellings\n- Grant me access to your desired libraries\n- Avoid special characters',
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
ephemeral: true,
|
ephemeral: true,
|
||||||
@ -136,15 +137,8 @@ export class PlayItemCommand {
|
|||||||
|
|
||||||
const focusedAutoCompleteAction = interaction.options.getFocused(true);
|
const focusedAutoCompleteAction = interaction.options.getFocused(true);
|
||||||
const typeIndex = interaction.options.getInteger('type');
|
const typeIndex = interaction.options.getInteger('type');
|
||||||
|
const type =
|
||||||
if (typeIndex === null) {
|
typeIndex !== null ? Object.values(SearchType)[typeIndex] : undefined;
|
||||||
this.logger.error(
|
|
||||||
`Failed to get type integer from play command interaction autocomplete`,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const type = Object.values(SearchType)[typeIndex] as SearchType;
|
|
||||||
const searchQuery = focusedAutoCompleteAction.value;
|
const searchQuery = focusedAutoCompleteAction.value;
|
||||||
|
|
||||||
if (!searchQuery || searchQuery.length < 1) {
|
if (!searchQuery || searchQuery.length < 1) {
|
||||||
|
@ -22,16 +22,19 @@ import {
|
|||||||
InteractionUpdateOptions,
|
InteractionUpdateOptions,
|
||||||
} from 'discord.js';
|
} 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 { DiscordMessageService } from '../../clients/discord/discord.message.service';
|
||||||
import { Track } from '../../models/shared/Track';
|
import { Track } from '../../models/shared/Track';
|
||||||
import { trimStringToFixedLength } from '../../utils/stringUtils/stringUtils';
|
import { PlaybackService } from '../../playback/playback.service';
|
||||||
|
import { chunkArray } from '../../utils/arrayUtils';
|
||||||
|
import { trimStringToFixedLength, zeroPad } from '../../utils/stringUtils/stringUtils';
|
||||||
|
|
||||||
|
import { Interval } from '@nestjs/schedule';
|
||||||
|
import { lightFormat } from 'date-fns';
|
||||||
import { PlaylistInteractionCollector } from './playlist.interaction-collector';
|
import { PlaylistInteractionCollector } from './playlist.interaction-collector';
|
||||||
import { PlaylistCommandParams } from './playlist.params';
|
import { PlaylistCommandParams } from './playlist.params';
|
||||||
|
import { PlaylistTempCommandData } from './playlist.types';
|
||||||
|
import { tr } from 'date-fns/locale';
|
||||||
|
import { takeCoverage } from 'v8';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@Command({
|
@Command({
|
||||||
@ -41,7 +44,7 @@ import { PlaylistCommandParams } from './playlist.params';
|
|||||||
@UseInterceptors(CollectorInterceptor)
|
@UseInterceptors(CollectorInterceptor)
|
||||||
@UseCollectors(PlaylistInteractionCollector)
|
@UseCollectors(PlaylistInteractionCollector)
|
||||||
export class PlaylistCommand {
|
export class PlaylistCommand {
|
||||||
public pageData: Map<string, number> = new Map();
|
public pageData: Map<string, PlaylistTempCommandData> = new Map();
|
||||||
private readonly logger = new Logger(PlaylistCommand.name);
|
private readonly logger = new Logger(PlaylistCommand.name);
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@ -61,7 +64,10 @@ export class PlaylistCommand {
|
|||||||
this.getReplyForPage(page) as InteractionReplyOptions,
|
this.getReplyForPage(page) as InteractionReplyOptions,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.pageData.set(interaction.id, page);
|
this.pageData.set(interaction.id, {
|
||||||
|
page,
|
||||||
|
interaction,
|
||||||
|
});
|
||||||
this.logger.debug(
|
this.logger.debug(
|
||||||
`Added '${interaction.id}' as a message id for page storage`,
|
`Added '${interaction.id}' as a message id for page storage`,
|
||||||
);
|
);
|
||||||
@ -82,6 +88,36 @@ export class PlaylistCommand {
|
|||||||
return chunkArray(playlist.tracks, 10);
|
return chunkArray(playlist.tracks, 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private createInterval(interaction: CommandInteraction) {
|
||||||
|
return setInterval(async () => {
|
||||||
|
const tempData = this.pageData.get(interaction.id);
|
||||||
|
|
||||||
|
if (!tempData) {
|
||||||
|
this.logger.warn(
|
||||||
|
`Failed to update from interval, because temp data was not found`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await interaction.editReply(this.getReplyForPage(tempData.page));
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Interval(2 * 1000)
|
||||||
|
private async updatePlaylists() {
|
||||||
|
if (this.pageData.size === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.verbose(
|
||||||
|
`Updating playlist for ${this.pageData.size} playlist datas`,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.pageData.forEach(async (value) => {
|
||||||
|
await value.interaction.editReply(this.getReplyForPage(value.page));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public getReplyForPage(
|
public getReplyForPage(
|
||||||
page: number,
|
page: number,
|
||||||
): InteractionReplyOptions | InteractionUpdateOptions {
|
): InteractionReplyOptions | InteractionUpdateOptions {
|
||||||
@ -176,26 +212,34 @@ export class PlaylistCommand {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const paddingNumber = playlist.getLength() >= 100 ? 3 : 2;
|
||||||
|
|
||||||
const content = chunk
|
const content = chunk
|
||||||
.map((track, index) => {
|
.map((track, index) => {
|
||||||
const isCurrent = track === playlist.getActiveTrack();
|
const isCurrent = track === playlist.getActiveTrack();
|
||||||
|
|
||||||
// use the offset for the page, add the current index and offset by one because the array index is used
|
let line = `\`\`${zeroPad(offset + index + 1, paddingNumber)}.\`\` `;
|
||||||
let point = `${offset + index + 1}. `;
|
line += this.getTrackName(track, isCurrent) + ' • ';
|
||||||
point += `**${trimStringToFixedLength(track.name, 30)}**`;
|
|
||||||
|
|
||||||
if (isCurrent) {
|
if (isCurrent) {
|
||||||
point += ' :loud_sound:';
|
line += lightFormat(track.getPlaybackProgress(), 'mm:ss') + ' / ';
|
||||||
}
|
}
|
||||||
|
line += lightFormat(track.getDuration(), 'mm:ss');
|
||||||
point += '\n';
|
if (isCurrent) {
|
||||||
point += Constants.Design.InvisibleSpace.repeat(2);
|
line += ' • (:play_pause:)';
|
||||||
point += formatMillisecondsAsHumanReadable(track.getDuration());
|
}
|
||||||
|
return line;
|
||||||
return point;
|
|
||||||
})
|
})
|
||||||
.join('\n');
|
.join('\n');
|
||||||
|
|
||||||
return new EmbedBuilder().setTitle('Your playlist').setDescription(content);
|
return new EmbedBuilder().setTitle('Your playlist').setDescription(content);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getTrackName(track: Track, active: boolean) {
|
||||||
|
const trimmedTitle = trimStringToFixedLength(track.name, 30);
|
||||||
|
if (active) {
|
||||||
|
return `**${trimmedTitle}**`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return trimmedTitle;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,6 +15,7 @@ import {
|
|||||||
} from 'discord.js';
|
} from 'discord.js';
|
||||||
|
|
||||||
import { PlaylistCommand } from './playlist.command';
|
import { PlaylistCommand } from './playlist.command';
|
||||||
|
import { PlaylistTempCommandData } from './playlist.types';
|
||||||
|
|
||||||
@Injectable({ scope: Scope.REQUEST })
|
@Injectable({ scope: Scope.REQUEST })
|
||||||
@InteractionEventCollector({ time: 60 * 1000 })
|
@InteractionEventCollector({ time: 60 * 1000 })
|
||||||
@ -40,7 +41,7 @@ export class PlaylistInteractionCollector {
|
|||||||
async onCollect(interaction: ButtonInteraction): Promise<void> {
|
async onCollect(interaction: ButtonInteraction): Promise<void> {
|
||||||
const targetPage = this.getInteraction(interaction);
|
const targetPage = this.getInteraction(interaction);
|
||||||
this.logger.verbose(
|
this.logger.verbose(
|
||||||
`Extracted the target page ${targetPage} from the button interaction`,
|
`Extracted the target page '${targetPage?.page}' from the button interaction`,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (targetPage === undefined) {
|
if (targetPage === undefined) {
|
||||||
@ -51,14 +52,16 @@ export class PlaylistInteractionCollector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.logger.debug(
|
this.logger.debug(
|
||||||
`Updating current page for interaction ${this.causeInteraction.id} to ${targetPage}`,
|
`Updating current page for interaction ${this.causeInteraction.id} to ${targetPage.page}`,
|
||||||
);
|
);
|
||||||
this.playlistCommand.pageData.set(this.causeInteraction.id, targetPage);
|
this.playlistCommand.pageData.set(this.causeInteraction.id, targetPage);
|
||||||
const reply = this.playlistCommand.getReplyForPage(targetPage);
|
const reply = this.playlistCommand.getReplyForPage(targetPage.page);
|
||||||
await interaction.update(reply as InteractionUpdateOptions);
|
await interaction.update(reply as InteractionUpdateOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
private getInteraction(interaction: ButtonInteraction): number | undefined {
|
private getInteraction(
|
||||||
|
interaction: ButtonInteraction,
|
||||||
|
): PlaylistTempCommandData | undefined {
|
||||||
const current = this.playlistCommand.pageData.get(this.causeInteraction.id);
|
const current = this.playlistCommand.pageData.get(this.causeInteraction.id);
|
||||||
|
|
||||||
if (current === undefined) {
|
if (current === undefined) {
|
||||||
@ -78,12 +81,18 @@ export class PlaylistInteractionCollector {
|
|||||||
|
|
||||||
switch (interaction.customId) {
|
switch (interaction.customId) {
|
||||||
case 'playlist-controls-next':
|
case 'playlist-controls-next':
|
||||||
return current + 1;
|
return {
|
||||||
|
...current,
|
||||||
|
page: current.page + 1,
|
||||||
|
};
|
||||||
case 'playlist-controls-previous':
|
case 'playlist-controls-previous':
|
||||||
return current - 1;
|
return {
|
||||||
|
...current,
|
||||||
|
page: current.page - 1,
|
||||||
|
};
|
||||||
default:
|
default:
|
||||||
this.logger.error(
|
this.logger.error(
|
||||||
"Unable to map button interaction from collector to target page",
|
'Unable to map button interaction from collector to target page',
|
||||||
);
|
);
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
6
src/commands/playlist/playlist.types.ts
Normal file
6
src/commands/playlist/playlist.types.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { CommandInteraction } from 'discord.js';
|
||||||
|
|
||||||
|
export type PlaylistTempCommandData = {
|
||||||
|
page: number;
|
||||||
|
interaction: CommandInteraction;
|
||||||
|
};
|
@ -2,6 +2,7 @@ import {
|
|||||||
BaseItemDto,
|
BaseItemDto,
|
||||||
SearchHint as JellyfinSearchHint,
|
SearchHint as JellyfinSearchHint,
|
||||||
} from '@jellyfin/sdk/lib/generated-client/models';
|
} from '@jellyfin/sdk/lib/generated-client/models';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { JellyfinSearchService } from '../../clients/jellyfin/jellyfin.search.service';
|
import { JellyfinSearchService } from '../../clients/jellyfin/jellyfin.search.service';
|
||||||
import { Track } from '../shared/Track';
|
import { Track } from '../shared/Track';
|
||||||
@ -26,12 +27,27 @@ export class SearchHint {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static constructFromHint(hint: JellyfinSearchHint) {
|
static constructFromHint(hint: JellyfinSearchHint) {
|
||||||
if (hint.Id === undefined || !hint.Name || !hint.RunTimeTicks) {
|
const schema = z.object({
|
||||||
|
Id: z.string(),
|
||||||
|
Name: z.string(),
|
||||||
|
RunTimeTicks: z.number(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = schema.safeParse(hint);
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'Unable to construct search hint, required properties were undefined',
|
`Unable to construct search hint, required properties were undefined: ${JSON.stringify(
|
||||||
|
hint,
|
||||||
|
)}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return new SearchHint(hint.Id, hint.Name, hint.RunTimeTicks / 10000);
|
|
||||||
|
return new SearchHint(
|
||||||
|
result.data.Id,
|
||||||
|
result.data.Name,
|
||||||
|
result.data.RunTimeTicks / 10000,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static constructFromBaseItem(baseItem: BaseItemDto) {
|
static constructFromBaseItem(baseItem: BaseItemDto) {
|
||||||
|
@ -29,6 +29,8 @@ export class Track {
|
|||||||
|
|
||||||
playing: boolean;
|
playing: boolean;
|
||||||
|
|
||||||
|
playbackProgress: number;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
id: string,
|
id: string,
|
||||||
name: string,
|
name: string,
|
||||||
@ -40,6 +42,7 @@ export class Track {
|
|||||||
this.duration = duration;
|
this.duration = duration;
|
||||||
this.remoteImages = remoteImages;
|
this.remoteImages = remoteImages;
|
||||||
this.playing = false;
|
this.playing = false;
|
||||||
|
this.playbackProgress = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
getDuration() {
|
getDuration() {
|
||||||
@ -53,4 +56,12 @@ export class Track {
|
|||||||
getRemoteImages(): RemoteImageInfo[] {
|
getRemoteImages(): RemoteImageInfo[] {
|
||||||
return this.remoteImages?.Images ?? [];
|
return this.remoteImages?.Images ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getPlaybackProgress() {
|
||||||
|
return this.playbackProgress;
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePlaybackProgress(progress: number) {
|
||||||
|
this.playbackProgress = progress;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,3 +11,6 @@ export const trimStringToFixedLength = (value: string, maxLength: number) => {
|
|||||||
|
|
||||||
return value.substring(0, upperBound) + '...';
|
return value.substring(0, upperBound) + '...';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const zeroPad = (num: number, places: number) =>
|
||||||
|
String(num).padStart(places, '0');
|
||||||
|
@ -5403,3 +5403,8 @@ yocto-queue@^0.1.0:
|
|||||||
version "0.1.0"
|
version "0.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
|
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
|
||||||
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
|
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
|
||||||
|
|
||||||
|
zod@^3.21.4:
|
||||||
|
version "3.21.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/zod/-/zod-3.21.4.tgz#10882231d992519f0a10b5dd58a38c9dabbb64db"
|
||||||
|
integrity sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==
|
||||||
|
Loading…
Reference in New Issue
Block a user