mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-07 04:20:12 +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 = {
|
||||
_utility: {
|
||||
getDownloadUrl: ApiControllerFn<DownloadRequest, string>;
|
||||
getDownloadUrl?: ApiControllerFn<DownloadRequest, string>;
|
||||
getImageUrl: (
|
||||
args: { id: string; size?: number; type: LibraryItem },
|
||||
server: ServerListItem,
|
||||
@@ -113,67 +113,67 @@ export type ApiController = {
|
||||
) => string;
|
||||
};
|
||||
album: {
|
||||
getDetail: ApiControllerFn<AlbumDetailRequest, AlbumDetailResponse>;
|
||||
getDetail?: ApiControllerFn<AlbumDetailRequest, AlbumDetailResponse>;
|
||||
getInfo?: ApiControllerFn<AlbumDetailRequest, AlbumInfo>;
|
||||
getList: ApiControllerFn<AlbumListRequest, AlbumListResponse>;
|
||||
getListCount: ApiControllerFn<AlbumListRequest, number>;
|
||||
getList?: ApiControllerFn<AlbumListRequest, AlbumListResponse>;
|
||||
getListCount?: ApiControllerFn<AlbumListRequest, number>;
|
||||
};
|
||||
albumArtist: {
|
||||
getDetail: ApiControllerFn<AlbumArtistDetailRequest, AlbumArtistDetailResponse>;
|
||||
getList: ApiControllerFn<AlbumArtistListRequest, AlbumArtistListResponse>;
|
||||
getListCount: ApiControllerFn<AlbumArtistListRequest, number>;
|
||||
getDetail?: ApiControllerFn<AlbumArtistDetailRequest, AlbumArtistDetailResponse>;
|
||||
getList?: ApiControllerFn<AlbumArtistListRequest, AlbumArtistListResponse>;
|
||||
getListCount?: ApiControllerFn<AlbumArtistListRequest, number>;
|
||||
};
|
||||
artist: {
|
||||
getList: ApiControllerFn<ArtistListRequest, ArtistListResponse>;
|
||||
getListCount: ApiControllerFn<ArtistListRequest, number>;
|
||||
getList?: ApiControllerFn<ArtistListRequest, ArtistListResponse>;
|
||||
getListCount?: ApiControllerFn<ArtistListRequest, number>;
|
||||
};
|
||||
favorite: {
|
||||
create: ApiControllerFn<FavoriteRequest, FavoriteResponse>;
|
||||
delete: ApiControllerFn<FavoriteRequest, FavoriteResponse>;
|
||||
create?: ApiControllerFn<FavoriteRequest, FavoriteResponse>;
|
||||
delete?: ApiControllerFn<FavoriteRequest, FavoriteResponse>;
|
||||
};
|
||||
genre: {
|
||||
getList: ApiControllerFn<GenreListRequest, GenreListResponse>;
|
||||
getList?: ApiControllerFn<GenreListRequest, GenreListResponse>;
|
||||
};
|
||||
musicFolder: {
|
||||
getList: ApiControllerFn<ServerMusicFolderListRequest, ServerMusicFolderListResponse>;
|
||||
getList?: ApiControllerFn<ServerMusicFolderListRequest, ServerMusicFolderListResponse>;
|
||||
};
|
||||
playlist: {
|
||||
addTo: ApiControllerFn<AddToPlaylistArgs, AddToPlaylistResponse>;
|
||||
create: ApiControllerFn<CreatePlaylistRequest, CreatePlaylistResponse>;
|
||||
delete: ApiControllerFn<DeletePlaylistRequest, DeletePlaylistResponse>;
|
||||
getDetail: ApiControllerFn<PlaylistDetailRequest, PlaylistDetailResponse>;
|
||||
getList: ApiControllerFn<PlaylistListRequest, PlaylistListResponse>;
|
||||
getListCount: ApiControllerFn<PlaylistListRequest, number>;
|
||||
getSongList: ApiControllerFn<PlaylistSongListRequest, SongListResponse>;
|
||||
addTo?: ApiControllerFn<AddToPlaylistArgs, AddToPlaylistResponse>;
|
||||
create?: ApiControllerFn<CreatePlaylistRequest, CreatePlaylistResponse>;
|
||||
delete?: ApiControllerFn<DeletePlaylistRequest, DeletePlaylistResponse>;
|
||||
getDetail?: ApiControllerFn<PlaylistDetailRequest, PlaylistDetailResponse>;
|
||||
getList?: ApiControllerFn<PlaylistListRequest, PlaylistListResponse>;
|
||||
getListCount?: ApiControllerFn<PlaylistListRequest, number>;
|
||||
getSongList?: ApiControllerFn<PlaylistSongListRequest, SongListResponse>;
|
||||
moveItem?: ApiControllerFn<MoveItemRequest, void>;
|
||||
removeFrom: ApiControllerFn<RemoveFromPlaylistRequest, RemoveFromPlaylistResponse>;
|
||||
update: ApiControllerFn<UpdatePlaylistRequest, UpdatePlaylistResponse>;
|
||||
removeFrom?: ApiControllerFn<RemoveFromPlaylistRequest, RemoveFromPlaylistResponse>;
|
||||
update?: ApiControllerFn<UpdatePlaylistRequest, UpdatePlaylistResponse>;
|
||||
};
|
||||
server: {
|
||||
authenticate: (
|
||||
url: string,
|
||||
body: { legacy?: boolean; password: string; username: string },
|
||||
) => Promise<AuthenticationResponse>;
|
||||
getRoles: ApiControllerFn<
|
||||
getRoles?: ApiControllerFn<
|
||||
BaseEndpointArgs,
|
||||
Array<string | { label: string; value: string }>
|
||||
>;
|
||||
getServerInfo: ApiControllerFn<ServerInfoRequest, ServerInfo>;
|
||||
getTags: ApiControllerFn<TagRequest, TagsResponse>;
|
||||
getTranscodingUrl: ApiControllerFn<TranscodingRequest, string>;
|
||||
getType: () => ServerType;
|
||||
scrobble: ApiControllerFn<ScrobbleRequest, ScrobbleResponse>;
|
||||
search: ApiControllerFn<SearchRequest, SearchResponse>;
|
||||
getServerInfo?: ApiControllerFn<ServerInfoRequest, ServerInfo>;
|
||||
getTags?: ApiControllerFn<TagRequest, TagsResponse>;
|
||||
getTranscodingUrl?: ApiControllerFn<TranscodingRequest, string>;
|
||||
getType?: () => ServerType;
|
||||
scrobble?: ApiControllerFn<ScrobbleRequest, ScrobbleResponse>;
|
||||
search?: ApiControllerFn<SearchRequest, SearchResponse>;
|
||||
};
|
||||
song: {
|
||||
getDetail: ApiControllerFn<SongDetailRequest, SongDetailResponse>;
|
||||
getList: ApiControllerFn<SongListRequest, SongListResponse>;
|
||||
getListCount: ApiControllerFn<SongListRequest, number>;
|
||||
getDetail?: ApiControllerFn<SongDetailRequest, SongDetailResponse>;
|
||||
getList?: ApiControllerFn<SongListRequest, SongListResponse>;
|
||||
getListCount?: ApiControllerFn<SongListRequest, number>;
|
||||
getLyrics?: ApiControllerFn<LyricsRequest, LyricsResponse>;
|
||||
getRandomList: ApiControllerFn<RandomSongListRequest, SongListResponse>;
|
||||
getSimilar: ApiControllerFn<SimilarSongsRequest, Song[]>;
|
||||
getRandomList?: ApiControllerFn<RandomSongListRequest, SongListResponse>;
|
||||
getSimilar?: ApiControllerFn<SimilarSongsRequest, Song[]>;
|
||||
getStructuredLyrics?: ApiControllerFn<StructuredLyricsRequest, StructuredLyric[]>;
|
||||
getTopList: ApiControllerFn<TopSongListRequest, TopSongListResponse>;
|
||||
getTopList?: ApiControllerFn<TopSongListRequest, TopSongListResponse>;
|
||||
};
|
||||
user: {
|
||||
getList?: ApiControllerFn<UserListRequest, UserListResponse>;
|
||||
|
||||
Reference in New Issue
Block a user