diff --git a/src/renderer/api/controller.ts b/src/renderer/api/controller.ts index 923880f5..63799c53 100644 --- a/src/renderer/api/controller.ts +++ b/src/renderer/api/controller.ts @@ -128,7 +128,7 @@ const endpoints: ApiController = { getUserList: undefined, removeFromPlaylist: jfController.removeFromPlaylist, scrobble: jfController.scrobble, - search: undefined, + search: jfController.search, setRating: undefined, updatePlaylist: jfController.updatePlaylist, }, diff --git a/src/renderer/api/jellyfin/jellyfin-api.ts b/src/renderer/api/jellyfin/jellyfin-api.ts index 8e523781..2815b725 100644 --- a/src/renderer/api/jellyfin/jellyfin-api.ts +++ b/src/renderer/api/jellyfin/jellyfin-api.ts @@ -224,6 +224,15 @@ export const contract = c.router({ 400: jfType._response.error, }, }, + search: { + method: 'GET', + path: 'users/:userId/items', + query: jfType._parameters.search, + responses: { + 200: jfType._response.search, + 400: jfType._response.error, + }, + }, updatePlaylist: { body: jfType._parameters.updatePlaylist, method: 'PUT', diff --git a/src/renderer/api/jellyfin/jellyfin-controller.ts b/src/renderer/api/jellyfin/jellyfin-controller.ts index 1962ef26..6dcbe46e 100644 --- a/src/renderer/api/jellyfin/jellyfin-controller.ts +++ b/src/renderer/api/jellyfin/jellyfin-controller.ts @@ -40,11 +40,14 @@ import { RemoveFromPlaylistResponse, PlaylistDetailResponse, PlaylistListResponse, + SearchArgs, + SearchResponse, } from '/@/renderer/api/types'; import { jfApiClient } from '/@/renderer/api/jellyfin/jellyfin-api'; import { jfNormalize } from './jellyfin-normalize'; import { jfType } from '/@/renderer/api/jellyfin/jellyfin-types'; import packageJson from '../../../../package.json'; +import { z } from 'zod'; const formatCommaDelimitedString = (value: string[]) => { return value.join(','); @@ -704,6 +707,97 @@ const scrobble = async (args: ScrobbleArgs): Promise => { return null; }; +const search = async (args: SearchArgs): Promise => { + const { query, apiClientProps } = args; + + if (!apiClientProps.server?.userId) { + throw new Error('No userId found'); + } + + let albums: z.infer['Items'] = []; + let albumArtists: z.infer['Items'] = []; + let songs: z.infer['Items'] = []; + + if (query.albumLimit) { + const res = await jfApiClient(apiClientProps).getAlbumList({ + params: { + userId: apiClientProps.server?.userId, + }, + query: { + EnableTotalRecordCount: true, + ImageTypeLimit: 1, + IncludeItemTypes: 'MusicAlbum', + Limit: query.albumLimit, + Recursive: true, + SearchTerm: query.query, + SortBy: 'SortName', + SortOrder: 'Ascending', + StartIndex: query.albumStartIndex || 0, + }, + }); + + if (res.status !== 200) { + throw new Error('Failed to get album list'); + } + + albums = res.body.Items; + } + + if (query.albumArtistLimit) { + const res = await jfApiClient(apiClientProps).getAlbumArtistList({ + query: { + EnableTotalRecordCount: true, + Fields: 'Genres, DateCreated, ExternalUrls, Overview', + ImageTypeLimit: 1, + IncludeArtists: true, + Limit: query.albumArtistLimit, + Recursive: true, + SearchTerm: query.query, + StartIndex: query.albumArtistStartIndex || 0, + UserId: apiClientProps.server?.userId, + }, + }); + + if (res.status !== 200) { + throw new Error('Failed to get album artist list'); + } + + albumArtists = res.body.Items; + } + + if (query.songLimit) { + const res = await jfApiClient(apiClientProps).getSongList({ + params: { + userId: apiClientProps.server?.userId, + }, + query: { + EnableTotalRecordCount: true, + Fields: 'Genres, DateCreated, MediaSources, ParentId', + IncludeItemTypes: 'Audio', + Limit: query.songLimit, + Recursive: true, + SearchTerm: query.query, + SortBy: 'Album,SortName', + SortOrder: 'Ascending', + StartIndex: query.songStartIndex || 0, + UserId: apiClientProps.server?.userId, + }, + }); + + if (res.status !== 200) { + throw new Error('Failed to get song list'); + } + + songs = res.body.Items; + } + + return { + albumArtists: albumArtists.map((item) => jfNormalize.albumArtist(item, apiClientProps.server)), + albums: albums.map((item) => jfNormalize.album(item, apiClientProps.server)), + songs: songs.map((item) => jfNormalize.song(item, apiClientProps.server, '')), + }; +}; + export const jfController = { addToPlaylist, authenticate, @@ -725,5 +819,6 @@ export const jfController = { getTopSongList, removeFromPlaylist, scrobble, + search, updatePlaylist, }; diff --git a/src/renderer/api/jellyfin/jellyfin-types.ts b/src/renderer/api/jellyfin/jellyfin-types.ts index 374b9eee..0ddc1e05 100644 --- a/src/renderer/api/jellyfin/jellyfin-types.ts +++ b/src/renderer/api/jellyfin/jellyfin-types.ts @@ -49,7 +49,12 @@ const baseParameters = z.object({ ExcludeItemTypes: z.string().optional(), Fields: z.string().optional(), ImageTypeLimit: z.number().optional(), + IncludeArtists: z.boolean().optional(), + IncludeGenres: z.boolean().optional(), IncludeItemTypes: z.string().optional(), + IncludeMedia: z.boolean().optional(), + IncludePeople: z.boolean().optional(), + IncludeStudios: z.boolean().optional(), IsFavorite: z.boolean().optional(), Limit: z.number().optional(), MediaTypes: z.string().optional(), @@ -622,6 +627,10 @@ const favorite = z.object({ const favoriteParameters = z.object({}); +const searchParameters = paginationParameters.merge(baseParameters); + +const search = z.any(); + export const jfType = { _enum: { collection: jfCollection, @@ -643,6 +652,7 @@ export const jfType = { playlistList: playlistListParameters, removeFromPlaylist: removeFromPlaylistParameters, scrobble: scrobbleParameters, + search: searchParameters, similarArtistList: similarArtistListParameters, songList: songListParameters, updatePlaylist: updatePlaylistParameters, @@ -666,6 +676,7 @@ export const jfType = { playlistSongList, removeFromPlaylist, scrobble, + search, song, songList, topSongsList,