mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-07 20:40:15 +02:00
improve domain types to better match OS, update normalizer functions
This commit is contained in:
@@ -1,42 +1,179 @@
|
||||
import { nanoid } from 'nanoid';
|
||||
import { z } from 'zod';
|
||||
import { components } from './subsonic-schema.d';
|
||||
|
||||
import { ssType } from '/@/shared/api/subsonic/subsonic-types';
|
||||
import { Album } from '/@/shared/types/domain/album-domain-types';
|
||||
import { AlbumArtist, RelatedArtist } from '/@/shared/types/domain/artist-domain-types';
|
||||
import { Genre } from '/@/shared/types/domain/genre-domain-types';
|
||||
import { QueueSong } from '/@/shared/types/domain/player-domain-types';
|
||||
import { Artist, RelatedArtist } from '/@/shared/types/domain/artist-domain-types';
|
||||
import { Genre, RelatedGenre } from '/@/shared/types/domain/genre-domain-types';
|
||||
import { Playlist } from '/@/shared/types/domain/playlist-domain-types';
|
||||
import { ServerListItem, ServerType } from '/@/shared/types/domain/server-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: {
|
||||
baseUrl: string | undefined;
|
||||
coverArtId?: string;
|
||||
credential: string | undefined;
|
||||
size: number;
|
||||
}) => {
|
||||
const size = args.size ? args.size : 250;
|
||||
export const normalize = {
|
||||
album: (
|
||||
item: components['schemas']['AlbumID3'] | components['schemas']['AlbumID3WithSongs'],
|
||||
server: ServerListItem,
|
||||
): Album => {
|
||||
const imageUrl = item.coverArt ? getCoverArtUrl(item.coverArt, server) : null;
|
||||
|
||||
if (!args.coverArtId || args.coverArtId.match('2a96cbd8b46e442fc41c2b86b821562f')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
`${args.baseUrl}/rest/getCoverArt.view` +
|
||||
`?id=${args.coverArtId}` +
|
||||
`&${args.credential}` +
|
||||
'&v=1.13.0' +
|
||||
'&c=Feishin' +
|
||||
`&size=${size}`
|
||||
);
|
||||
return {
|
||||
_itemType: LibraryItem.ALBUM,
|
||||
_serverId: server?.id || 'unknown',
|
||||
_serverType: ServerType.SUBSONIC,
|
||||
artistName: item.artist || null,
|
||||
artists: getArtistList(item.artists, item.artistId, item.artist),
|
||||
comment: null,
|
||||
createdDate: item.created,
|
||||
discTitles: getDiscTitles(item),
|
||||
displayArtist: null,
|
||||
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,
|
||||
artistId?: number | string,
|
||||
artistName?: string,
|
||||
) => {
|
||||
) {
|
||||
return artists
|
||||
? artists.map((item) => ({
|
||||
id: item.id.toString(),
|
||||
@@ -50,19 +187,94 @@ const getArtistList = (
|
||||
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:
|
||||
| z.infer<typeof ssType._response.album>
|
||||
| z.infer<typeof ssType._response.albumListEntry>
|
||||
| z.infer<typeof ssType._response.song>,
|
||||
) => {
|
||||
let participants: null | Record<string, RelatedArtist[]> = null;
|
||||
| components['schemas']['AlbumID3']
|
||||
| components['schemas']['AlbumID3WithSongs']
|
||||
| components['schemas']['Child'],
|
||||
): RelatedGenre[] {
|
||||
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) {
|
||||
participants = {};
|
||||
|
||||
for (const contributor of item.contributors) {
|
||||
const artist = {
|
||||
id: contributor.artist.id?.toString() || '',
|
||||
@@ -74,248 +286,54 @@ const getParticipants = (
|
||||
? `${contributor.role} (${contributor.subRole})`
|
||||
: contributor.role;
|
||||
|
||||
if (role in participants) {
|
||||
participants[role].push(artist);
|
||||
} else {
|
||||
participants[role] = [artist];
|
||||
if (!participants[role]) {
|
||||
participants[role] = [];
|
||||
}
|
||||
|
||||
participants[role].push(artist);
|
||||
}
|
||||
}
|
||||
|
||||
return participants;
|
||||
};
|
||||
}
|
||||
|
||||
const getGenres = (
|
||||
item:
|
||||
| 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,
|
||||
},
|
||||
]
|
||||
: [];
|
||||
};
|
||||
function getPeakInfo(item: components['schemas']['Child']) {
|
||||
return item.replayGain && (item.replayGain.albumPeak || item.replayGain.trackPeak)
|
||||
? {
|
||||
album: item.replayGain.albumPeak,
|
||||
track: item.replayGain.trackPeak,
|
||||
}
|
||||
: null;
|
||||
}
|
||||
|
||||
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;
|
||||
function getRecordLabels(
|
||||
item: components['schemas']['AlbumID3'] | components['schemas']['AlbumID3WithSongs'],
|
||||
) {
|
||||
return (item.recordLabels || []).map((recordLabel) => ({
|
||||
id: recordLabel.name,
|
||||
name: recordLabel.name,
|
||||
}));
|
||||
}
|
||||
|
||||
const streamUrl = `${server?.url}/rest/stream.view?id=${item.id}&v=1.13.0&c=Feishin&${server?.credential}`;
|
||||
function getReleaseDate(
|
||||
item: components['schemas']['AlbumID3'] | components['schemas']['AlbumID3WithSongs'],
|
||||
) {
|
||||
return item.releaseDate
|
||||
? formatDate.toUTCDate(
|
||||
`${item.releaseDate.year}-${item.releaseDate.month}-${item.releaseDate.day}`,
|
||||
)
|
||||
: null;
|
||||
}
|
||||
|
||||
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,
|
||||
track: item.replayGain.trackPeak,
|
||||
}
|
||||
: 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,
|
||||
};
|
||||
};
|
||||
function getReleaseTypes(
|
||||
item: components['schemas']['AlbumID3'] | components['schemas']['AlbumID3WithSongs'],
|
||||
) {
|
||||
return (item.releaseTypes || []).map((releaseType) => ({
|
||||
id: releaseType,
|
||||
name: releaseType,
|
||||
}));
|
||||
}
|
||||
|
||||
const normalizeAlbumArtist = (
|
||||
item:
|
||||
| z.infer<typeof ssType._response.albumArtist>
|
||||
| z.infer<typeof ssType._response.artistListEntry>,
|
||||
server: null | ServerListItem,
|
||||
imageSize?: number,
|
||||
): AlbumArtist => {
|
||||
const imageUrl =
|
||||
getCoverArtUrl({
|
||||
baseUrl: server?.url,
|
||||
coverArtId: item.coverArt?.toString(),
|
||||
credential: server?.credential,
|
||||
size: imageSize || 100,
|
||||
}) || null;
|
||||
|
||||
return {
|
||||
albumCount: item.albumCount ? Number(item.albumCount) : 0,
|
||||
backgroundImageUrl: null,
|
||||
biography: null,
|
||||
duration: null,
|
||||
genres: [],
|
||||
id: item.id.toString(),
|
||||
imageUrl,
|
||||
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 = (
|
||||
item: z.infer<typeof ssType._response.album> | z.infer<typeof ssType._response.albumListEntry>,
|
||||
server: null | ServerListItem,
|
||||
imageSize?: number,
|
||||
): Album => {
|
||||
const imageUrl =
|
||||
getCoverArtUrl({
|
||||
baseUrl: server?.url,
|
||||
coverArtId: item.coverArt?.toString(),
|
||||
credential: server?.credential,
|
||||
size: imageSize || 300,
|
||||
}) || null;
|
||||
|
||||
return {
|
||||
albumArtist: item.artist,
|
||||
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,
|
||||
};
|
||||
function getStreamUrl(id: string, server: ServerListItem) {
|
||||
return `${server.url}/rest/stream.view?id=${id}&v=1.16.1&c=Feishin&${server.credential}`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user