improve domain types to better match OS, update normalizer functions

This commit is contained in:
jeffvli
2025-07-12 21:52:12 -07:00
parent 67eec51e5f
commit fe36535aee
9 changed files with 417 additions and 368 deletions
+284 -266
View File
@@ -1,42 +1,179 @@
import { nanoid } from 'nanoid'; import { components } from './subsonic-schema.d';
import { z } from 'zod';
import { ssType } from '/@/shared/api/subsonic/subsonic-types'; import { ssType } from '/@/shared/api/subsonic/subsonic-types';
import { Album } from '/@/shared/types/domain/album-domain-types'; import { Album } from '/@/shared/types/domain/album-domain-types';
import { AlbumArtist, RelatedArtist } from '/@/shared/types/domain/artist-domain-types'; import { Artist, RelatedArtist } from '/@/shared/types/domain/artist-domain-types';
import { Genre } from '/@/shared/types/domain/genre-domain-types'; import { Genre, RelatedGenre } from '/@/shared/types/domain/genre-domain-types';
import { QueueSong } from '/@/shared/types/domain/player-domain-types';
import { Playlist } from '/@/shared/types/domain/playlist-domain-types'; import { Playlist } from '/@/shared/types/domain/playlist-domain-types';
import { ServerListItem, ServerType } from '/@/shared/types/domain/server-domain-types'; import { ServerListItem, ServerType } from '/@/shared/types/domain/server-domain-types';
import { LibraryItem } from '/@/shared/types/domain/shared-domain-types'; import { LibraryItem } from '/@/shared/types/domain/shared-domain-types';
import { Song } from '/@/shared/types/domain/song-domain-types';
import { formatDate } from '/@/shared/utils/format-date';
const getCoverArtUrl = (args: { export const normalize = {
baseUrl: string | undefined; album: (
coverArtId?: string; item: components['schemas']['AlbumID3'] | components['schemas']['AlbumID3WithSongs'],
credential: string | undefined; server: ServerListItem,
size: number; ): Album => {
}) => { const imageUrl = item.coverArt ? getCoverArtUrl(item.coverArt, server) : null;
const size = args.size ? args.size : 250;
if (!args.coverArtId || args.coverArtId.match('2a96cbd8b46e442fc41c2b86b821562f')) { return {
return null; _itemType: LibraryItem.ALBUM,
} _serverId: server?.id || 'unknown',
_serverType: ServerType.SUBSONIC,
return ( artistName: item.artist || null,
`${args.baseUrl}/rest/getCoverArt.view` + artists: getArtistList(item.artists, item.artistId, item.artist),
`?id=${args.coverArtId}` + comment: null,
`&${args.credential}` + createdDate: item.created,
'&v=1.13.0' + discTitles: getDiscTitles(item),
'&c=Feishin' + displayArtist: null,
`&size=${size}` duration: getDuration(item.duration),
); explicit: item.explicitStatus === 'explicit',
genres: getGenres(item),
id: item.id.toString(),
imagePlaceholderUrl: null,
imageUrl,
isCompilation: item.isCompilation || null,
mbzId: item.musicBrainzId || null,
mbzReleaseGroupId: null,
missing: null,
moods: getMoods(item),
name: item.name,
originalReleaseDate: getOriginalReleaseDate(item),
participants: {},
recordLabels: getRecordLabels(item),
releaseDate: getReleaseDate(item),
releaseTypes: getReleaseTypes(item),
releaseYear: item.year || null,
size: null,
songCount: item.songCount,
sortName: item.sortName || item.name,
tags: {},
updatedDate: null,
userFavorite: Boolean(item.starred),
userFavoriteDate: item.starred || null,
userLastPlayedDate: item.played || null,
userPlayCount: item.playCount ?? null,
userRating: item.userRating || null,
version: item.version || null,
};
},
albumArtist: (
item: components['schemas']['ArtistID3'] & components['schemas']['ArtistInfo2'],
server: ServerListItem,
): Artist => {
return {
_serverId: server?.id || 'unknown',
_serverType: ServerType.SUBSONIC,
albumCount: item.albumCount ? Number(item.albumCount) : 0,
biography: item.biography || null,
duration: null,
genres: [],
id: item.id.toString(),
imageUrl: item.coverArt ? getCoverArtUrl(item.coverArt.toString(), server) : null,
itemType: LibraryItem.ALBUM_ARTIST,
mbzId: item.musicBrainzId || null,
name: item.name,
playCount: null,
similarArtists: [],
songCount: null,
userFavorite: false,
userLastPlayedDate: null,
userRating: null,
};
},
genre: (item: components['schemas']['Genre'], server: ServerListItem): Genre => {
return {
_itemType: LibraryItem.GENRE,
_serverId: server.id,
_serverType: ServerType.SUBSONIC,
albumCount: item.albumCount,
id: item.value,
imageUrl: null,
name: item.value,
songCount: item.songCount,
};
},
playlist: (item: components['schemas']['Playlist'], server: ServerListItem): Playlist => {
return {
_itemType: LibraryItem.PLAYLIST,
_serverId: server.id,
_serverType: ServerType.SUBSONIC,
createdDate: item.created || null,
description: item.comment || null,
duration: getDuration(item.duration),
genres: [],
id: item.id.toString(),
imageUrl: item.coverArt ? getCoverArtUrl(item.coverArt.toString(), server) : null,
name: item.name,
owner: item.owner || null,
ownerId: item.owner || null,
public: item.public || null,
size: null,
songCount: item.songCount,
updatedDate: item.changed,
};
},
song: (item: components['schemas']['Child'], server: ServerListItem): Song => {
return {
_itemType: LibraryItem.SONG,
_serverId: server.id,
_serverType: ServerType.SUBSONIC,
album: item.album || null,
albumArtistName: item.displayAlbumArtist || null,
albumArtists: getArtistList(item.albumArtists, item.artistId, item.artist),
albumId: item.albumId || null,
artistName: item.displayArtist || item.artist || null,
artists: getArtistList(item.artists, item.artistId, item.artist),
bitDepth: item.bitDepth || null,
bitRate: item.bitRate || null,
bpm: item.bpm || null,
channels: item.channelCount || null,
comment: item.comment || null,
composer: item.displayComposer || null,
container: item.contentType || null,
createdDate: item.created || null,
discNumber: item.discNumber || 1,
discSubtitle: null,
duration: getDuration(item.duration),
explicit: item.explicitStatus === 'explicit',
gain: getGainInfo(item),
genres: getGenres(item),
id: item.id.toString(),
imageUrl: item.coverArt ? getCoverArtUrl(item.coverArt, server) : null,
isCompilation: null,
isrc: item.isrc || [],
lyrics: null,
mbzId: item.musicBrainzId || null,
missing: false,
moods: getMoods(item),
name: item.title,
participants: getParticipants(item),
path: item.path || null,
peak: getPeakInfo(item),
playCount: item?.playCount || 0,
releaseDate: null,
releaseYear: item.year ?? null,
samplingRate: item.samplingRate || null,
size: item.size ?? 0,
sortName: item.sortName || item.title,
streamUrl: getStreamUrl(item.id, server),
tags: {},
trackNumber: item.track || 1,
updatedDate: null,
userFavorite: Boolean(item.starred),
userFavoriteDate: item.starred || null,
userLastPlayedDate: item.played || null,
userRating: item.userRating || null,
};
},
}; };
const getArtistList = ( function getArtistList(
artists?: typeof ssType._response.song._type.artists, artists?: typeof ssType._response.song._type.artists,
artistId?: number | string, artistId?: number | string,
artistName?: string, artistName?: string,
) => { ) {
return artists return artists
? artists.map((item) => ({ ? artists.map((item) => ({
id: item.id.toString(), id: item.id.toString(),
@@ -50,19 +187,94 @@ const getArtistList = (
name: artistName || '', name: artistName || '',
}, },
]; ];
}; }
const getParticipants = ( function getCoverArtUrl(id: string, server: ServerListItem) {
return (
`${server.url}/rest/getCoverArt.view` +
`?id=${id}` +
`&${server.credential}` +
'&v=1.16.1' +
'&c=Feishin'
);
}
function getDiscTitles(
item: components['schemas']['AlbumID3'] | components['schemas']['AlbumID3WithSongs'],
) {
return (item.discTitles || []).map((discTitle) => ({
disc: discTitle.disc,
title: discTitle.title,
}));
}
function getDuration(duration?: number) {
// Transform from seconds to milliseconds
return duration ? duration * 1000 : 0;
}
function getGainInfo(item: components['schemas']['Child']) {
return item.replayGain && (item.replayGain.albumGain || item.replayGain.trackGain)
? {
album: item.replayGain.albumGain,
track: item.replayGain.trackGain,
}
: null;
}
function getGenres(
item: item:
| z.infer<typeof ssType._response.album> | components['schemas']['AlbumID3']
| z.infer<typeof ssType._response.albumListEntry> | components['schemas']['AlbumID3WithSongs']
| z.infer<typeof ssType._response.song>, | components['schemas']['Child'],
) => { ): RelatedGenre[] {
let participants: null | Record<string, RelatedArtist[]> = null; if (item.genres) {
return item.genres.map((genre) => ({
id: genre.name,
imageUrl: null,
name: genre.name,
}));
}
if (item.genre) {
return [
{
id: item.genre,
imageUrl: null,
name: item.genre,
},
];
}
return [];
}
function getMoods(
item:
| components['schemas']['AlbumID3']
| components['schemas']['AlbumID3WithSongs']
| components['schemas']['Child'],
) {
return (item.moods || []).map((mood) => ({
id: mood,
name: mood,
}));
}
function getOriginalReleaseDate(
item: components['schemas']['AlbumID3'] | components['schemas']['AlbumID3WithSongs'],
) {
return item.originalReleaseDate
? formatDate.toUTCDate(
`${item.originalReleaseDate.year}-${item.originalReleaseDate.month}-${item.originalReleaseDate.day}`,
)
: null;
}
function getParticipants(item: components['schemas']['Child']) {
const participants: Record<string, RelatedArtist[]> = {};
if (item.contributors) { if (item.contributors) {
participants = {};
for (const contributor of item.contributors) { for (const contributor of item.contributors) {
const artist = { const artist = {
id: contributor.artist.id?.toString() || '', id: contributor.artist.id?.toString() || '',
@@ -74,248 +286,54 @@ const getParticipants = (
? `${contributor.role} (${contributor.subRole})` ? `${contributor.role} (${contributor.subRole})`
: contributor.role; : contributor.role;
if (role in participants) { if (!participants[role]) {
participants[role].push(artist); participants[role] = [];
} else {
participants[role] = [artist];
} }
participants[role].push(artist);
} }
} }
return participants; return participants;
}; }
const getGenres = ( function getPeakInfo(item: components['schemas']['Child']) {
item: return item.replayGain && (item.replayGain.albumPeak || item.replayGain.trackPeak)
| z.infer<typeof ssType._response.album>
| z.infer<typeof ssType._response.albumListEntry>
| z.infer<typeof ssType._response.song>,
): Genre[] => {
return item.genres
? item.genres.map((genre) => ({
id: genre.name,
imageUrl: null,
itemType: LibraryItem.GENRE,
name: genre.name,
}))
: item.genre
? [
{
id: item.genre,
imageUrl: null,
itemType: LibraryItem.GENRE,
name: item.genre,
},
]
: [];
};
const normalizeSong = (
item: z.infer<typeof ssType._response.song>,
server: null | ServerListItem,
size?: number,
): QueueSong => {
const imageUrl =
getCoverArtUrl({
baseUrl: server?.url,
coverArtId: item.coverArt?.toString(),
credential: server?.credential,
size: size || 300,
}) || null;
const streamUrl = `${server?.url}/rest/stream.view?id=${item.id}&v=1.13.0&c=Feishin&${server?.credential}`;
return {
album: item.album || '',
albumArtists: getArtistList(item.albumArtists, item.artistId, item.artist),
albumId: item.albumId?.toString() || '',
artistName: item.artist || '',
artists: getArtistList(item.artists, item.artistId, item.artist),
bitDepth: item.bitDepth || null,
bitRate: item.bitRate || 0,
bpm: item.bpm || null,
channels: item.channelCount || null,
comment: null,
compilation: null,
container: item.contentType,
createdAt: item.created,
discNumber: item.discNumber || 1,
discSubtitle: null,
duration: item.duration ? item.duration * 1000 : 0,
gain:
item.replayGain && (item.replayGain.albumGain || item.replayGain.trackGain)
? {
album: item.replayGain.albumGain,
track: item.replayGain.trackGain,
}
: null,
genres: getGenres(item),
id: item.id.toString(),
imagePlaceholderUrl: null,
imageUrl,
itemType: LibraryItem.SONG,
lastPlayedAt: null,
lyrics: null,
name: item.title,
participants: getParticipants(item),
path: item.path,
peak:
item.replayGain && (item.replayGain.albumPeak || item.replayGain.trackPeak)
? { ? {
album: item.replayGain.albumPeak, album: item.replayGain.albumPeak,
track: item.replayGain.trackPeak, track: item.replayGain.trackPeak,
} }
: null, : null;
playCount: item?.playCount || 0, }
releaseDate: null,
releaseYear: item.year ? String(item.year) : null,
sampleRate: item.samplingRate || null,
serverId: server?.id || 'unknown',
serverType: ServerType.SUBSONIC,
size: item.size,
streamUrl,
tags: null,
trackNumber: item.track || 1,
uniqueId: nanoid(),
updatedAt: '',
userFavorite: item.starred || false,
userRating: item.userRating || null,
};
};
const normalizeAlbumArtist = ( function getRecordLabels(
item: item: components['schemas']['AlbumID3'] | components['schemas']['AlbumID3WithSongs'],
| z.infer<typeof ssType._response.albumArtist> ) {
| z.infer<typeof ssType._response.artistListEntry>, return (item.recordLabels || []).map((recordLabel) => ({
server: null | ServerListItem, id: recordLabel.name,
imageSize?: number, name: recordLabel.name,
): AlbumArtist => { }));
const imageUrl = }
getCoverArtUrl({
baseUrl: server?.url,
coverArtId: item.coverArt?.toString(),
credential: server?.credential,
size: imageSize || 100,
}) || null;
return { function getReleaseDate(
albumCount: item.albumCount ? Number(item.albumCount) : 0, item: components['schemas']['AlbumID3'] | components['schemas']['AlbumID3WithSongs'],
backgroundImageUrl: null, ) {
biography: null, return item.releaseDate
duration: null, ? formatDate.toUTCDate(
genres: [], `${item.releaseDate.year}-${item.releaseDate.month}-${item.releaseDate.day}`,
id: item.id.toString(), )
imageUrl, : null;
itemType: LibraryItem.ALBUM_ARTIST, }
lastPlayedAt: null,
mbz: null,
name: item.name,
playCount: null,
serverId: server?.id || 'unknown',
serverType: ServerType.SUBSONIC,
similarArtists: [],
songCount: null,
userFavorite: false,
userRating: null,
};
};
const normalizeAlbum = ( function getReleaseTypes(
item: z.infer<typeof ssType._response.album> | z.infer<typeof ssType._response.albumListEntry>, item: components['schemas']['AlbumID3'] | components['schemas']['AlbumID3WithSongs'],
server: null | ServerListItem, ) {
imageSize?: number, return (item.releaseTypes || []).map((releaseType) => ({
): Album => { id: releaseType,
const imageUrl = name: releaseType,
getCoverArtUrl({ }));
baseUrl: server?.url, }
coverArtId: item.coverArt?.toString(),
credential: server?.credential,
size: imageSize || 300,
}) || null;
return { function getStreamUrl(id: string, server: ServerListItem) {
albumArtist: item.artist, return `${server.url}/rest/stream.view?id=${id}&v=1.16.1&c=Feishin&${server.credential}`;
albumArtists: getArtistList(item.artists, item.artistId, item.artist), }
artists: [],
backdropImageUrl: null,
comment: null,
createdAt: item.created,
duration: item.duration * 1000,
genres: getGenres(item),
id: item.id.toString(),
imagePlaceholderUrl: null,
imageUrl,
isCompilation: null,
itemType: LibraryItem.ALBUM,
lastPlayedAt: null,
mbzId: null,
name: item.name,
originalDate: null,
participants: getParticipants(item),
playCount: null,
releaseDate: item.year ? new Date(Date.UTC(item.year, 0, 1)).toISOString() : null,
releaseYear: item.year ? Number(item.year) : null,
serverId: server?.id || 'unknown',
serverType: ServerType.SUBSONIC,
size: null,
songCount: item.songCount,
songs:
(item as z.infer<typeof ssType._response.album>).song?.map((song) =>
normalizeSong(song, server),
) || [],
tags: null,
uniqueId: nanoid(),
updatedAt: item.created,
userFavorite: item.starred || false,
userRating: item.userRating || null,
};
};
const normalizePlaylist = (
item:
| z.infer<typeof ssType._response.playlist>
| z.infer<typeof ssType._response.playlistListEntry>,
server: null | ServerListItem,
): Playlist => {
return {
description: item.comment || null,
duration: item.duration * 1000,
genres: [],
id: item.id.toString(),
imagePlaceholderUrl: null,
imageUrl: getCoverArtUrl({
baseUrl: server?.url,
coverArtId: item.coverArt?.toString(),
credential: server?.credential,
size: 300,
}),
itemType: LibraryItem.PLAYLIST,
name: item.name,
owner: item.owner,
ownerId: item.owner,
public: item.public,
serverId: server?.id || 'unknown',
serverType: ServerType.SUBSONIC,
size: null,
songCount: item.songCount,
};
};
const normalizeGenre = (item: z.infer<typeof ssType._response.genre>): Genre => {
return {
albumCount: item.albumCount,
id: item.value,
imageUrl: null,
itemType: LibraryItem.GENRE,
name: item.value,
songCount: item.songCount,
};
};
export const ssNormalize = {
album: normalizeAlbum,
albumArtist: normalizeAlbumArtist,
genre: normalizeGenre,
playlist: normalizePlaylist,
song: normalizeSong,
};
+35 -27
View File
@@ -8,9 +8,17 @@ import { NDAlbumListSort } from '/@/shared/api/navidrome.types';
import { ndType } from '/@/shared/api/navidrome/navidrome-types'; import { ndType } from '/@/shared/api/navidrome/navidrome-types';
import { BasePaginatedResponse, BaseQuery } from '/@/shared/types/adapter/api-controller-types'; import { BasePaginatedResponse, BaseQuery } from '/@/shared/types/adapter/api-controller-types';
import { RelatedArtist } from '/@/shared/types/domain/artist-domain-types'; import { RelatedArtist } from '/@/shared/types/domain/artist-domain-types';
import { Genre } from '/@/shared/types/domain/genre-domain-types'; import { RelatedGenre } from '/@/shared/types/domain/genre-domain-types';
import { ServerType } from '/@/shared/types/domain/server-domain-types'; import { ServerType } from '/@/shared/types/domain/server-domain-types';
import { LibraryItem, ListSortOrder } from '/@/shared/types/domain/shared-domain-types'; import {
LibraryItem,
ListSortOrder,
Mood,
Participants,
RecordLabel,
ReleaseType,
Tags,
} from '/@/shared/types/domain/shared-domain-types';
import { Song } from '/@/shared/types/domain/song-domain-types'; import { Song } from '/@/shared/types/domain/song-domain-types';
export enum AlbumListSortOptions { export enum AlbumListSortOptions {
@@ -150,52 +158,45 @@ export const albumListSortMap: AlbumListSortMap = {
}; };
export type Album = { export type Album = {
albumArtist: string; _itemType: LibraryItem.ALBUM;
albumArtists: RelatedArtist[]; _serverId: string;
_serverType: ServerType;
artistName: null | string;
artists: RelatedArtist[]; artists: RelatedArtist[];
backdropImageUrl: null | string;
comment: null | string; comment: null | string;
createdDate: string; createdDate: null | string;
description: null | string; discTitles: DiscTitle[];
discTitles: {
disc: number;
title: string;
}[];
displayArtist: null | string; displayArtist: null | string;
duration: null | number; duration: null | number;
genres: Genre[]; explicit: boolean | null;
genres: RelatedGenre[];
id: string; id: string;
imagePlaceholderUrl: null | string; imagePlaceholderUrl: null | string;
imageUrl: null | string; imageUrl: null | string;
isCompilation: boolean | null; isCompilation: boolean | null;
itemType: LibraryItem.ALBUM; mbzId: null | string;
mbzAlbumId: null | string;
mbzReleaseGroupId: null | string; mbzReleaseGroupId: null | string;
missing: boolean; missing: boolean | null;
moods: Mood[];
name: string; name: string;
originalReleaseDate: null | string; originalReleaseDate: null | string;
participants: null | Record<string, RelatedArtist[]>; participants: Participants;
recordLabels: RecordLabel[];
releaseDate: null | string; releaseDate: null | string;
releaseTypes: { releaseTypes: ReleaseType[];
id: string;
name: string;
}[];
releaseYear: null | number; releaseYear: null | number;
serverId: string;
serverType: ServerType;
size: null | number; size: null | number;
songCount: null | number; songCount: null | number;
songs?: Song[];
sortName: string; sortName: string;
tags: Record<string, string[]>; tags: Tags;
uniqueId: string; updatedDate: null | string;
updatedDate: string;
userFavorite: boolean; userFavorite: boolean;
userFavoriteDate: null | string; userFavoriteDate: null | string;
userLastPlayedDate: null | string; userLastPlayedDate: null | string;
userPlayCount: null | number; userPlayCount: null | number;
userRating: null | number; userRating: null | number;
} & { songs?: Song[] }; version: null | string;
};
export type AlbumDetailQuery = { id: string }; export type AlbumDetailQuery = { id: string };
@@ -208,6 +209,13 @@ export type AlbumInfo = {
notes: null | string; notes: null | string;
}; };
export type AlbumWithSongs = Album & { songs: Song[] };
export type DiscTitle = {
disc: number;
title: string;
};
export const sortAlbumList = (albums: Album[], sortBy: AlbumListSort, sortOrder: ListSortOrder) => { export const sortAlbumList = (albums: Album[], sortBy: AlbumListSort, sortOrder: ListSortOrder) => {
let results = albums; let results = albums;
+11 -30
View File
@@ -7,7 +7,7 @@ import { jfType } from '/@/shared/api/jellyfin/jellyfin-types';
import { NDAlbumArtistListSort } from '/@/shared/api/navidrome.types'; import { NDAlbumArtistListSort } from '/@/shared/api/navidrome.types';
import { ndType } from '/@/shared/api/navidrome/navidrome-types'; import { ndType } from '/@/shared/api/navidrome/navidrome-types';
import { BasePaginatedResponse, BaseQuery } from '/@/shared/types/adapter/api-controller-types'; import { BasePaginatedResponse, BaseQuery } from '/@/shared/types/adapter/api-controller-types';
import { Genre } from '/@/shared/types/domain/genre-domain-types'; import { RelatedGenre } from '/@/shared/types/domain/genre-domain-types';
import { ServerType } from '/@/shared/types/domain/server-domain-types'; import { ServerType } from '/@/shared/types/domain/server-domain-types';
import { LibraryItem, ListSortOrder } from '/@/shared/types/domain/shared-domain-types'; import { LibraryItem, ListSortOrder } from '/@/shared/types/domain/shared-domain-types';
@@ -31,45 +31,26 @@ export const ArtistListSortOptionsLabels = {
[ArtistListSortOptions.TRACK_COUNT]: i18n.t('filter.trackCount'), [ArtistListSortOptions.TRACK_COUNT]: i18n.t('filter.trackCount'),
}; };
export type AlbumArtist = { export type Artist = {
_serverId: string;
_serverType: ServerType;
albumCount: null | number; albumCount: null | number;
backgroundImageUrl: null | string;
biography: null | string; biography: null | string;
duration: null | number; duration: null | number;
genres: Genre[]; genres: RelatedGenre[];
id: string; id: string;
imageUrl: null | string; imageUrl: null | string;
itemType: LibraryItem.ALBUM_ARTIST; itemType: LibraryItem.ALBUM_ARTIST;
lastPlayedAt: null | string; mbzId: null | string;
mbz: null | string;
name: string; name: string;
playCount: null | number; playCount: null | number;
serverId: string;
serverType: ServerType;
similarArtists: null | RelatedArtist[]; similarArtists: null | RelatedArtist[];
songCount: null | number; songCount: null | number;
userFavorite: boolean; userFavorite: boolean;
userLastPlayedDate: null | string;
userRating: null | number; userRating: null | number;
}; };
export type Artist = {
biography: null | string;
createdAt: string;
id: string;
itemType: LibraryItem.ARTIST;
name: string;
remoteCreatedAt: null | string;
serverFolderId: string;
serverId: string;
serverType: ServerType;
updatedAt: string;
};
export type RelatedAlbumArtist = {
id: string;
name: string;
};
export type RelatedArtist = { export type RelatedArtist = {
id: string; id: string;
imageUrl: null | string; imageUrl: null | string;
@@ -142,7 +123,7 @@ export type AlbumArtistDetailQuery = { id: string };
export type AlbumArtistDetailRequest = { query: AlbumArtistDetailQuery }; export type AlbumArtistDetailRequest = { query: AlbumArtistDetailQuery };
export type AlbumArtistDetailResponse = AlbumArtist | null; export type AlbumArtistDetailResponse = Artist | null;
export interface ArtistListQuery extends BaseQuery<ArtistListSort> { export interface ArtistListQuery extends BaseQuery<ArtistListSort> {
_custom?: { _custom?: {
@@ -158,7 +139,7 @@ export interface ArtistListQuery extends BaseQuery<ArtistListSort> {
export type ArtistListRequest = { query: ArtistListQuery }; export type ArtistListRequest = { query: ArtistListQuery };
export type ArtistListResponse = BasePaginatedResponse<AlbumArtist[]> | null | undefined; export type ArtistListResponse = BasePaginatedResponse<Artist[]> | null | undefined;
type ArtistListSortMap = { type ArtistListSortMap = {
jellyfin: Record<ArtistListSort, JFArtistListSort | undefined>; jellyfin: Record<ArtistListSort, JFArtistListSort | undefined>;
navidrome: Record<ArtistListSort, undefined>; navidrome: Record<ArtistListSort, undefined>;
@@ -233,7 +214,7 @@ export interface AlbumArtistListQuery extends BaseQuery<AlbumArtistListSort> {
export type AlbumArtistListRequest = { query: AlbumArtistListQuery }; export type AlbumArtistListRequest = { query: AlbumArtistListQuery };
export type AlbumArtistListResponse = BasePaginatedResponse<AlbumArtist[]> | null | undefined; export type AlbumArtistListResponse = BasePaginatedResponse<Artist[]> | null | undefined;
export type ArtistInfoQuery = { export type ArtistInfoQuery = {
artistId: string; artistId: string;
@@ -244,7 +225,7 @@ export type ArtistInfoQuery = {
export type ArtistInfoRequest = { query: ArtistInfoQuery }; export type ArtistInfoRequest = { query: ArtistInfoQuery };
export const sortAlbumArtistList = ( export const sortAlbumArtistList = (
artists: AlbumArtist[], artists: Artist[],
sortBy: AlbumArtistListSort | ArtistListSort, sortBy: AlbumArtistListSort | ArtistListSort,
sortOrder: ListSortOrder, sortOrder: ListSortOrder,
) => { ) => {
+12 -3
View File
@@ -2,6 +2,7 @@ import i18n from '/@/i18n/i18n';
import { JFGenreListSort } from '/@/shared/api/jellyfin.types'; import { JFGenreListSort } from '/@/shared/api/jellyfin.types';
import { NDGenreListSort } from '/@/shared/api/navidrome.types'; import { NDGenreListSort } from '/@/shared/api/navidrome.types';
import { BasePaginatedResponse, BaseQuery } from '/@/shared/types/adapter/api-controller-types'; import { BasePaginatedResponse, BaseQuery } from '/@/shared/types/adapter/api-controller-types';
import { ServerType } from '/@/shared/types/domain/server-domain-types';
import { LibraryItem } from '/@/shared/types/domain/shared-domain-types'; import { LibraryItem } from '/@/shared/types/domain/shared-domain-types';
import { UserListSort } from '/@/shared/types/domain/user-domain-types'; import { UserListSort } from '/@/shared/types/domain/user-domain-types';
@@ -18,12 +19,14 @@ export const GenreListSortOptionsLabels = {
}; };
export type Genre = { export type Genre = {
albumCount?: number; _itemType: LibraryItem.GENRE;
_serverId: string;
_serverType: ServerType;
albumCount: null | number;
id: string; id: string;
imageUrl: null | string; imageUrl: null | string;
itemType: LibraryItem.GENRE;
name: string; name: string;
songCount?: number; songCount: null | number;
}; };
export interface GenreListQuery extends BaseQuery<GenreListSort> { export interface GenreListQuery extends BaseQuery<GenreListSort> {
@@ -41,6 +44,12 @@ export type GenreListRequest = { query: GenreListQuery };
export type GenreListResponse = BasePaginatedResponse<Genre[]> | null | undefined; export type GenreListResponse = BasePaginatedResponse<Genre[]> | null | undefined;
export type RelatedGenre = {
id: string;
imageUrl: null | string;
name: string;
};
type GenreListSortMap = { type GenreListSortMap = {
jellyfin: Record<GenreListSort, JFGenreListSort | undefined>; jellyfin: Record<GenreListSort, JFGenreListSort | undefined>;
navidrome: Record<GenreListSort, NDGenreListSort | undefined>; navidrome: Record<GenreListSort, NDGenreListSort | undefined>;
@@ -10,7 +10,7 @@ import {
BasePaginatedResponse, BasePaginatedResponse,
BaseQuery, BaseQuery,
} from '/@/shared/types/adapter/api-controller-types'; } from '/@/shared/types/adapter/api-controller-types';
import { Genre } from '/@/shared/types/domain/genre-domain-types'; import { Genre, RelatedGenre } from '/@/shared/types/domain/genre-domain-types';
import { ServerType } from '/@/shared/types/domain/server-domain-types'; import { ServerType } from '/@/shared/types/domain/server-domain-types';
import { LibraryItem, ListSortOrder } from '/@/shared/types/domain/shared-domain-types'; import { LibraryItem, ListSortOrder } from '/@/shared/types/domain/shared-domain-types';
import { Song, SongListSort } from '/@/shared/types/domain/song-domain-types'; import { Song, SongListSort } from '/@/shared/types/domain/song-domain-types';
@@ -89,24 +89,26 @@ export type DeletePlaylistRequest = {
export type DeletePlaylistResponse = null | undefined; export type DeletePlaylistResponse = null | undefined;
export type Playlist = { export type Playlist = {
_itemType: LibraryItem.PLAYLIST;
_serverId: string;
_serverType: ServerType;
createdDate: null | string;
description: null | string; description: null | string;
duration: null | number; duration: null | number;
genres: Genre[]; genres: RelatedGenre[];
id: string; id: string;
imagePlaceholderUrl: null | string;
imageUrl: null | string; imageUrl: null | string;
itemType: LibraryItem.PLAYLIST;
name: string; name: string;
owner: null | string; owner: null | string;
ownerId: null | string; ownerId: null | string;
public: boolean | null; public: boolean | null;
rules?: null | Record<string, any>; rules?: null | Record<string, any>;
serverId: string;
serverType: ServerType;
size: null | number; size: null | number;
songCount: null | number; songCount: null | number;
sync?: boolean | null; sync?: boolean | null;
updatedDate: null | string;
}; };
export interface PlaylistListQuery extends BaseQuery<PlaylistListSort> { export interface PlaylistListQuery extends BaseQuery<PlaylistListSort> {
_custom?: { _custom?: {
jellyfin?: Partial<z.infer<typeof jfType._parameters.playlistList>>; jellyfin?: Partial<z.infer<typeof jfType._parameters.playlistList>>;
@@ -121,6 +123,10 @@ export type PlaylistListRequest = { query: PlaylistListQuery };
export type PlaylistListResponse = BasePaginatedResponse<Playlist[]> | null | undefined; export type PlaylistListResponse = BasePaginatedResponse<Playlist[]> | null | undefined;
export type PlaylistSong = Song & {
playlistItemId: string;
};
export type RemoveFromPlaylistQuery = { export type RemoveFromPlaylistQuery = {
id: string; id: string;
songId: string[]; songId: string[];
@@ -1,5 +1,5 @@
import { Album } from '/@/shared/types/domain/album-domain-types'; import { Album } from '/@/shared/types/domain/album-domain-types';
import { AlbumArtist } from '/@/shared/types/domain/artist-domain-types'; import { Artist } from '/@/shared/types/domain/artist-domain-types';
import { Song } from '/@/shared/types/domain/song-domain-types'; import { Song } from '/@/shared/types/domain/song-domain-types';
export type SearchAlbumArtistsQuery = { export type SearchAlbumArtistsQuery = {
@@ -30,7 +30,7 @@ export type SearchQuery = {
export type SearchRequest = { query: SearchQuery }; export type SearchRequest = { query: SearchQuery };
export type SearchResponse = { export type SearchResponse = {
albumArtists: AlbumArtist[]; albumArtists: Artist[];
albums: Album[]; albums: Album[];
songs: Song[]; songs: Song[];
}; };
+22 -9
View File
@@ -1,7 +1,7 @@
import { JFSortOrder } from '/@/shared/api/jellyfin.types'; import { JFSortOrder } from '/@/shared/api/jellyfin.types';
import { NDSortOrder } from '/@/shared/api/navidrome.types'; import { NDSortOrder } from '/@/shared/api/navidrome.types';
import { Album } from '/@/shared/types/domain/album-domain-types'; import { Album } from '/@/shared/types/domain/album-domain-types';
import { AlbumArtist, Artist } from '/@/shared/types/domain/artist-domain-types'; import { Artist, Artist, RelatedArtist } from '/@/shared/types/domain/artist-domain-types';
import { QueueSong } from '/@/shared/types/domain/player-domain-types'; import { QueueSong } from '/@/shared/types/domain/player-domain-types';
import { Playlist } from '/@/shared/types/domain/playlist-domain-types'; import { Playlist } from '/@/shared/types/domain/playlist-domain-types';
import { Song } from '/@/shared/types/domain/song-domain-types'; import { Song } from '/@/shared/types/domain/song-domain-types';
@@ -19,15 +19,9 @@ export enum ListSortOrder {
DESC = 'DESC', DESC = 'DESC',
} }
export type AnyLibraryItem = Album | AlbumArtist | Artist | Playlist | QueueSong | Song; export type AnyLibraryItem = Album | Artist | Artist | Playlist | QueueSong | Song;
export type AnyLibraryItems = export type AnyLibraryItems = Album[] | Artist[] | Artist[] | Playlist[] | QueueSong[] | Song[];
| Album[]
| AlbumArtist[]
| Artist[]
| Playlist[]
| QueueSong[]
| Song[];
type SortOrderMap = { type SortOrderMap = {
jellyfin: Record<ListSortOrder, JFSortOrder>; jellyfin: Record<ListSortOrder, JFSortOrder>;
navidrome: Record<ListSortOrder, NDSortOrder>; navidrome: Record<ListSortOrder, NDSortOrder>;
@@ -56,3 +50,22 @@ export type FontData = {
postscriptName: string; postscriptName: string;
style: string; style: string;
}; };
export type Mood = {
id: string;
name: string;
};
export type Participants = Record<string, RelatedArtist[]>;
export type RecordLabel = {
id: string;
name: string;
};
export type ReleaseType = {
id: string;
name: string;
};
export type Tags = Record<string, string[]>;
+33 -19
View File
@@ -8,10 +8,16 @@ import { NDSongListSort } from '/@/shared/api/navidrome.types';
import { ndType } from '/@/shared/api/navidrome/navidrome-types'; import { ndType } from '/@/shared/api/navidrome/navidrome-types';
import { BasePaginatedResponse, BaseQuery } from '/@/shared/types/adapter/api-controller-types'; import { BasePaginatedResponse, BaseQuery } from '/@/shared/types/adapter/api-controller-types';
import { RelatedArtist } from '/@/shared/types/domain/artist-domain-types'; import { RelatedArtist } from '/@/shared/types/domain/artist-domain-types';
import { Genre } from '/@/shared/types/domain/genre-domain-types'; import { RelatedGenre } from '/@/shared/types/domain/genre-domain-types';
import { Played, QueueSong } from '/@/shared/types/domain/player-domain-types'; import { Played, QueueSong } from '/@/shared/types/domain/player-domain-types';
import { ServerType } from '/@/shared/types/domain/server-domain-types'; import { ServerType } from '/@/shared/types/domain/server-domain-types';
import { LibraryItem, ListSortOrder } from '/@/shared/types/domain/shared-domain-types'; import {
LibraryItem,
ListSortOrder,
Mood,
Participants,
Tags,
} from '/@/shared/types/domain/shared-domain-types';
export enum SongListSortOptions { export enum SongListSortOptions {
ALBUM = 'album', ALBUM = 'album',
@@ -77,46 +83,54 @@ export enum SongListSort {
} }
export type Song = { export type Song = {
_itemType: LibraryItem.SONG;
_serverId: string;
_serverType: ServerType;
album: null | string; album: null | string;
albumArtistName: null | string;
albumArtists: RelatedArtist[]; albumArtists: RelatedArtist[];
albumId: string; albumId: null | string;
artistName: string; artistName: null | string;
artists: RelatedArtist[]; artists: RelatedArtist[];
bitRate: number; bitDepth: null | number;
bitRate: null | number;
bpm: null | number; bpm: null | number;
channels: null | number; channels: null | number;
comment: null | string; comment: null | string;
compilation: boolean | null; composer: null | string;
container: null | string; container: null | string;
createdAt: string; createdDate: null | string;
discNumber: number; discNumber: number;
discSubtitle: null | string; discSubtitle: null | string;
duration: number; duration: number;
explicit: boolean | null;
gain: GainInfo | null; gain: GainInfo | null;
genres: Genre[]; genres: RelatedGenre[];
id: string; id: string;
imagePlaceholderUrl: null | string;
imageUrl: null | string; imageUrl: null | string;
itemType: LibraryItem.SONG; isCompilation: boolean | null;
lastPlayedAt: null | string; isrc: string[];
lyrics: null | string; lyrics: null | string;
mbzId: null | string;
missing: boolean | null;
moods: Mood[];
name: string; name: string;
participants: null | Record<string, RelatedArtist[]>; participants: Participants;
path: null | string; path: null | string;
peak: GainInfo | null; peak: GainInfo | null;
playCount: number; playCount: number;
playlistItemId?: string;
releaseDate: null | string; releaseDate: null | string;
releaseYear: null | string; releaseYear: null | number;
serverId: string; samplingRate: null | number;
serverType: ServerType;
size: number; size: number;
sortName: string;
streamUrl: string; streamUrl: string;
tags: null | Record<string, string[]>; tags: Tags;
trackNumber: number; trackNumber: number;
uniqueId: string; updatedDate: null | string;
updatedAt: string;
userFavorite: boolean; userFavorite: boolean;
userFavoriteDate: null | string;
userLastPlayedDate: null | string;
userRating: null | number; userRating: null | number;
}; };
+2 -2
View File
@@ -3,7 +3,7 @@ import { ReactNode } from 'react';
import { Song } from 'src/main/features/core/lyrics/netease'; import { Song } from 'src/main/features/core/lyrics/netease';
import { Album } from '/@/shared/types/domain/album-domain-types'; import { Album } from '/@/shared/types/domain/album-domain-types';
import { AlbumArtist, Artist } from '/@/shared/types/domain/artist-domain-types'; import { Artist } from '/@/shared/types/domain/artist-domain-types';
import { QueueSong } from '/@/shared/types/domain/player-domain-types'; import { QueueSong } from '/@/shared/types/domain/player-domain-types';
import { Playlist } from '/@/shared/types/domain/playlist-domain-types'; import { Playlist } from '/@/shared/types/domain/playlist-domain-types';
import { ServerType } from '/@/shared/types/domain/server-domain-types'; import { ServerType } from '/@/shared/types/domain/server-domain-types';
@@ -159,7 +159,7 @@ export enum TableColumn {
export type GridCardData = { export type GridCardData = {
cardControls: any; cardControls: any;
cardRows: CardRow<Album | AlbumArtist | Artist | Playlist | Song>[]; cardRows: CardRow<Album | Artist | Artist | Playlist | Song>[];
columnCount: number; columnCount: number;
display: ListDisplayType; display: ListDisplayType;
handleFavorite: (options: { id: string[]; isFavorite: boolean; itemType: LibraryItem }) => void; handleFavorite: (options: { id: string[]; isFavorite: boolean; itemType: LibraryItem }) => void;