Add new navidrome api controller
This commit is contained in:
parent
52049ce163
commit
ea8c63b71b
2 changed files with 733 additions and 0 deletions
505
src/renderer/api/navidrome/navidrome-controller.ts
Normal file
505
src/renderer/api/navidrome/navidrome-controller.ts
Normal file
|
@ -0,0 +1,505 @@
|
||||||
|
import {
|
||||||
|
AlbumArtistDetailArgs,
|
||||||
|
AlbumArtistDetailResponse,
|
||||||
|
AddToPlaylistArgs,
|
||||||
|
AddToPlaylistResponse,
|
||||||
|
CreatePlaylistResponse,
|
||||||
|
CreatePlaylistArgs,
|
||||||
|
DeletePlaylistArgs,
|
||||||
|
DeletePlaylistResponse,
|
||||||
|
AlbumArtistListResponse,
|
||||||
|
AlbumArtistListArgs,
|
||||||
|
albumArtistListSortMap,
|
||||||
|
sortOrderMap,
|
||||||
|
AuthenticationResponse,
|
||||||
|
UserListResponse,
|
||||||
|
UserListArgs,
|
||||||
|
userListSortMap,
|
||||||
|
GenreListArgs,
|
||||||
|
GenreListResponse,
|
||||||
|
AlbumDetailResponse,
|
||||||
|
AlbumDetailArgs,
|
||||||
|
AlbumListArgs,
|
||||||
|
albumListSortMap,
|
||||||
|
AlbumListResponse,
|
||||||
|
SongListResponse,
|
||||||
|
SongListArgs,
|
||||||
|
songListSortMap,
|
||||||
|
SongDetailResponse,
|
||||||
|
SongDetailArgs,
|
||||||
|
UpdatePlaylistArgs,
|
||||||
|
UpdatePlaylistResponse,
|
||||||
|
PlaylistListResponse,
|
||||||
|
PlaylistDetailArgs,
|
||||||
|
PlaylistListArgs,
|
||||||
|
playlistListSortMap,
|
||||||
|
PlaylistDetailResponse,
|
||||||
|
PlaylistSongListArgs,
|
||||||
|
PlaylistSongListResponse,
|
||||||
|
RemoveFromPlaylistResponse,
|
||||||
|
RemoveFromPlaylistArgs,
|
||||||
|
} from '../types';
|
||||||
|
import { ndApiClient } from '/@/renderer/api/navidrome/navidrome-api';
|
||||||
|
import { ndNormalize } from '/@/renderer/api/navidrome/navidrome-normalize';
|
||||||
|
import { ndType } from '/@/renderer/api/navidrome/navidrome-types';
|
||||||
|
|
||||||
|
const authenticate = async (
|
||||||
|
url: string,
|
||||||
|
body: { password: string; username: string },
|
||||||
|
): Promise<AuthenticationResponse> => {
|
||||||
|
const cleanServerUrl = url.replace(/\/$/, '');
|
||||||
|
|
||||||
|
const res = await ndApiClient({ server: cleanServerUrl }).authenticate({
|
||||||
|
body: {
|
||||||
|
password: body.password,
|
||||||
|
username: body.username,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status !== 200) {
|
||||||
|
throw new Error('Failed to authenticate');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
credential: `u=${body.username}&s=${res.body.data.subsonicSalt}&t=${res.body.data.subsonicToken}`,
|
||||||
|
ndCredential: res.body.data.token,
|
||||||
|
userId: res.body.data.id,
|
||||||
|
username: res.body.data.username,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const getUserList = async (args: UserListArgs): Promise<UserListResponse> => {
|
||||||
|
const { query, server, signal } = args;
|
||||||
|
|
||||||
|
if (!server) {
|
||||||
|
throw new Error('No server');
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await ndApiClient({ server, signal }).getUserList({
|
||||||
|
query: {
|
||||||
|
_end: query.startIndex + (query.limit || 0),
|
||||||
|
_order: sortOrderMap.navidrome[query.sortOrder],
|
||||||
|
_sort: userListSortMap.navidrome[query.sortBy],
|
||||||
|
_start: query.startIndex,
|
||||||
|
...query.ndParams,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status !== 200) {
|
||||||
|
throw new Error('Failed to get user list');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
items: res.body.data.map((user) => ndNormalize.user(user)),
|
||||||
|
startIndex: query?.startIndex || 0,
|
||||||
|
totalRecordCount: Number(res.body.headers.get('x-total-count') || 0),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const getGenreList = async (args: GenreListArgs): Promise<GenreListResponse> => {
|
||||||
|
const { server, signal } = args;
|
||||||
|
|
||||||
|
if (!server) {
|
||||||
|
throw new Error('No server');
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await ndApiClient({ server, signal }).getGenreList({});
|
||||||
|
|
||||||
|
if (res.status !== 200) {
|
||||||
|
throw new Error('Failed to get genre list');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
items: res.body.data,
|
||||||
|
startIndex: 0,
|
||||||
|
totalRecordCount: Number(res.body.headers.get('x-total-count') || 0),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAlbumArtistDetail = async (
|
||||||
|
args: AlbumArtistDetailArgs,
|
||||||
|
): Promise<AlbumArtistDetailResponse> => {
|
||||||
|
const { query, server, signal } = args;
|
||||||
|
|
||||||
|
if (!server) {
|
||||||
|
throw new Error('No server');
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await ndApiClient({ server, signal }).getAlbumArtistDetail({
|
||||||
|
params: {
|
||||||
|
id: query.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status !== 200) {
|
||||||
|
throw new Error('Failed to get album artist detail');
|
||||||
|
}
|
||||||
|
|
||||||
|
return ndNormalize.albumArtist(res.body.data, server);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAlbumArtistList = async (args: AlbumArtistListArgs): Promise<AlbumArtistListResponse> => {
|
||||||
|
const { query, server, signal } = args;
|
||||||
|
|
||||||
|
if (!server) {
|
||||||
|
throw new Error('No server');
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await ndApiClient({ server, signal }).getAlbumArtistList({
|
||||||
|
query: {
|
||||||
|
_end: query.startIndex + (query.limit || 0),
|
||||||
|
_order: sortOrderMap.navidrome[query.sortOrder],
|
||||||
|
_sort: albumArtistListSortMap.navidrome[query.sortBy],
|
||||||
|
_start: query.startIndex,
|
||||||
|
name: query.searchTerm,
|
||||||
|
...query._custom?.navidrome,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status !== 200) {
|
||||||
|
throw new Error('Failed to get album artist list');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
items: res.body.data.map((albumArtist) => ndNormalize.albumArtist(albumArtist, server)),
|
||||||
|
startIndex: query.startIndex,
|
||||||
|
totalRecordCount: Number(res.body.headers.get('x-total-count') || 0),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAlbumDetail = async (args: AlbumDetailArgs): Promise<AlbumDetailResponse> => {
|
||||||
|
const { query, server, signal } = args;
|
||||||
|
|
||||||
|
if (!server) {
|
||||||
|
throw new Error('No server');
|
||||||
|
}
|
||||||
|
|
||||||
|
const albumRes = await ndApiClient({ server, signal }).getAlbumDetail({
|
||||||
|
params: {
|
||||||
|
id: query.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const songsData = await ndApiClient({ server, signal }).getSongList({
|
||||||
|
query: {
|
||||||
|
_end: 0,
|
||||||
|
_order: 'ASC',
|
||||||
|
_sort: 'album',
|
||||||
|
_start: 0,
|
||||||
|
album_id: [query.id],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (albumRes.status !== 200 || songsData.status !== 200) {
|
||||||
|
throw new Error('Failed to get album detail');
|
||||||
|
}
|
||||||
|
|
||||||
|
return ndNormalize.album({ ...albumRes.body.data, songs: songsData.body.data }, server);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAlbumList = async (args: AlbumListArgs): Promise<AlbumListResponse> => {
|
||||||
|
const { query, server, signal } = args;
|
||||||
|
|
||||||
|
if (!server) {
|
||||||
|
throw new Error('No server');
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await ndApiClient({ server, signal }).getAlbumList({
|
||||||
|
query: {
|
||||||
|
_end: query.startIndex + (query.limit || 0),
|
||||||
|
_order: sortOrderMap.navidrome[query.sortOrder],
|
||||||
|
_sort: albumListSortMap.navidrome[query.sortBy],
|
||||||
|
_start: query.startIndex,
|
||||||
|
artist_id: query.artistIds?.[0],
|
||||||
|
name: query.searchTerm,
|
||||||
|
...query._custom?.navidrome,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status !== 200) {
|
||||||
|
throw new Error('Failed to get album list');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
items: res.body.data.map((album) => ndNormalize.album(album, server)),
|
||||||
|
startIndex: query?.startIndex || 0,
|
||||||
|
totalRecordCount: Number(res.body.headers.get('x-total-count') || 0),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSongList = async (args: SongListArgs): Promise<SongListResponse> => {
|
||||||
|
const { query, server, signal } = args;
|
||||||
|
|
||||||
|
if (!server) {
|
||||||
|
throw new Error('No server');
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await ndApiClient({ server, signal }).getSongList({
|
||||||
|
query: {
|
||||||
|
_end: query.startIndex + (query.limit || -1),
|
||||||
|
_order: sortOrderMap.navidrome[query.sortOrder],
|
||||||
|
_sort: songListSortMap.navidrome[query.sortBy],
|
||||||
|
_start: query.startIndex,
|
||||||
|
album_id: query.albumIds,
|
||||||
|
artist_id: query.artistIds,
|
||||||
|
title: query.searchTerm,
|
||||||
|
...query._custom?.navidrome,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status !== 200) {
|
||||||
|
throw new Error('Failed to get song list');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
items: res.body.data.map((song) => ndNormalize.song(song, server, '')),
|
||||||
|
startIndex: query?.startIndex || 0,
|
||||||
|
totalRecordCount: Number(res.body.headers.get('x-total-count') || 0),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSongDetail = async (args: SongDetailArgs): Promise<SongDetailResponse> => {
|
||||||
|
const { query, server, signal } = args;
|
||||||
|
|
||||||
|
if (!server) {
|
||||||
|
throw new Error('No server');
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await ndApiClient({ server, signal }).getSongDetail({
|
||||||
|
params: {
|
||||||
|
id: query.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status !== 200) {
|
||||||
|
throw new Error('Failed to get song detail');
|
||||||
|
}
|
||||||
|
|
||||||
|
return ndNormalize.song(res.body.data, server, '');
|
||||||
|
};
|
||||||
|
|
||||||
|
const createPlaylist = async (args: CreatePlaylistArgs): Promise<CreatePlaylistResponse> => {
|
||||||
|
const { body, server } = args;
|
||||||
|
|
||||||
|
if (!server) {
|
||||||
|
throw new Error('No server');
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await ndApiClient({ server }).createPlaylist({
|
||||||
|
body: {
|
||||||
|
comment: body.comment,
|
||||||
|
name: body.name,
|
||||||
|
public: body._custom.navidrome?.public,
|
||||||
|
rules: body._custom.navidrome?.rules,
|
||||||
|
sync: body._custom.navidrome?.sync,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status !== 200) {
|
||||||
|
throw new Error('Failed to create playlist');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: res.body.data.id,
|
||||||
|
name: body.name,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const updatePlaylist = async (args: UpdatePlaylistArgs): Promise<UpdatePlaylistResponse> => {
|
||||||
|
const { query, body, server, signal } = args;
|
||||||
|
|
||||||
|
if (!server) {
|
||||||
|
throw new Error('No server');
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await ndApiClient({ server, signal }).updatePlaylist({
|
||||||
|
body: {
|
||||||
|
comment: body.comment || '',
|
||||||
|
name: body.name,
|
||||||
|
public: body.ndParams?.public || false,
|
||||||
|
rules: body.ndParams?.rules ? body.ndParams?.rules : undefined,
|
||||||
|
sync: body.ndParams?.sync || undefined,
|
||||||
|
},
|
||||||
|
params: {
|
||||||
|
id: query.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status !== 200) {
|
||||||
|
throw new Error('Failed to update playlist');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: res.body.data.id,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const deletePlaylist = async (args: DeletePlaylistArgs): Promise<DeletePlaylistResponse> => {
|
||||||
|
const { query, server } = args;
|
||||||
|
|
||||||
|
if (!server) {
|
||||||
|
throw new Error('No server');
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await ndApiClient({ server }).deletePlaylist({
|
||||||
|
body: null,
|
||||||
|
params: {
|
||||||
|
id: query.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status !== 200) {
|
||||||
|
throw new Error('Failed to delete playlist');
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPlaylistList = async (args: PlaylistListArgs): Promise<PlaylistListResponse> => {
|
||||||
|
const { query, server, signal } = args;
|
||||||
|
|
||||||
|
if (!server) {
|
||||||
|
throw new Error('No server');
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await ndApiClient({ server, signal }).getPlaylistList({
|
||||||
|
query: {
|
||||||
|
_end: query.startIndex + (query.limit || 0),
|
||||||
|
_order: sortOrderMap.navidrome[query.sortOrder],
|
||||||
|
_sort: query.sortBy ? playlistListSortMap.navidrome[query.sortBy] : undefined,
|
||||||
|
_start: query.startIndex,
|
||||||
|
...query._custom?.navidrome,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status !== 200) {
|
||||||
|
throw new Error('Failed to get playlist list');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
items: res.body.data.map((item) => ndNormalize.playlist(item, server)),
|
||||||
|
startIndex: query?.startIndex || 0,
|
||||||
|
totalRecordCount: Number(res.body.headers.get('x-total-count') || 0),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPlaylistDetail = async (args: PlaylistDetailArgs): Promise<PlaylistDetailResponse> => {
|
||||||
|
const { query, server, signal } = args;
|
||||||
|
|
||||||
|
if (!server) {
|
||||||
|
throw new Error('No server');
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await ndApiClient({ server, signal }).getPlaylistDetail({
|
||||||
|
params: {
|
||||||
|
id: query.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status !== 200) {
|
||||||
|
throw new Error('Failed to get playlist detail');
|
||||||
|
}
|
||||||
|
|
||||||
|
return ndNormalize.playlist(res.body.data, server);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPlaylistSongList = async (
|
||||||
|
args: PlaylistSongListArgs,
|
||||||
|
): Promise<PlaylistSongListResponse> => {
|
||||||
|
const { query, server, signal } = args;
|
||||||
|
|
||||||
|
if (!server) {
|
||||||
|
throw new Error('No server');
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await ndApiClient({ server, signal }).getPlaylistSongList({
|
||||||
|
params: {
|
||||||
|
id: query.id,
|
||||||
|
},
|
||||||
|
query: {
|
||||||
|
_end: query.startIndex + (query.limit || 0),
|
||||||
|
_order: query.sortOrder ? sortOrderMap.navidrome[query.sortOrder] : 'ASC',
|
||||||
|
_sort: query.sortBy ? songListSortMap.navidrome[query.sortBy] : ndType._enum.songList.ID,
|
||||||
|
_start: query.startIndex,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status !== 200) {
|
||||||
|
throw new Error('Failed to get playlist song list');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
items: res.body.data.map((item) => ndNormalize.song(item, server, '')),
|
||||||
|
startIndex: query?.startIndex || 0,
|
||||||
|
totalRecordCount: Number(res.body.headers.get('x-total-count') || 0),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const addToPlaylist = async (args: AddToPlaylistArgs): Promise<AddToPlaylistResponse> => {
|
||||||
|
const { body, query, server } = args;
|
||||||
|
|
||||||
|
if (!server) {
|
||||||
|
throw new Error('No server');
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await ndApiClient({ server }).addToPlaylist({
|
||||||
|
body: {
|
||||||
|
ids: body.songId,
|
||||||
|
},
|
||||||
|
params: {
|
||||||
|
id: query.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status !== 200) {
|
||||||
|
throw new Error('Failed to add to playlist');
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeFromPlaylist = async (
|
||||||
|
args: RemoveFromPlaylistArgs,
|
||||||
|
): Promise<RemoveFromPlaylistResponse> => {
|
||||||
|
const { query, server, signal } = args;
|
||||||
|
|
||||||
|
if (!server) {
|
||||||
|
throw new Error('No server');
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await ndApiClient({ server, signal }).removeFromPlaylist({
|
||||||
|
body: null,
|
||||||
|
params: {
|
||||||
|
id: query.id,
|
||||||
|
},
|
||||||
|
query: {
|
||||||
|
ids: query.songId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status !== 200) {
|
||||||
|
throw new Error('Failed to remove from playlist');
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ndController = {
|
||||||
|
addToPlaylist,
|
||||||
|
authenticate,
|
||||||
|
createPlaylist,
|
||||||
|
deletePlaylist,
|
||||||
|
getAlbumArtistDetail,
|
||||||
|
getAlbumArtistList,
|
||||||
|
getAlbumDetail,
|
||||||
|
getAlbumList,
|
||||||
|
getGenreList,
|
||||||
|
getPlaylistDetail,
|
||||||
|
getPlaylistList,
|
||||||
|
getPlaylistSongList,
|
||||||
|
getSongDetail,
|
||||||
|
getSongList,
|
||||||
|
getUserList,
|
||||||
|
removeFromPlaylist,
|
||||||
|
updatePlaylist,
|
||||||
|
};
|
228
src/renderer/api/navidrome/navidrome-normalize.ts
Normal file
228
src/renderer/api/navidrome/navidrome-normalize.ts
Normal file
|
@ -0,0 +1,228 @@
|
||||||
|
import { nanoid } from 'nanoid';
|
||||||
|
import { Song, LibraryItem, Album, AlbumArtist, Playlist, User } from '/@/renderer/api/types';
|
||||||
|
import { ServerListItem, ServerType } from '/@/renderer/types';
|
||||||
|
import z from 'zod';
|
||||||
|
import { ndType } from './navidrome-types';
|
||||||
|
|
||||||
|
const getCoverArtUrl = (args: {
|
||||||
|
baseUrl: string;
|
||||||
|
coverArtId: string;
|
||||||
|
credential: string;
|
||||||
|
size: number;
|
||||||
|
}) => {
|
||||||
|
const size = args.size ? args.size : 250;
|
||||||
|
|
||||||
|
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}`
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeSong = (
|
||||||
|
item: z.infer<typeof ndType._response.song> | z.infer<typeof ndType._response.playlistSong>,
|
||||||
|
server: ServerListItem,
|
||||||
|
deviceId: string,
|
||||||
|
imageSize?: number,
|
||||||
|
): Song => {
|
||||||
|
let id;
|
||||||
|
let playlistItemId;
|
||||||
|
|
||||||
|
// Dynamically determine the id field based on whether or not the item is a playlist song
|
||||||
|
if ('mediaFileId' in item) {
|
||||||
|
id = item.mediaFileId;
|
||||||
|
playlistItemId = item.id;
|
||||||
|
} else {
|
||||||
|
id = item.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
const imageUrl = getCoverArtUrl({
|
||||||
|
baseUrl: server.url,
|
||||||
|
coverArtId: id,
|
||||||
|
credential: server.credential,
|
||||||
|
size: imageSize || 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
const imagePlaceholderUrl = null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
album: item.album,
|
||||||
|
albumArtists: [{ id: item.artistId, imageUrl: null, name: item.artist }],
|
||||||
|
albumId: item.albumId,
|
||||||
|
artistName: item.artist,
|
||||||
|
artists: [{ id: item.artistId, imageUrl: null, name: item.artist }],
|
||||||
|
bitRate: item.bitRate,
|
||||||
|
bpm: item.bpm ? item.bpm : null,
|
||||||
|
channels: item.channels ? item.channels : null,
|
||||||
|
comment: item.comment ? item.comment : null,
|
||||||
|
compilation: item.compilation,
|
||||||
|
container: item.suffix,
|
||||||
|
createdAt: item.createdAt.split('T')[0],
|
||||||
|
discNumber: item.discNumber,
|
||||||
|
duration: item.duration,
|
||||||
|
genres: item.genres,
|
||||||
|
id,
|
||||||
|
imagePlaceholderUrl,
|
||||||
|
imageUrl,
|
||||||
|
itemType: LibraryItem.SONG,
|
||||||
|
lastPlayedAt: item.playDate.includes('0001-') ? null : item.playDate,
|
||||||
|
name: item.title,
|
||||||
|
path: item.path,
|
||||||
|
playCount: item.playCount,
|
||||||
|
playlistItemId,
|
||||||
|
releaseDate: new Date(item.year, 0, 1).toISOString(),
|
||||||
|
releaseYear: String(item.year),
|
||||||
|
serverId: server.id,
|
||||||
|
serverType: ServerType.NAVIDROME,
|
||||||
|
size: item.size,
|
||||||
|
streamUrl: `${server.url}/rest/stream.view?id=${id}&v=1.13.0&c=feishin_${deviceId}&${server.credential}`,
|
||||||
|
trackNumber: item.trackNumber,
|
||||||
|
uniqueId: nanoid(),
|
||||||
|
updatedAt: item.updatedAt,
|
||||||
|
userFavorite: item.starred || false,
|
||||||
|
userRating: item.rating || null,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeAlbum = (
|
||||||
|
item: z.infer<typeof ndType._response.album> & {
|
||||||
|
songs?: z.infer<typeof ndType._response.songList>;
|
||||||
|
},
|
||||||
|
server: ServerListItem,
|
||||||
|
imageSize?: number,
|
||||||
|
): Album => {
|
||||||
|
const imageUrl = getCoverArtUrl({
|
||||||
|
baseUrl: server.url,
|
||||||
|
coverArtId: item.coverArtId || item.id,
|
||||||
|
credential: server.credential,
|
||||||
|
size: imageSize || 300,
|
||||||
|
});
|
||||||
|
|
||||||
|
const imagePlaceholderUrl = null;
|
||||||
|
|
||||||
|
const imageBackdropUrl = imageUrl?.replace(/size=\d+/, 'size=1000') || null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
albumArtists: [{ id: item.albumArtistId, imageUrl: null, name: item.albumArtist }],
|
||||||
|
artists: [{ id: item.artistId, imageUrl: null, name: item.artist }],
|
||||||
|
backdropImageUrl: imageBackdropUrl,
|
||||||
|
createdAt: item.createdAt.split('T')[0],
|
||||||
|
duration: item.duration * 1000 || null,
|
||||||
|
genres: item.genres,
|
||||||
|
id: item.id,
|
||||||
|
imagePlaceholderUrl,
|
||||||
|
imageUrl,
|
||||||
|
isCompilation: item.compilation,
|
||||||
|
itemType: LibraryItem.ALBUM,
|
||||||
|
lastPlayedAt: item.playDate.includes('0001-') ? null : item.playDate,
|
||||||
|
name: item.name,
|
||||||
|
playCount: item.playCount,
|
||||||
|
releaseDate: new Date(item.minYear, 0, 1).toISOString(),
|
||||||
|
releaseYear: item.minYear,
|
||||||
|
serverId: server.id,
|
||||||
|
serverType: ServerType.NAVIDROME,
|
||||||
|
size: item.size,
|
||||||
|
songCount: item.songCount,
|
||||||
|
songs: item.songs ? item.songs.map((song) => normalizeSong(song, server, '')) : undefined,
|
||||||
|
uniqueId: nanoid(),
|
||||||
|
updatedAt: item.updatedAt,
|
||||||
|
userFavorite: item.starred,
|
||||||
|
userRating: item.rating || null,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeAlbumArtist = (
|
||||||
|
item: z.infer<typeof ndType._response.albumArtist>,
|
||||||
|
server: ServerListItem,
|
||||||
|
): AlbumArtist => {
|
||||||
|
const imageUrl =
|
||||||
|
item.largeImageUrl === '/app/artist-placeholder.webp' ? null : item.largeImageUrl;
|
||||||
|
|
||||||
|
return {
|
||||||
|
albumCount: item.albumCount,
|
||||||
|
backgroundImageUrl: null,
|
||||||
|
biography: item.biography || null,
|
||||||
|
duration: null,
|
||||||
|
genres: item.genres,
|
||||||
|
id: item.id,
|
||||||
|
imageUrl: imageUrl || null,
|
||||||
|
itemType: LibraryItem.ALBUM_ARTIST,
|
||||||
|
lastPlayedAt: item.playDate.includes('0001-') ? null : item.playDate,
|
||||||
|
name: item.name,
|
||||||
|
playCount: item.playCount,
|
||||||
|
serverId: server?.id || '',
|
||||||
|
serverType: ServerType.NAVIDROME,
|
||||||
|
similarArtists: null,
|
||||||
|
// similarArtists:
|
||||||
|
// item.similarArtists?.map((artist) => ({
|
||||||
|
// id: artist.id,
|
||||||
|
// imageUrl: artist?.artistImageUrl || null,
|
||||||
|
// name: artist.name,
|
||||||
|
// })) || null,
|
||||||
|
songCount: item.songCount,
|
||||||
|
userFavorite: item.starred,
|
||||||
|
userRating: item.rating,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizePlaylist = (
|
||||||
|
item: z.infer<typeof ndType._response.playlist>,
|
||||||
|
server: ServerListItem,
|
||||||
|
imageSize?: number,
|
||||||
|
): Playlist => {
|
||||||
|
const imageUrl = getCoverArtUrl({
|
||||||
|
baseUrl: server.url,
|
||||||
|
coverArtId: item.id,
|
||||||
|
credential: server.credential,
|
||||||
|
size: imageSize || 300,
|
||||||
|
});
|
||||||
|
|
||||||
|
const imagePlaceholderUrl = null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
description: item.comment,
|
||||||
|
duration: item.duration * 1000,
|
||||||
|
genres: [],
|
||||||
|
id: item.id,
|
||||||
|
imagePlaceholderUrl,
|
||||||
|
imageUrl,
|
||||||
|
itemType: LibraryItem.PLAYLIST,
|
||||||
|
name: item.name,
|
||||||
|
owner: item.ownerName,
|
||||||
|
ownerId: item.ownerId,
|
||||||
|
public: item.public,
|
||||||
|
rules: item?.rules || null,
|
||||||
|
serverId: server.id,
|
||||||
|
serverType: ServerType.NAVIDROME,
|
||||||
|
size: item.size,
|
||||||
|
songCount: item.songCount,
|
||||||
|
sync: item.sync,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeUser = (item: z.infer<typeof ndType._response.user>): User => {
|
||||||
|
return {
|
||||||
|
createdAt: item.createdAt,
|
||||||
|
email: item.email || null,
|
||||||
|
id: item.id,
|
||||||
|
isAdmin: item.isAdmin,
|
||||||
|
lastLoginAt: item.lastLoginAt,
|
||||||
|
name: item.userName,
|
||||||
|
updatedAt: item.updatedAt,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ndNormalize = {
|
||||||
|
album: normalizeAlbum,
|
||||||
|
albumArtist: normalizeAlbumArtist,
|
||||||
|
playlist: normalizePlaylist,
|
||||||
|
song: normalizeSong,
|
||||||
|
user: normalizeUser,
|
||||||
|
};
|
Reference in a new issue