mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-06 20:10:12 +02:00
449 lines
14 KiB
TypeScript
449 lines
14 KiB
TypeScript
import { z } from 'zod';
|
|
|
|
import { ssType } from '/@/shared/api/subsonic/subsonic-types';
|
|
import { replacePathPrefix } from '/@/shared/api/utils';
|
|
import {
|
|
Album,
|
|
AlbumArtist,
|
|
ExplicitStatus,
|
|
Folder,
|
|
Genre,
|
|
InternetRadioStation,
|
|
LibraryItem,
|
|
Playlist,
|
|
RelatedArtist,
|
|
ServerListItemWithCredential,
|
|
ServerType,
|
|
Song,
|
|
} from '/@/shared/types/domain-types';
|
|
|
|
const getArtistList = (
|
|
artists?: typeof ssType._response.song._type.artists,
|
|
artistId?: number | string,
|
|
artistName?: string,
|
|
participants?: null | Record<string, RelatedArtist[]>,
|
|
) => {
|
|
if (!artists && !participants) {
|
|
return [
|
|
{
|
|
id: artistId?.toString() || '',
|
|
imageId: null,
|
|
imageUrl: null,
|
|
name: artistName || '',
|
|
userFavorite: false,
|
|
userRating: null,
|
|
},
|
|
];
|
|
}
|
|
|
|
const result: RelatedArtist[] = [];
|
|
|
|
artists?.forEach((item) => {
|
|
result.push({
|
|
id: item.id.toString(),
|
|
imageId: null,
|
|
imageUrl: null,
|
|
name: item.name,
|
|
userFavorite: false,
|
|
userRating: null,
|
|
});
|
|
});
|
|
|
|
if (participants?.['remixer']) {
|
|
const existingIds = new Set(result.map((artist) => artist.id));
|
|
for (const participant of participants['remixer']) {
|
|
if (!existingIds.has(participant.id)) {
|
|
result.push(participant);
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
};
|
|
|
|
const getParticipants = (
|
|
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;
|
|
|
|
if (item.contributors) {
|
|
participants = {};
|
|
|
|
for (const contributor of item.contributors) {
|
|
const artist = {
|
|
id: contributor.artist.id?.toString() || '',
|
|
imageId: null,
|
|
imageUrl: null,
|
|
name: contributor.artist.name || '',
|
|
userFavorite: false,
|
|
userRating: null,
|
|
};
|
|
|
|
const role = contributor.subRole
|
|
? `${contributor.role} (${contributor.subRole})`
|
|
: contributor.role;
|
|
|
|
if (role in participants) {
|
|
participants[role].push(artist);
|
|
} else {
|
|
participants[role] = [artist];
|
|
}
|
|
}
|
|
}
|
|
|
|
return participants;
|
|
};
|
|
|
|
const getGenres = (
|
|
item:
|
|
| z.infer<typeof ssType._response.album>
|
|
| z.infer<typeof ssType._response.albumListEntry>
|
|
| z.infer<typeof ssType._response.song>,
|
|
server?: null | ServerListItemWithCredential,
|
|
): Genre[] => {
|
|
return item.genres
|
|
? item.genres.map((genre) => ({
|
|
_itemType: LibraryItem.GENRE,
|
|
_serverId: server?.id || 'unknown',
|
|
_serverType: ServerType.SUBSONIC,
|
|
albumCount: null,
|
|
id: genre.name,
|
|
imageId: null,
|
|
imageUrl: null,
|
|
name: genre.name,
|
|
songCount: null,
|
|
}))
|
|
: item.genre
|
|
? [
|
|
{
|
|
_itemType: LibraryItem.GENRE,
|
|
_serverId: server?.id || 'unknown',
|
|
_serverType: ServerType.SUBSONIC,
|
|
albumCount: null,
|
|
id: item.genre,
|
|
imageId: null,
|
|
imageUrl: null,
|
|
name: item.genre,
|
|
songCount: null,
|
|
},
|
|
]
|
|
: [];
|
|
};
|
|
|
|
const normalizeSong = (
|
|
item: z.infer<typeof ssType._response.song>,
|
|
server?: null | ServerListItemWithCredential,
|
|
pathReplace?: string,
|
|
pathReplaceWith?: string,
|
|
playlistIndex?: number,
|
|
discTitleMap?: Map<number, string>,
|
|
): Song => {
|
|
const participants = getParticipants(item);
|
|
const albumArtistsList = getArtistList(item.albumArtists, item.artistId, item.artist);
|
|
const albumArtistName =
|
|
item.albumArtists?.length > 0
|
|
? item.albumArtists.map((a) => a.name).join(', ')
|
|
: item.artist || '';
|
|
|
|
return {
|
|
_itemType: LibraryItem.SONG,
|
|
_serverId: server?.id || 'unknown',
|
|
_serverType: ServerType.SUBSONIC,
|
|
album: item.album || '',
|
|
albumArtistName,
|
|
albumArtists: albumArtistsList,
|
|
albumId: item.albumId?.toString() || '',
|
|
artistName: item.artist || '',
|
|
artists: getArtistList(item.artists, item.artistId, item.artist, participants),
|
|
bitDepth: item.bitDepth || null,
|
|
bitRate: item.bitRate || 0,
|
|
bpm: item.bpm || null,
|
|
channels: item.channelCount || null,
|
|
comment: null,
|
|
compilation: null,
|
|
container: item.contentType.startsWith('audio/') ? item.contentType.split('/')[1] : null,
|
|
createdAt: item.created,
|
|
discNumber: item.discNumber || 1,
|
|
discSubtitle: discTitleMap?.get(item.discNumber ?? 1) ?? null,
|
|
duration: item.duration ? item.duration * 1000 : 0,
|
|
explicitStatus:
|
|
item.explicitStatus === 'explicit'
|
|
? ExplicitStatus.EXPLICIT
|
|
: item.explicitStatus === 'clean'
|
|
? ExplicitStatus.CLEAN
|
|
: null,
|
|
gain:
|
|
item.replayGain && (item.replayGain.albumGain || item.replayGain.trackGain)
|
|
? {
|
|
album: item.replayGain.albumGain,
|
|
track: item.replayGain.trackGain,
|
|
}
|
|
: null,
|
|
genres: getGenres(item, server),
|
|
id: item.id.toString(),
|
|
imageId: item.coverArt?.toString() || null,
|
|
imageUrl: null,
|
|
lastPlayedAt: null,
|
|
lyrics: null,
|
|
mbzRecordingId: item.musicBrainzId || null,
|
|
mbzTrackId: null,
|
|
name: item.title,
|
|
participants,
|
|
path: replacePathPrefix(item.path || '', pathReplace, pathReplaceWith),
|
|
peak:
|
|
item.replayGain && (item.replayGain.albumPeak || item.replayGain.trackPeak)
|
|
? {
|
|
album: item.replayGain.albumPeak,
|
|
track: item.replayGain.trackPeak,
|
|
}
|
|
: null,
|
|
playCount: item?.playCount || 0,
|
|
playlistItemId: playlistIndex !== undefined ? playlistIndex.toString() : undefined,
|
|
releaseDate: null,
|
|
releaseYear: item.year || null,
|
|
sampleRate: item.samplingRate || null,
|
|
size: item.size,
|
|
sortName: item.title,
|
|
tags: null,
|
|
trackNumber: item.track || 1,
|
|
trackSubtitle: null,
|
|
updatedAt: '',
|
|
userFavorite: Boolean(item.starred) || false,
|
|
userRating: item.userRating || null,
|
|
};
|
|
};
|
|
|
|
const normalizeAlbumArtist = (
|
|
item:
|
|
| (z.infer<typeof ssType._response.albumArtist> & {
|
|
similarArtists?: z.infer<
|
|
typeof ssType._response.artistInfo
|
|
>['artistInfo']['similarArtist'];
|
|
})
|
|
| (z.infer<typeof ssType._response.artistListEntry> & {
|
|
similarArtists?: z.infer<
|
|
typeof ssType._response.artistInfo
|
|
>['artistInfo']['similarArtist'];
|
|
}),
|
|
server?: null | ServerListItemWithCredential,
|
|
): AlbumArtist => {
|
|
return {
|
|
_itemType: LibraryItem.ALBUM_ARTIST,
|
|
_serverId: server?.id || 'unknown',
|
|
_serverType: ServerType.SUBSONIC,
|
|
albumCount: item.albumCount ? Number(item.albumCount) : 0,
|
|
biography: null,
|
|
duration: null,
|
|
genres: [],
|
|
id: item.id.toString(),
|
|
imageId: item.coverArt?.toString() || null,
|
|
imageUrl: null,
|
|
lastPlayedAt: null,
|
|
mbz: null,
|
|
name: item.name,
|
|
playCount: null,
|
|
similarArtists:
|
|
item.similarArtists?.map((artist) => ({
|
|
id: artist.id,
|
|
imageId: null,
|
|
imageUrl: null,
|
|
name: artist.name,
|
|
userFavorite: Boolean(artist.starred) || false,
|
|
userRating: artist.userRating || null,
|
|
})) || [],
|
|
songCount: null,
|
|
userFavorite: Boolean(item.starred) || false,
|
|
userRating: null,
|
|
};
|
|
};
|
|
|
|
const PRIMARY_RELEASE_TYPES = ['album', 'ep', 'single', 'broadcast', 'other'];
|
|
|
|
const getReleaseType = (
|
|
item: z.infer<typeof ssType._response.album> | z.infer<typeof ssType._response.albumListEntry>,
|
|
) => {
|
|
if (!item.releaseTypes) {
|
|
return null;
|
|
}
|
|
|
|
// Return the first primary release type
|
|
return item.releaseTypes.find((type) => PRIMARY_RELEASE_TYPES.includes(type)) || null;
|
|
};
|
|
|
|
const normalizeAlbum = (
|
|
item: z.infer<typeof ssType._response.album> | z.infer<typeof ssType._response.albumListEntry>,
|
|
server?: null | ServerListItemWithCredential,
|
|
pathReplace?: string,
|
|
pathReplaceWith?: string,
|
|
): Album => {
|
|
const discTitleMap = new Map<number, string>();
|
|
|
|
(item as z.infer<typeof ssType._response.album>).discTitles?.forEach((discTitle) => {
|
|
discTitleMap.set(discTitle.disc, discTitle.title);
|
|
});
|
|
|
|
const releaseDate =
|
|
item.releaseDate &&
|
|
typeof item.releaseDate.year === 'number' &&
|
|
typeof item.releaseDate.month === 'number' &&
|
|
typeof item.releaseDate.day === 'number'
|
|
? `${item.releaseDate.year}-${item.releaseDate.month}-${item.releaseDate.day}`
|
|
: null;
|
|
|
|
return {
|
|
_itemType: LibraryItem.ALBUM,
|
|
_serverId: server?.id || 'unknown',
|
|
_serverType: ServerType.SUBSONIC,
|
|
albumArtistName: item.artist,
|
|
albumArtists: getArtistList(item.artists, item.artistId, item.artist),
|
|
artists: [],
|
|
comment: null,
|
|
createdAt: item.created,
|
|
duration: item.duration * 1000,
|
|
explicitStatus:
|
|
item.explicitStatus === 'explicit'
|
|
? ExplicitStatus.EXPLICIT
|
|
: item.explicitStatus === 'clean'
|
|
? ExplicitStatus.CLEAN
|
|
: null,
|
|
genres: getGenres(item, server),
|
|
id: item.id.toString(),
|
|
imageId: item.coverArt?.toString() || null,
|
|
imageUrl: null,
|
|
isCompilation: null,
|
|
lastPlayedAt: null,
|
|
mbzId: null,
|
|
mbzReleaseGroupId: null,
|
|
name: item.name,
|
|
originalDate: releaseDate,
|
|
originalYear: item.year || null,
|
|
participants: getParticipants(item),
|
|
playCount: null,
|
|
recordLabels: item.recordLabels?.map((item) => item.name) || [],
|
|
releaseDate,
|
|
releaseType: getReleaseType(item),
|
|
releaseTypes: item.releaseTypes || [],
|
|
releaseYear: item.year || null,
|
|
size: null,
|
|
songCount: item.songCount,
|
|
songs:
|
|
(item as z.infer<typeof ssType._response.album>).song?.map((song) =>
|
|
normalizeSong(song, server, pathReplace, pathReplaceWith, undefined, discTitleMap),
|
|
) || [],
|
|
sortName: item.title,
|
|
tags: null,
|
|
updatedAt: item.created,
|
|
userFavorite: Boolean(item.starred) || false,
|
|
userRating: item.userRating || null,
|
|
version: item.version || null,
|
|
};
|
|
};
|
|
|
|
const normalizePlaylist = (
|
|
item:
|
|
| z.infer<typeof ssType._response.playlist>
|
|
| z.infer<typeof ssType._response.playlistListEntry>,
|
|
server?: null | ServerListItemWithCredential,
|
|
): Playlist => {
|
|
return {
|
|
_itemType: LibraryItem.PLAYLIST,
|
|
_serverId: server?.id || 'unknown',
|
|
_serverType: ServerType.SUBSONIC,
|
|
description: item.comment || null,
|
|
duration: item.duration * 1000,
|
|
genres: [],
|
|
id: item.id.toString(),
|
|
imageId: item.coverArt?.toString() || null,
|
|
imageUrl: null,
|
|
name: item.name,
|
|
owner: item.owner,
|
|
ownerId: item.owner,
|
|
public: item.public,
|
|
size: null,
|
|
songCount: item.songCount,
|
|
};
|
|
};
|
|
|
|
const normalizeGenre = (
|
|
item: z.infer<typeof ssType._response.genre>,
|
|
server: null | ServerListItemWithCredential,
|
|
): Genre => {
|
|
return {
|
|
_itemType: LibraryItem.GENRE,
|
|
_serverId: server?.id || 'unknown',
|
|
_serverType: ServerType.SUBSONIC,
|
|
albumCount: item.albumCount,
|
|
id: item.value,
|
|
imageId: null,
|
|
imageUrl: null,
|
|
name: item.value,
|
|
songCount: item.songCount,
|
|
};
|
|
};
|
|
|
|
const normalizeFolder = (
|
|
item: z.infer<typeof ssType._response.directory>,
|
|
server?: null | ServerListItemWithCredential,
|
|
pathReplace?: string,
|
|
pathReplaceWith?: string,
|
|
): Folder => {
|
|
const results = item.child?.reduce(
|
|
(acc: { folders: Folder[]; songs: Song[] }, item) => {
|
|
const isDirectory = item.isDir === true;
|
|
|
|
if (isDirectory) {
|
|
const folder = normalizeFolder(item, server);
|
|
acc.folders.push(folder);
|
|
} else {
|
|
const song = normalizeSong(item, server, pathReplace, pathReplaceWith);
|
|
acc.songs.push(song);
|
|
}
|
|
|
|
return acc;
|
|
},
|
|
{
|
|
folders: [],
|
|
songs: [],
|
|
},
|
|
);
|
|
|
|
return {
|
|
_itemType: LibraryItem.FOLDER,
|
|
_serverId: server?.id || 'unknown',
|
|
_serverType: ServerType.SUBSONIC,
|
|
children: {
|
|
folders: results?.folders || [],
|
|
songs: results?.songs || [],
|
|
},
|
|
id: item.id.toString(),
|
|
imageId: item.coverArt?.toString() || null,
|
|
imageUrl: null,
|
|
name: item.title,
|
|
parentId: item.parent,
|
|
};
|
|
};
|
|
|
|
const normalizeInternetRadioStation = (
|
|
item: z.infer<typeof ssType._response.internetRadioStation>,
|
|
): InternetRadioStation => {
|
|
return {
|
|
homepageUrl: item.homepageUrl || null,
|
|
id: item.id,
|
|
name: item.name,
|
|
streamUrl: item.streamUrl,
|
|
};
|
|
};
|
|
|
|
export const ssNormalize = {
|
|
album: normalizeAlbum,
|
|
albumArtist: normalizeAlbumArtist,
|
|
folder: normalizeFolder,
|
|
genre: normalizeGenre,
|
|
internetRadioStation: normalizeInternetRadioStation,
|
|
playlist: normalizePlaylist,
|
|
song: normalizeSong,
|
|
};
|