reorganize global types to shared directory

This commit is contained in:
jeffvli 2025-05-20 18:08:51 -07:00
parent 26c02e03c5
commit 9db2e51d2d
17 changed files with 160 additions and 144 deletions

View file

@ -0,0 +1,327 @@
import { nanoid } from 'nanoid';
import { z } from 'zod';
import { ssType } from '/@/shared/api/subsonic/subsonic-types';
import {
Album,
AlbumArtist,
Genre,
LibraryItem,
Playlist,
QueueSong,
RelatedArtist,
ServerListItem,
ServerType,
} from '/@/shared/types/domain-types';
const getCoverArtUrl = (args: {
baseUrl: string | undefined;
coverArtId?: string;
credential: string | undefined;
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 getArtists = (
item:
| z.infer<typeof ssType._response.album>
| z.infer<typeof ssType._response.albumListEntry>
| z.infer<typeof ssType._response.song>,
) => {
const albumArtists: RelatedArtist[] = item.albumArtists
? item.albumArtists.map((item) => ({
id: item.id.toString(),
imageUrl: null,
name: item.name,
}))
: [
{
id: item.artistId?.toString() || '',
imageUrl: null,
name: item.artist || '',
},
];
const artists: RelatedArtist[] = item.artists
? item.artists.map((item) => ({
id: item.id.toString(),
imageUrl: null,
name: item.name,
}))
: [
{
id: item.artistId?.toString() || '',
imageUrl: null,
name: item.artist || '',
},
];
let participants: null | Record<string, RelatedArtist[]> = null;
if (item.contributors) {
participants = {};
for (const contributor of item.contributors) {
const artist = {
id: contributor.artist.id?.toString() || '',
imageUrl: null,
name: contributor.artist.name || '',
};
const role = contributor.subRole
? `${contributor.role} (${contributor.subRole})`
: contributor.role;
if (role in participants) {
participants[role].push(artist);
} else {
participants[role] = [artist];
}
}
}
return { albumArtists, artists, 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,
},
]
: [];
};
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 || '',
albumId: item.albumId?.toString() || '',
artistName: item.artist || '',
...getArtists(item),
bitRate: item.bitRate || 0,
bpm: item.bpm || null,
channels: 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,
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,
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 = (
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,
...getArtists(item),
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,
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: item.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,
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,
};

View file

@ -0,0 +1,609 @@
import { z } from 'zod';
const baseResponse = z.object({
'subsonic-response': z.object({
status: z.string(),
version: z.string(),
}),
});
const authenticate = z.null();
const authenticateParameters = z.object({
c: z.string(),
f: z.string(),
p: z.string().optional(),
s: z.string().optional(),
t: z.string().optional(),
u: z.string(),
v: z.string(),
});
const id = z.number().or(z.string());
const createFavoriteParameters = z.object({
albumId: z.array(z.string()).optional(),
artistId: z.array(z.string()).optional(),
id: z.array(z.string()).optional(),
});
const createFavorite = z.null();
const removeFavoriteParameters = z.object({
albumId: z.array(z.string()).optional(),
artistId: z.array(z.string()).optional(),
id: z.array(z.string()).optional(),
});
const removeFavorite = z.null();
const setRatingParameters = z.object({
id: z.string(),
rating: z.number(),
});
const setRating = z.null();
const musicFolder = z.object({
id,
name: z.string(),
});
const musicFolderList = z.object({
musicFolders: z.object({
musicFolder: z.array(musicFolder),
}),
});
const songGain = z.object({
albumGain: z.number().optional(),
albumPeak: z.number().optional(),
trackGain: z.number().optional(),
trackPeak: z.number().optional(),
});
const genreItem = z.object({
name: z.string(),
});
const simpleArtist = z.object({
id: z.string(),
name: z.string(),
});
const contributor = z.object({
artist: simpleArtist,
role: z.string(),
subRole: z.string().optional(),
});
const song = z.object({
album: z.string().optional(),
albumArtists: z.array(simpleArtist),
albumId: id.optional(),
artist: z.string().optional(),
artistId: id.optional(),
artists: z.array(simpleArtist),
averageRating: z.number().optional(),
bitRate: z.number().optional(),
bpm: z.number().optional(),
contentType: z.string(),
contributors: z.array(contributor).optional(),
coverArt: z.string().optional(),
created: z.string(),
discNumber: z.number(),
duration: z.number().optional(),
genre: z.string().optional(),
genres: z.array(genreItem).optional(),
id,
isDir: z.boolean(),
isVideo: z.boolean(),
musicBrainzId: z.string().optional(),
parent: z.string(),
path: z.string(),
playCount: z.number().optional(),
replayGain: songGain.optional(),
size: z.number(),
starred: z.boolean().optional(),
suffix: z.string(),
title: z.string(),
track: z.number().optional(),
type: z.string(),
userRating: z.number().optional(),
year: z.number().optional(),
});
const album = z.object({
album: z.string(),
albumArtists: z.array(simpleArtist),
artist: z.string(),
artistId: id,
artists: z.array(simpleArtist),
contributors: z.array(contributor).optional(),
coverArt: z.string(),
created: z.string(),
duration: z.number(),
genre: z.string().optional(),
genres: z.array(genreItem).optional(),
id,
isCompilation: z.boolean().optional(),
isDir: z.boolean(),
isVideo: z.boolean(),
name: z.string(),
parent: z.string(),
song: z.array(song),
songCount: z.number(),
starred: z.boolean().optional(),
title: z.string(),
userRating: z.number().optional(),
year: z.number().optional(),
});
const albumListEntry = album.omit({
song: true,
});
const albumListParameters = z.object({
fromYear: z.number().optional(),
genre: z.string().optional(),
musicFolderId: z.string().optional(),
offset: z.number().optional(),
size: z.number().optional(),
toYear: z.number().optional(),
type: z.string().optional(),
});
const albumList = z.array(album.omit({ song: true }));
const albumArtist = z.object({
album: z.array(album).optional(),
albumCount: z.string(),
artistImageUrl: z.string().optional(),
coverArt: z.string().optional(),
id,
name: z.string(),
roles: z.array(z.string()).optional(),
starred: z.string().optional(),
});
const albumArtistList = z.object({
artist: z.array(albumArtist),
name: z.string(),
});
const artistListEntry = albumArtist.pick({
albumCount: true,
coverArt: true,
id: true,
name: true,
roles: true,
starred: true,
});
const artistInfoParameters = z.object({
count: z.number().optional(),
id: z.string(),
includeNotPresent: z.boolean().optional(),
});
const artistInfo = z.object({
artistInfo: z.object({
biography: z.string().optional(),
largeImageUrl: z.string().optional(),
lastFmUrl: z.string().optional(),
mediumImageUrl: z.string().optional(),
musicBrainzId: z.string().optional(),
similarArtist: z.array(
z.object({
albumCount: z.string(),
artistImageUrl: z.string().optional(),
coverArt: z.string().optional(),
id: z.string(),
name: z.string(),
}),
),
smallImageUrl: z.string().optional(),
}),
});
const topSongsListParameters = z.object({
artist: z.string(), // The name of the artist, not the artist ID
count: z.number().optional(),
});
const topSongsList = z.object({
topSongs: z
.object({
song: z.array(song),
})
.optional(),
});
const scrobbleParameters = z.object({
id: z.string(),
submission: z.boolean().optional(),
time: z.number().optional(), // The time (in milliseconds since 1 Jan 1970) at which the song was listened to.
});
const scrobble = z.null();
const search3 = z.object({
searchResult3: z
.object({
album: z.array(album).optional(),
artist: z.array(albumArtist).optional(),
song: z.array(song).optional(),
})
.optional(),
});
const search3Parameters = z.object({
albumCount: z.number().optional(),
albumOffset: z.number().optional(),
artistCount: z.number().optional(),
artistOffset: z.number().optional(),
musicFolderId: z.string().optional(),
query: z.string().optional(),
songCount: z.number().optional(),
songOffset: z.number().optional(),
});
const randomSongListParameters = z.object({
fromYear: z.number().optional(),
genre: z.string().optional(),
musicFolderId: z.string().optional(),
size: z.number().optional(),
toYear: z.number().optional(),
});
const randomSongList = z.object({
randomSongs: z
.object({
song: z.array(song),
})
.optional(),
});
const ping = z.object({
openSubsonic: z.boolean().optional(),
serverVersion: z.string().optional(),
version: z.string(),
});
const extension = z.object({
name: z.string(),
versions: z.number().array(),
});
const serverInfo = z.object({
openSubsonicExtensions: z.array(extension).optional(),
});
const structuredLyricsParameters = z.object({
id: z.string(),
});
const lyricLine = z.object({
start: z.number().optional(),
value: z.string(),
});
const structuredLyric = z.object({
displayArtist: z.string().optional(),
displayTitle: z.string().optional(),
lang: z.string(),
line: z.array(lyricLine),
offset: z.number().optional(),
synced: z.boolean(),
});
const structuredLyrics = z.object({
lyricsList: z
.object({
structuredLyrics: z.array(structuredLyric).optional(),
})
.optional(),
});
const similarSongsParameters = z.object({
count: z.number().optional(),
id: z.string(),
});
const similarSongs = z.object({
similarSongs: z
.object({
song: z.array(song),
})
.optional(),
});
export enum SubsonicExtensions {
FORM_POST = 'formPost',
SONG_LYRICS = 'songLyrics',
TRANSCODE_OFFSET = 'transcodeOffset',
}
const updatePlaylistParameters = z.object({
comment: z.string().optional(),
name: z.string().optional(),
playlistId: z.string(),
public: z.boolean().optional(),
songIdToAdd: z.array(z.string()).optional(),
songIndexToRemove: z.array(z.string()).optional(),
});
const getStarredParameters = z.object({
musicFolderId: z.string().optional(),
});
const getStarred = z.object({
starred: z
.object({
album: z.array(albumListEntry),
artist: z.array(artistListEntry),
song: z.array(song),
})
.optional(),
});
const getSongsByGenreParameters = z.object({
count: z.number().optional(),
genre: z.string(),
musicFolderId: z.string().optional(),
offset: z.number().optional(),
});
const getSongsByGenre = z.object({
songsByGenre: z
.object({
song: z.array(song),
})
.optional(),
});
const getAlbumParameters = z.object({
id: z.string(),
musicFolderId: z.string().optional(),
});
const getAlbum = z.object({
album,
});
const getArtistParameters = z.object({
id: z.string(),
});
const getArtist = z.object({
artist: albumArtist,
});
const getSongParameters = z.object({
id: z.string(),
});
const getSong = z.object({
song,
});
const getArtistsParameters = z.object({
musicFolderId: z.string().optional(),
});
const getArtists = z.object({
artists: z.object({
ignoredArticles: z.string(),
index: z.array(
z.object({
artist: z.array(artistListEntry),
name: z.string(),
}),
),
}),
});
const deletePlaylistParameters = z.object({
id: z.string(),
});
const createPlaylistParameters = z.object({
name: z.string(),
playlistId: z.string().optional(),
songId: z.array(z.string()).optional(),
});
const playlist = z.object({
changed: z.string().optional(),
comment: z.string().optional(),
coverArt: z.string().optional(),
created: z.string(),
duration: z.number(),
entry: z.array(song).optional(),
id,
name: z.string(),
owner: z.string(),
public: z.boolean(),
songCount: z.number(),
});
const createPlaylist = z.object({
playlist,
});
const getPlaylistsParameters = z.object({
username: z.string().optional(),
});
const playlistListEntry = playlist.omit({
entry: true,
});
const getPlaylists = z.object({
playlists: z
.object({
playlist: z.array(playlistListEntry),
})
.optional(),
});
const getPlaylistParameters = z.object({
id: z.string(),
});
const getPlaylist = z.object({
playlist,
});
const genre = z.object({
albumCount: z.number(),
songCount: z.number(),
value: z.string(),
});
const getGenresParameters = z.object({});
const getGenres = z.object({
genres: z
.object({
genre: z.array(genre),
})
.optional(),
});
export enum AlbumListSortType {
ALPHABETICAL_BY_ARTIST = 'alphabeticalByArtist',
ALPHABETICAL_BY_NAME = 'alphabeticalByName',
BY_GENRE = 'byGenre',
BY_YEAR = 'byYear',
FREQUENT = 'frequent',
NEWEST = 'newest',
RANDOM = 'random',
RECENT = 'recent',
STARRED = 'starred',
}
const getAlbumList2Parameters = z
.object({
fromYear: z.number().optional(),
genre: z.string().optional(),
musicFolderId: z.string().optional(),
offset: z.number().optional(),
size: z.number().optional(),
toYear: z.number().optional(),
type: z.nativeEnum(AlbumListSortType),
})
.refine(
(val) => {
if (val.type === AlbumListSortType.BY_YEAR) {
return val.fromYear !== undefined && val.toYear !== undefined;
}
return true;
},
{
message: 'Parameters "fromYear" and "toYear" are required when using sort "byYear"',
},
)
.refine(
(val) => {
if (val.type === AlbumListSortType.BY_GENRE) {
return val.genre !== undefined;
}
return true;
},
{ message: 'Parameter "genre" is required when using sort "byGenre"' },
);
const getAlbumList2 = z.object({
albumList2: z.object({
album: z.array(albumListEntry),
}),
});
const albumInfoParameters = z.object({
id: z.string(),
});
const albumInfo = z.object({
albumInfo: z.object({
largeImageUrl: z.string().optional(),
lastFmUrl: z.string().optional(),
mediumImageUrl: z.string().optional(),
musicBrainzId: z.string().optional(),
notes: z.string().optional(),
smallImageUrl: z.string().optional(),
}),
});
export const ssType = {
_parameters: {
albumInfo: albumInfoParameters,
albumList: albumListParameters,
artistInfo: artistInfoParameters,
authenticate: authenticateParameters,
createFavorite: createFavoriteParameters,
createPlaylist: createPlaylistParameters,
deletePlaylist: deletePlaylistParameters,
getAlbum: getAlbumParameters,
getAlbumList2: getAlbumList2Parameters,
getArtist: getArtistParameters,
getArtists: getArtistsParameters,
getGenre: getGenresParameters,
getGenres: getGenresParameters,
getPlaylist: getPlaylistParameters,
getPlaylists: getPlaylistsParameters,
getSong: getSongParameters,
getSongsByGenre: getSongsByGenreParameters,
getStarred: getStarredParameters,
randomSongList: randomSongListParameters,
removeFavorite: removeFavoriteParameters,
scrobble: scrobbleParameters,
search3: search3Parameters,
setRating: setRatingParameters,
similarSongs: similarSongsParameters,
structuredLyrics: structuredLyricsParameters,
topSongsList: topSongsListParameters,
updatePlaylist: updatePlaylistParameters,
},
_response: {
album,
albumArtist,
albumArtistList,
albumInfo,
albumList,
albumListEntry,
artistInfo,
artistListEntry,
authenticate,
baseResponse,
createFavorite,
createPlaylist,
genre,
getAlbum,
getAlbumList2,
getArtist,
getArtists,
getGenres,
getPlaylist,
getPlaylists,
getSong,
getSongsByGenre,
getStarred,
musicFolderList,
ping,
playlist,
playlistListEntry,
randomSongList,
removeFavorite,
scrobble,
search3,
serverInfo,
setRating,
similarSongs,
song,
structuredLyrics,
topSongsList,
},
};