mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-15 04:51:06 +02:00
scaffold new OS controller
This commit is contained in:
@@ -0,0 +1,208 @@
|
|||||||
|
import createClient, { Middleware } from 'openapi-fetch';
|
||||||
|
import qs from 'qs';
|
||||||
|
|
||||||
|
import { paths } from './subsonic-schema';
|
||||||
|
|
||||||
|
import i18n from '/@/i18n/i18n';
|
||||||
|
import { normalize } from '/@/shared/api/subsonic/subsonic-normalize';
|
||||||
|
import { ApiController } from '/@/shared/types/adapter/api-controller-types';
|
||||||
|
import { ApiControllerError } from '/@/shared/types/adapter/api-controller-types';
|
||||||
|
import { ServerListItem, ServerType } from '/@/shared/types/domain/server-domain-types';
|
||||||
|
import { LibraryItem } from '/@/shared/types/domain/shared-domain-types';
|
||||||
|
|
||||||
|
export function deserializeCredential(credential: string): Record<string, string> {
|
||||||
|
return JSON.parse(credential);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function serializeCredential(
|
||||||
|
username: string,
|
||||||
|
credential: Record<string, string>,
|
||||||
|
type: string,
|
||||||
|
) {
|
||||||
|
switch (type) {
|
||||||
|
case 'apiKey':
|
||||||
|
return JSON.stringify({ apiKey: credential.apiKey });
|
||||||
|
case 'plaintext':
|
||||||
|
return JSON.stringify({ p: credential.password, u: username });
|
||||||
|
case 'token':
|
||||||
|
return JSON.stringify({ s: credential.s, t: credential.t, u: username });
|
||||||
|
default:
|
||||||
|
throw new Error(`Invalid credential type: ${type}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const middleware: Middleware = {
|
||||||
|
onRequest: async () => {},
|
||||||
|
};
|
||||||
|
|
||||||
|
const client = createClient<paths>({
|
||||||
|
querySerializer: (params) => qs.stringify(params, { arrayFormat: 'repeat' }),
|
||||||
|
});
|
||||||
|
|
||||||
|
client.use(middleware);
|
||||||
|
|
||||||
|
type ErrorResponseArgs = {
|
||||||
|
code?: number;
|
||||||
|
message?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
// type Req<T extends keyof paths> = paths[T]['get']['parameters'];
|
||||||
|
|
||||||
|
// type Res<T extends keyof paths> = T extends keyof paths
|
||||||
|
// ? paths[T]['get'] extends {
|
||||||
|
// responses: {
|
||||||
|
// '200': { content: { 'application/json': { 'subsonic-response'?: infer R } } };
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
// ? NonNullable<R>
|
||||||
|
// : never
|
||||||
|
// : never;
|
||||||
|
|
||||||
|
function errorResponse(args: ErrorResponseArgs): [ApiControllerError, null] {
|
||||||
|
const message = `${i18n.t('error.genericError', { postProcess: 'sentenceCase' }) as string}${
|
||||||
|
args.message ? `: ${args.message}` : ''
|
||||||
|
}`;
|
||||||
|
|
||||||
|
return [{ code: args.code || 500, message }, null];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSubsonicErrorMessage(subsonicErrorCode: number): string {
|
||||||
|
switch (subsonicErrorCode) {
|
||||||
|
case 0:
|
||||||
|
return 'A generic error occurred';
|
||||||
|
case 10:
|
||||||
|
return 'Required parameter is missing';
|
||||||
|
case 20:
|
||||||
|
return 'Incompatible Subsonic REST protocol version. Client must upgrade';
|
||||||
|
case 30:
|
||||||
|
return 'Incompatible Subsonic REST protocol version. Server must upgrade';
|
||||||
|
case 40:
|
||||||
|
return 'Wrong username or password';
|
||||||
|
case 41:
|
||||||
|
return 'Token authentication not supported for LDAP users';
|
||||||
|
case 50:
|
||||||
|
return 'User is not authorized for the given operation';
|
||||||
|
case 60:
|
||||||
|
return 'The trial period for the Subsonic server is over. Please upgrade to Subsonic Premium';
|
||||||
|
case 70:
|
||||||
|
return 'The requested data was not found';
|
||||||
|
default:
|
||||||
|
return 'An unknown error occurred';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function subsonicErrorResponse(
|
||||||
|
subsonicErrorCode: number,
|
||||||
|
customMessage?: string,
|
||||||
|
): [ApiControllerError, null] {
|
||||||
|
const httpStatus = toHttpErrorCode(subsonicErrorCode);
|
||||||
|
const message = customMessage || getSubsonicErrorMessage(subsonicErrorCode);
|
||||||
|
|
||||||
|
return [{ code: httpStatus, message }, null];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps Subsonic error codes to appropriate HTTP status codes
|
||||||
|
* @param subsonicErrorCode - The Subsonic error code
|
||||||
|
* @returns The corresponding HTTP status code
|
||||||
|
*/
|
||||||
|
function toHttpErrorCode(subsonicErrorCode: number): number {
|
||||||
|
switch (subsonicErrorCode) {
|
||||||
|
case 0:
|
||||||
|
return 500; // Generic error - Internal Server Error
|
||||||
|
case 10:
|
||||||
|
return 400; // Required parameter is missing - Bad Request
|
||||||
|
case 20:
|
||||||
|
return 426; // Client must upgrade - Upgrade Required
|
||||||
|
case 30:
|
||||||
|
return 503; // Server must upgrade - Service Unavailable
|
||||||
|
case 40:
|
||||||
|
return 401; // Wrong username or password - Unauthorized
|
||||||
|
case 41:
|
||||||
|
return 403; // Token authentication not supported for LDAP users - Forbidden
|
||||||
|
case 50:
|
||||||
|
return 403; // User is not authorized for the given operation - Forbidden
|
||||||
|
case 60:
|
||||||
|
return 402; // Trial period over - Payment Required
|
||||||
|
case 70:
|
||||||
|
return 404; // The requested data was not found - Not Found
|
||||||
|
default:
|
||||||
|
return 500; // Unknown error - Internal Server Error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const adapter: ApiController = {
|
||||||
|
_utility: {
|
||||||
|
getImageUrl: (
|
||||||
|
args: { id: string; size?: number; type: LibraryItem },
|
||||||
|
server: ServerListItem,
|
||||||
|
) => {
|
||||||
|
return `${server.url}/rest/getCoverArt?id=${args.id}&size=${args.size || 300}`;
|
||||||
|
},
|
||||||
|
getStreamUrl: (
|
||||||
|
args: { bitRate?: number; format?: string; id: string },
|
||||||
|
server: ServerListItem,
|
||||||
|
) => {
|
||||||
|
return `${server.url}/rest/stream?id=${args.id}&format=${args.format || 'mp3'}&maxBitRate=${args.bitRate || 320}`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
album: {
|
||||||
|
getDetail: async (request, server, options) => {
|
||||||
|
const { data, error } = await client.GET('/rest/getAlbum', {
|
||||||
|
baseUrl: server.url,
|
||||||
|
params: { query: { id: request.query.id } },
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return errorResponse({ code: 500, message: error });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data['subsonic-response']) {
|
||||||
|
return errorResponse({ code: 404, message: 'No album found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data['subsonic-response'].status !== 'ok') {
|
||||||
|
const errorCode = data['subsonic-response'].error.code;
|
||||||
|
const errorMessage = data['subsonic-response'].error.message;
|
||||||
|
return subsonicErrorResponse(errorCode, errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [null, normalize.album(data['subsonic-response'].album, server)];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
albumArtist: {
|
||||||
|
// TODO: Implement album artist methods
|
||||||
|
},
|
||||||
|
artist: {
|
||||||
|
// TODO: Implement artist methods
|
||||||
|
},
|
||||||
|
favorite: {
|
||||||
|
// TODO: Implement favorite methods
|
||||||
|
},
|
||||||
|
genre: {
|
||||||
|
// TODO: Implement genre methods
|
||||||
|
},
|
||||||
|
musicFolder: {
|
||||||
|
// TODO: Implement music folder methods
|
||||||
|
},
|
||||||
|
playlist: {
|
||||||
|
// TODO: Implement playlist methods
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
authenticate: async (
|
||||||
|
url: string,
|
||||||
|
body: { legacy?: boolean; password: string; username: string },
|
||||||
|
) => {
|
||||||
|
// TODO: Implement authentication logic
|
||||||
|
throw new Error('Authentication not implemented yet');
|
||||||
|
},
|
||||||
|
getType: () => ServerType.SUBSONIC,
|
||||||
|
},
|
||||||
|
song: {
|
||||||
|
// TODO: Implement song methods
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
// TODO: Implement user methods
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -102,7 +102,7 @@ export type ApiClientProps = {
|
|||||||
|
|
||||||
export type ApiController = {
|
export type ApiController = {
|
||||||
_utility: {
|
_utility: {
|
||||||
getDownloadUrl: ApiControllerFn<DownloadRequest, string>;
|
getDownloadUrl?: ApiControllerFn<DownloadRequest, string>;
|
||||||
getImageUrl: (
|
getImageUrl: (
|
||||||
args: { id: string; size?: number; type: LibraryItem },
|
args: { id: string; size?: number; type: LibraryItem },
|
||||||
server: ServerListItem,
|
server: ServerListItem,
|
||||||
@@ -113,67 +113,67 @@ export type ApiController = {
|
|||||||
) => string;
|
) => string;
|
||||||
};
|
};
|
||||||
album: {
|
album: {
|
||||||
getDetail: ApiControllerFn<AlbumDetailRequest, AlbumDetailResponse>;
|
getDetail?: ApiControllerFn<AlbumDetailRequest, AlbumDetailResponse>;
|
||||||
getInfo?: ApiControllerFn<AlbumDetailRequest, AlbumInfo>;
|
getInfo?: ApiControllerFn<AlbumDetailRequest, AlbumInfo>;
|
||||||
getList: ApiControllerFn<AlbumListRequest, AlbumListResponse>;
|
getList?: ApiControllerFn<AlbumListRequest, AlbumListResponse>;
|
||||||
getListCount: ApiControllerFn<AlbumListRequest, number>;
|
getListCount?: ApiControllerFn<AlbumListRequest, number>;
|
||||||
};
|
};
|
||||||
albumArtist: {
|
albumArtist: {
|
||||||
getDetail: ApiControllerFn<AlbumArtistDetailRequest, AlbumArtistDetailResponse>;
|
getDetail?: ApiControllerFn<AlbumArtistDetailRequest, AlbumArtistDetailResponse>;
|
||||||
getList: ApiControllerFn<AlbumArtistListRequest, AlbumArtistListResponse>;
|
getList?: ApiControllerFn<AlbumArtistListRequest, AlbumArtistListResponse>;
|
||||||
getListCount: ApiControllerFn<AlbumArtistListRequest, number>;
|
getListCount?: ApiControllerFn<AlbumArtistListRequest, number>;
|
||||||
};
|
};
|
||||||
artist: {
|
artist: {
|
||||||
getList: ApiControllerFn<ArtistListRequest, ArtistListResponse>;
|
getList?: ApiControllerFn<ArtistListRequest, ArtistListResponse>;
|
||||||
getListCount: ApiControllerFn<ArtistListRequest, number>;
|
getListCount?: ApiControllerFn<ArtistListRequest, number>;
|
||||||
};
|
};
|
||||||
favorite: {
|
favorite: {
|
||||||
create: ApiControllerFn<FavoriteRequest, FavoriteResponse>;
|
create?: ApiControllerFn<FavoriteRequest, FavoriteResponse>;
|
||||||
delete: ApiControllerFn<FavoriteRequest, FavoriteResponse>;
|
delete?: ApiControllerFn<FavoriteRequest, FavoriteResponse>;
|
||||||
};
|
};
|
||||||
genre: {
|
genre: {
|
||||||
getList: ApiControllerFn<GenreListRequest, GenreListResponse>;
|
getList?: ApiControllerFn<GenreListRequest, GenreListResponse>;
|
||||||
};
|
};
|
||||||
musicFolder: {
|
musicFolder: {
|
||||||
getList: ApiControllerFn<ServerMusicFolderListRequest, ServerMusicFolderListResponse>;
|
getList?: ApiControllerFn<ServerMusicFolderListRequest, ServerMusicFolderListResponse>;
|
||||||
};
|
};
|
||||||
playlist: {
|
playlist: {
|
||||||
addTo: ApiControllerFn<AddToPlaylistArgs, AddToPlaylistResponse>;
|
addTo?: ApiControllerFn<AddToPlaylistArgs, AddToPlaylistResponse>;
|
||||||
create: ApiControllerFn<CreatePlaylistRequest, CreatePlaylistResponse>;
|
create?: ApiControllerFn<CreatePlaylistRequest, CreatePlaylistResponse>;
|
||||||
delete: ApiControllerFn<DeletePlaylistRequest, DeletePlaylistResponse>;
|
delete?: ApiControllerFn<DeletePlaylistRequest, DeletePlaylistResponse>;
|
||||||
getDetail: ApiControllerFn<PlaylistDetailRequest, PlaylistDetailResponse>;
|
getDetail?: ApiControllerFn<PlaylistDetailRequest, PlaylistDetailResponse>;
|
||||||
getList: ApiControllerFn<PlaylistListRequest, PlaylistListResponse>;
|
getList?: ApiControllerFn<PlaylistListRequest, PlaylistListResponse>;
|
||||||
getListCount: ApiControllerFn<PlaylistListRequest, number>;
|
getListCount?: ApiControllerFn<PlaylistListRequest, number>;
|
||||||
getSongList: ApiControllerFn<PlaylistSongListRequest, SongListResponse>;
|
getSongList?: ApiControllerFn<PlaylistSongListRequest, SongListResponse>;
|
||||||
moveItem?: ApiControllerFn<MoveItemRequest, void>;
|
moveItem?: ApiControllerFn<MoveItemRequest, void>;
|
||||||
removeFrom: ApiControllerFn<RemoveFromPlaylistRequest, RemoveFromPlaylistResponse>;
|
removeFrom?: ApiControllerFn<RemoveFromPlaylistRequest, RemoveFromPlaylistResponse>;
|
||||||
update: ApiControllerFn<UpdatePlaylistRequest, UpdatePlaylistResponse>;
|
update?: ApiControllerFn<UpdatePlaylistRequest, UpdatePlaylistResponse>;
|
||||||
};
|
};
|
||||||
server: {
|
server: {
|
||||||
authenticate: (
|
authenticate: (
|
||||||
url: string,
|
url: string,
|
||||||
body: { legacy?: boolean; password: string; username: string },
|
body: { legacy?: boolean; password: string; username: string },
|
||||||
) => Promise<AuthenticationResponse>;
|
) => Promise<AuthenticationResponse>;
|
||||||
getRoles: ApiControllerFn<
|
getRoles?: ApiControllerFn<
|
||||||
BaseEndpointArgs,
|
BaseEndpointArgs,
|
||||||
Array<string | { label: string; value: string }>
|
Array<string | { label: string; value: string }>
|
||||||
>;
|
>;
|
||||||
getServerInfo: ApiControllerFn<ServerInfoRequest, ServerInfo>;
|
getServerInfo?: ApiControllerFn<ServerInfoRequest, ServerInfo>;
|
||||||
getTags: ApiControllerFn<TagRequest, TagsResponse>;
|
getTags?: ApiControllerFn<TagRequest, TagsResponse>;
|
||||||
getTranscodingUrl: ApiControllerFn<TranscodingRequest, string>;
|
getTranscodingUrl?: ApiControllerFn<TranscodingRequest, string>;
|
||||||
getType: () => ServerType;
|
getType?: () => ServerType;
|
||||||
scrobble: ApiControllerFn<ScrobbleRequest, ScrobbleResponse>;
|
scrobble?: ApiControllerFn<ScrobbleRequest, ScrobbleResponse>;
|
||||||
search: ApiControllerFn<SearchRequest, SearchResponse>;
|
search?: ApiControllerFn<SearchRequest, SearchResponse>;
|
||||||
};
|
};
|
||||||
song: {
|
song: {
|
||||||
getDetail: ApiControllerFn<SongDetailRequest, SongDetailResponse>;
|
getDetail?: ApiControllerFn<SongDetailRequest, SongDetailResponse>;
|
||||||
getList: ApiControllerFn<SongListRequest, SongListResponse>;
|
getList?: ApiControllerFn<SongListRequest, SongListResponse>;
|
||||||
getListCount: ApiControllerFn<SongListRequest, number>;
|
getListCount?: ApiControllerFn<SongListRequest, number>;
|
||||||
getLyrics?: ApiControllerFn<LyricsRequest, LyricsResponse>;
|
getLyrics?: ApiControllerFn<LyricsRequest, LyricsResponse>;
|
||||||
getRandomList: ApiControllerFn<RandomSongListRequest, SongListResponse>;
|
getRandomList?: ApiControllerFn<RandomSongListRequest, SongListResponse>;
|
||||||
getSimilar: ApiControllerFn<SimilarSongsRequest, Song[]>;
|
getSimilar?: ApiControllerFn<SimilarSongsRequest, Song[]>;
|
||||||
getStructuredLyrics?: ApiControllerFn<StructuredLyricsRequest, StructuredLyric[]>;
|
getStructuredLyrics?: ApiControllerFn<StructuredLyricsRequest, StructuredLyric[]>;
|
||||||
getTopList: ApiControllerFn<TopSongListRequest, TopSongListResponse>;
|
getTopList?: ApiControllerFn<TopSongListRequest, TopSongListResponse>;
|
||||||
};
|
};
|
||||||
user: {
|
user: {
|
||||||
getList?: ApiControllerFn<UserListRequest, UserListResponse>;
|
getList?: ApiControllerFn<UserListRequest, UserListResponse>;
|
||||||
|
|||||||
Reference in New Issue
Block a user