2023-05-08 03:34:15 -07:00
import {
2023-07-01 19:10:05 -07:00
albumArtistListSortMap ,
sortOrderMap ,
albumListSortMap ,
songListSortMap ,
playlistListSortMap ,
2023-07-31 17:16:48 -07:00
genreListSortMap ,
2024-02-19 08:53:50 -08:00
Song ,
2024-09-01 12:25:50 -07:00
Played ,
2024-09-26 04:23:08 +00:00
ControllerEndpoint ,
2025-05-18 09:23:52 -07:00
LibraryItem ,
2023-05-08 03:34:15 -07:00
} 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' ;
2023-05-19 02:06:58 -07:00
import { z } from 'zod' ;
2023-05-21 07:30:28 -07:00
import { JFSongListSort , JFSortOrder } from '/@/renderer/api/jellyfin.types' ;
2024-04-22 19:44:10 -07:00
import { ServerFeature } from '/@/renderer/api/features-types' ;
2025-05-18 09:23:52 -07:00
import { VersionInfo , getFeatures , hasFeature } from '/@/renderer/api/utils' ;
2024-05-02 18:42:49 -07:00
import chunk from 'lodash/chunk' ;
2023-05-08 03:34:15 -07:00
const formatCommaDelimitedString = ( value : string [ ] ) = > {
2023-07-01 19:10:05 -07:00
return value . join ( ',' ) ;
2023-05-08 03:34:15 -07:00
} ;
2024-09-26 04:23:08 +00:00
// Limit the query to 50 at a time to be *extremely* conservative on the
// length of the full URL, since the ids are part of the query string and
// not the POST body
const MAX_ITEMS_PER_PLAYLIST_ADD = 50 ;
2023-05-08 03:34:15 -07:00
2024-09-26 04:23:08 +00:00
const VERSION_INFO : VersionInfo = [
[
'10.9.0' ,
{
[ ServerFeature . LYRICS_SINGLE_STRUCTURED ] : [ 1 ] ,
[ ServerFeature . PUBLIC_PLAYLIST ] : [ 1 ] ,
2023-07-01 19:10:05 -07:00
} ,
2024-09-26 04:23:08 +00:00
] ,
2025-05-18 09:23:52 -07:00
[ '10.0.0' , { [ ServerFeature . TAGS ] : [ 1 ] } ] ,
2024-09-26 04:23:08 +00:00
] ;
2023-07-01 19:10:05 -07:00
2024-09-26 04:23:08 +00:00
export const JellyfinController : ControllerEndpoint = {
addToPlaylist : async ( args ) = > {
const { query , body , apiClientProps } = args ;
2023-05-08 03:34:15 -07:00
2024-09-26 04:23:08 +00:00
if ( ! apiClientProps . server ? . userId ) {
throw new Error ( 'No userId found' ) ;
}
2023-05-08 03:34:15 -07:00
2024-09-26 04:23:08 +00:00
const chunks = chunk ( body . songId , MAX_ITEMS_PER_PLAYLIST_ADD ) ;
for ( const chunk of chunks ) {
const res = await jfApiClient ( apiClientProps ) . addToPlaylist ( {
body : null ,
params : {
id : query.id ,
} ,
query : {
Ids : chunk.join ( ',' ) ,
UserId : apiClientProps.server?.userId ,
} ,
} ) ;
if ( res . status !== 204 ) {
throw new Error ( 'Failed to add to playlist' ) ;
}
}
2023-05-08 03:34:15 -07:00
2024-09-26 04:23:08 +00:00
return null ;
} ,
authenticate : async ( url , body ) = > {
const cleanServerUrl = url . replace ( /\/$/ , '' ) ;
2023-05-08 03:34:15 -07:00
2024-09-26 04:23:08 +00:00
const res = await jfApiClient ( { server : null , url : cleanServerUrl } ) . authenticate ( {
body : {
Pw : body.password ,
Username : body.username ,
} ,
} ) ;
2023-05-08 03:34:15 -07:00
2024-09-26 04:23:08 +00:00
if ( res . status !== 200 ) {
throw new Error ( 'Failed to authenticate' ) ;
}
2023-05-08 03:34:15 -07:00
2024-09-26 04:23:08 +00:00
return {
credential : res.body.AccessToken ,
userId : res.body.User.Id ,
username : res.body.User.Name ,
} ;
} ,
createFavorite : async ( args ) = > {
const { query , apiClientProps } = args ;
2023-05-08 03:34:15 -07:00
2024-09-26 04:23:08 +00:00
if ( ! apiClientProps . server ? . userId ) {
throw new Error ( 'No userId found' ) ;
}
2023-05-08 03:34:15 -07:00
2024-09-26 04:23:08 +00:00
for ( const id of query . id ) {
await jfApiClient ( apiClientProps ) . createFavorite ( {
body : { } ,
params : {
id ,
userId : apiClientProps.server?.userId ,
} ,
} ) ;
2023-07-01 19:10:05 -07:00
}
2023-05-08 03:34:15 -07:00
2024-09-26 04:23:08 +00:00
return null ;
} ,
createPlaylist : async ( args ) = > {
const { body , apiClientProps } = args ;
2023-05-08 03:34:15 -07:00
2024-09-26 04:23:08 +00:00
if ( ! apiClientProps . server ? . userId ) {
throw new Error ( 'No userId found' ) ;
}
2023-05-08 03:34:15 -07:00
2024-09-26 04:23:08 +00:00
const res = await jfApiClient ( apiClientProps ) . createPlaylist ( {
body : {
IsPublic : body.public ,
MediaType : 'Audio' ,
Name : body.name ,
UserId : apiClientProps.server.userId ,
} ,
} ) ;
2023-05-08 03:34:15 -07:00
2024-09-26 04:23:08 +00:00
if ( res . status !== 200 ) {
throw new Error ( 'Failed to create playlist' ) ;
}
2023-05-08 03:34:15 -07:00
2024-09-26 04:23:08 +00:00
return {
id : res.body.Id ,
} ;
} ,
deleteFavorite : async ( args ) = > {
const { query , apiClientProps } = args ;
2023-05-08 03:34:15 -07:00
2024-09-26 04:23:08 +00:00
if ( ! apiClientProps . server ? . userId ) {
throw new Error ( 'No userId found' ) ;
}
2023-05-08 03:34:15 -07:00
2024-09-26 04:23:08 +00:00
for ( const id of query . id ) {
await jfApiClient ( apiClientProps ) . removeFavorite ( {
body : { } ,
params : {
id ,
userId : apiClientProps.server?.userId ,
} ,
} ) ;
}
2023-05-08 03:34:15 -07:00
2024-09-26 04:23:08 +00:00
return null ;
} ,
deletePlaylist : async ( args ) = > {
const { query , apiClientProps } = args ;
2023-05-08 03:34:15 -07:00
2024-09-26 04:23:08 +00:00
const res = await jfApiClient ( apiClientProps ) . deletePlaylist ( {
params : {
id : query.id ,
} ,
} ) ;
2023-05-08 03:34:15 -07:00
2024-09-26 04:23:08 +00:00
if ( res . status !== 204 ) {
throw new Error ( 'Failed to delete playlist' ) ;
2023-07-01 19:10:05 -07:00
}
2024-09-26 04:23:08 +00:00
return null ;
} ,
getAlbumArtistDetail : async ( args ) = > {
const { query , apiClientProps } = args ;
2023-07-01 19:10:05 -07:00
2024-09-26 04:23:08 +00:00
if ( ! apiClientProps . server ? . userId ) {
throw new Error ( 'No userId found' ) ;
}
2023-05-08 03:34:15 -07:00
2024-09-26 04:23:08 +00:00
const res = await jfApiClient ( apiClientProps ) . getAlbumArtistDetail ( {
params : {
id : query.id ,
userId : apiClientProps.server?.userId ,
} ,
query : {
Fields : 'Genres, Overview' ,
} ,
} ) ;
2024-04-13 16:28:36 -07:00
2024-09-26 04:23:08 +00:00
const similarArtistsRes = await jfApiClient ( apiClientProps ) . getSimilarArtistList ( {
params : {
id : query.id ,
} ,
query : {
Limit : 10 ,
} ,
} ) ;
2024-04-13 16:28:36 -07:00
2024-09-26 04:23:08 +00:00
if ( res . status !== 200 || similarArtistsRes . status !== 200 ) {
throw new Error ( 'Failed to get album artist detail' ) ;
2024-04-13 16:28:36 -07:00
}
2023-05-08 03:34:15 -07:00
2024-09-26 04:23:08 +00:00
return jfNormalize . albumArtist (
{ . . . res . body , similarArtists : similarArtistsRes.body } ,
apiClientProps . server ,
) ;
} ,
getAlbumArtistList : async ( args ) = > {
const { query , apiClientProps } = args ;
2024-05-02 18:42:49 -07:00
2024-09-26 04:23:08 +00:00
const res = await jfApiClient ( apiClientProps ) . getAlbumArtistList ( {
query : {
Fields : 'Genres, DateCreated, ExternalUrls, Overview' ,
ImageTypeLimit : 1 ,
Limit : query.limit ,
ParentId : query.musicFolderId ,
Recursive : true ,
SearchTerm : query.searchTerm ,
SortBy : albumArtistListSortMap.jellyfin [ query . sortBy ] || 'SortName,Name' ,
SortOrder : sortOrderMap.jellyfin [ query . sortOrder ] ,
StartIndex : query.startIndex ,
UserId : apiClientProps.server?.userId || undefined ,
} ,
} ) ;
2023-05-08 03:34:15 -07:00
2024-09-26 04:23:08 +00:00
if ( res . status !== 200 ) {
throw new Error ( 'Failed to get album artist list' ) ;
}
2023-05-08 03:34:15 -07:00
2024-09-26 04:23:08 +00:00
return {
items : res.body.Items.map ( ( item ) = >
jfNormalize . albumArtist ( item , apiClientProps . server ) ,
) ,
startIndex : query.startIndex ,
totalRecordCount : res.body.TotalRecordCount ,
} ;
} ,
getAlbumArtistListCount : async ( { apiClientProps , query } ) = >
JellyfinController . getAlbumArtistList ( {
apiClientProps ,
query : { . . . query , limit : 1 , startIndex : 0 } ,
} ) . then ( ( result ) = > result ! . totalRecordCount ! ) ,
getAlbumDetail : async ( args ) = > {
const { query , apiClientProps } = args ;
if ( ! apiClientProps . server ? . userId ) {
throw new Error ( 'No userId found' ) ;
}
2024-05-02 18:42:49 -07:00
2024-09-26 04:23:08 +00:00
const res = await jfApiClient ( apiClientProps ) . getAlbumDetail ( {
2024-05-02 18:42:49 -07:00
params : {
id : query.id ,
2024-09-26 04:23:08 +00:00
userId : apiClientProps.server.userId ,
2024-05-02 18:42:49 -07:00
} ,
query : {
2025-05-17 21:35:58 -07:00
Fields : 'Genres, DateCreated, ChildCount, People, Tags' ,
2024-05-02 18:42:49 -07:00
} ,
} ) ;
2024-09-26 04:23:08 +00:00
const songsRes = await jfApiClient ( apiClientProps ) . getSongList ( {
params : {
userId : apiClientProps.server.userId ,
} ,
query : {
2025-05-17 21:35:58 -07:00
Fields : 'Genres, DateCreated, MediaSources, ParentId, People, Tags' ,
2024-09-26 04:23:08 +00:00
IncludeItemTypes : 'Audio' ,
ParentId : query.id ,
SortBy : 'ParentIndexNumber,IndexNumber,SortName' ,
} ,
} ) ;
if ( res . status !== 200 || songsRes . status !== 200 ) {
throw new Error ( 'Failed to get album detail' ) ;
2024-05-02 18:42:49 -07:00
}
2023-05-08 03:34:15 -07:00
2024-09-26 04:23:08 +00:00
return jfNormalize . album (
{ . . . res . body , Songs : songsRes.body.Items } ,
apiClientProps . server ,
) ;
} ,
getAlbumList : async ( args ) = > {
const { query , apiClientProps } = args ;
2023-05-08 03:34:15 -07:00
2024-09-26 04:23:08 +00:00
if ( ! apiClientProps . server ? . userId ) {
throw new Error ( 'No userId found' ) ;
}
2023-07-01 19:10:05 -07:00
2024-09-26 04:23:08 +00:00
const yearsGroup = [ ] ;
if ( query . minYear && query . maxYear ) {
for ( let i = Number ( query . minYear ) ; i <= Number ( query . maxYear ) ; i += 1 ) {
yearsGroup . push ( String ( i ) ) ;
}
}
2024-05-02 18:42:49 -07:00
2024-09-26 04:23:08 +00:00
const yearsFilter = yearsGroup . length ? yearsGroup . join ( ',' ) : undefined ;
const res = await jfApiClient ( apiClientProps ) . getAlbumList ( {
2024-05-02 18:42:49 -07:00
params : {
2024-09-26 04:23:08 +00:00
userId : apiClientProps.server?.userId ,
2024-05-02 18:42:49 -07:00
} ,
query : {
2024-10-31 21:33:10 +03:00
. . . ( ! query . compilation &&
query . artistIds && {
AlbumArtistIds : formatCommaDelimitedString ( query . artistIds ) ,
} ) ,
. . . ( query . compilation &&
query . artistIds && {
ContributingArtistIds : query.artistIds [ 0 ] ,
} ) ,
2025-05-17 21:35:58 -07:00
Fields : 'People, Tags' ,
2024-09-26 04:23:08 +00:00
GenreIds : query.genres ? query . genres . join ( ',' ) : undefined ,
IncludeItemTypes : 'MusicAlbum' ,
IsFavorite : query.favorite ,
Limit : query.limit ,
ParentId : query.musicFolderId ,
Recursive : true ,
SearchTerm : query.searchTerm ,
SortBy : albumListSortMap.jellyfin [ query . sortBy ] || 'SortName' ,
SortOrder : sortOrderMap.jellyfin [ query . sortOrder ] ,
StartIndex : query.startIndex ,
. . . query . _custom ? . jellyfin ,
Years : yearsFilter ,
2024-05-02 18:42:49 -07:00
} ,
} ) ;
2024-09-26 04:23:08 +00:00
if ( res . status !== 200 ) {
throw new Error ( 'Failed to get album list' ) ;
2024-05-02 18:42:49 -07:00
}
2023-05-08 03:34:15 -07:00
2024-09-26 04:23:08 +00:00
return {
items : res.body.Items.map ( ( item ) = > jfNormalize . album ( item , apiClientProps . server ) ) ,
startIndex : query.startIndex ,
totalRecordCount : res.body.TotalRecordCount ,
} ;
} ,
getAlbumListCount : async ( { apiClientProps , query } ) = >
JellyfinController . getAlbumList ( {
apiClientProps ,
query : { . . . query , limit : 1 , startIndex : 0 } ,
} ) . then ( ( result ) = > result ! . totalRecordCount ! ) ,
2025-04-23 23:27:06 -07:00
getArtistList : async ( args ) = > {
const { query , apiClientProps } = args ;
const res = await jfApiClient ( apiClientProps ) . getArtistList ( {
query : {
Fields : 'Genres, DateCreated, ExternalUrls, Overview' ,
ImageTypeLimit : 1 ,
Limit : query.limit ,
ParentId : query.musicFolderId ,
Recursive : true ,
SearchTerm : query.searchTerm ,
SortBy : albumArtistListSortMap.jellyfin [ query . sortBy ] || 'SortName,Name' ,
SortOrder : sortOrderMap.jellyfin [ query . sortOrder ] ,
StartIndex : query.startIndex ,
UserId : apiClientProps.server?.userId || undefined ,
} ,
} ) ;
if ( res . status !== 200 ) {
throw new Error ( 'Failed to get album artist list' ) ;
}
return {
items : res.body.Items.map ( ( item ) = >
jfNormalize . albumArtist ( item , apiClientProps . server ) ,
) ,
startIndex : query.startIndex ,
totalRecordCount : res.body.TotalRecordCount ,
} ;
} ,
getArtistListCount : async ( { apiClientProps , query } ) = >
JellyfinController . getArtistList ( {
apiClientProps ,
query : { . . . query , limit : 1 , startIndex : 0 } ,
} ) . then ( ( result ) = > result ! . totalRecordCount ! ) ,
2024-09-26 04:23:08 +00:00
getDownloadUrl : ( args ) = > {
const { apiClientProps , query } = args ;
return ` ${ apiClientProps . server ? . url } /items/ ${ query . id } /download?api_key= ${ apiClientProps . server ? . credential } ` ;
} ,
getGenreList : async ( args ) = > {
const { apiClientProps , query } = args ;
2023-05-08 03:34:15 -07:00
2024-09-26 04:23:08 +00:00
if ( ! apiClientProps . server ? . userId ) {
throw new Error ( 'No userId found' ) ;
}
2023-05-08 03:34:15 -07:00
2024-09-26 04:23:08 +00:00
const res = await jfApiClient ( apiClientProps ) . getGenreList ( {
query : {
Fields : 'ItemCounts' ,
ParentId : query?.musicFolderId ,
Recursive : true ,
SearchTerm : query?.searchTerm ,
SortBy : genreListSortMap.jellyfin [ query . sortBy ] || 'SortName' ,
SortOrder : sortOrderMap.jellyfin [ query . sortOrder ] ,
StartIndex : query.startIndex ,
UserId : apiClientProps.server?.userId ,
} ,
} ) ;
2023-05-08 03:34:15 -07:00
2024-09-26 04:23:08 +00:00
if ( res . status !== 200 ) {
throw new Error ( 'Failed to get genre list' ) ;
}
2023-05-08 03:34:15 -07:00
2024-09-26 04:23:08 +00:00
return {
items : res.body.Items.map ( ( item ) = > jfNormalize . genre ( item , apiClientProps . server ) ) ,
startIndex : query.startIndex || 0 ,
totalRecordCount : res.body?.TotalRecordCount || 0 ,
} ;
} ,
getLyrics : async ( args ) = > {
const { query , apiClientProps } = args ;
2023-05-08 03:34:15 -07:00
2024-09-26 04:23:08 +00:00
if ( ! apiClientProps . server ? . userId ) {
throw new Error ( 'No userId found' ) ;
}
2023-05-08 03:34:15 -07:00
2024-09-26 04:23:08 +00:00
const res = await jfApiClient ( apiClientProps ) . getSongLyrics ( {
params : {
id : query.songId ,
} ,
} ) ;
2023-05-08 03:34:15 -07:00
2024-09-26 04:23:08 +00:00
if ( res . status !== 200 ) {
throw new Error ( 'Failed to get lyrics' ) ;
}
2023-05-08 03:34:15 -07:00
2024-09-26 04:23:08 +00:00
if ( res . body . Lyrics . length > 0 && res . body . Lyrics [ 0 ] . Start === undefined ) {
return res . body . Lyrics . map ( ( lyric ) = > lyric . Text ) . join ( '\n' ) ;
}
2023-05-08 03:34:15 -07:00
2024-09-26 04:23:08 +00:00
return res . body . Lyrics . map ( ( lyric ) = > [ lyric . Start ! / 1 e 4 , l y r i c . T e x t ] ) ;
} ,
getMusicFolderList : async ( args ) = > {
const { apiClientProps } = args ;
const userId = apiClientProps . server ? . userId ;
2023-05-08 03:34:15 -07:00
2024-09-26 04:23:08 +00:00
if ( ! userId ) throw new Error ( 'No userId found' ) ;
2023-05-08 03:34:15 -07:00
2024-09-26 04:23:08 +00:00
const res = await jfApiClient ( apiClientProps ) . getMusicFolderList ( {
params : {
userId ,
} ,
} ) ;
2023-05-08 03:34:15 -07:00
2024-09-26 04:23:08 +00:00
if ( res . status !== 200 ) {
throw new Error ( 'Failed to get genre list' ) ;
}
2023-05-08 03:34:15 -07:00
2024-09-26 04:23:08 +00:00
const musicFolders = res . body . Items . filter (
( folder ) = > folder . CollectionType === jfType . _enum . collection . MUSIC ,
) ;
2023-05-08 03:34:15 -07:00
2024-09-26 04:23:08 +00:00
return {
items : musicFolders.map ( jfNormalize . musicFolder ) ,
startIndex : 0 ,
totalRecordCount : musicFolders?.length || 0 ,
} ;
} ,
getPlaylistDetail : async ( args ) = > {
const { query , apiClientProps } = args ;
2023-05-08 03:34:15 -07:00
2024-09-26 04:23:08 +00:00
if ( ! apiClientProps . server ? . userId ) {
throw new Error ( 'No userId found' ) ;
}
2023-05-08 03:34:15 -07:00
2024-09-26 04:23:08 +00:00
const res = await jfApiClient ( apiClientProps ) . getPlaylistDetail ( {
params : {
id : query.id ,
userId : apiClientProps.server?.userId ,
} ,
query : {
Fields : 'Genres, DateCreated, MediaSources, ChildCount, ParentId' ,
Ids : query.id ,
} ,
} ) ;
2023-05-08 03:34:15 -07:00
2024-09-26 04:23:08 +00:00
if ( res . status !== 200 ) {
throw new Error ( 'Failed to get playlist detail' ) ;
}
2023-05-08 03:34:15 -07:00
2024-09-26 04:23:08 +00:00
return jfNormalize . playlist ( res . body , apiClientProps . server ) ;
} ,
getPlaylistList : async ( args ) = > {
const { query , apiClientProps } = args ;
2023-05-08 03:34:15 -07:00
2024-09-26 04:23:08 +00:00
if ( ! apiClientProps . server ? . userId ) {
throw new Error ( 'No userId found' ) ;
}
2023-05-08 03:34:15 -07:00
2024-09-26 04:23:08 +00:00
const res = await jfApiClient ( apiClientProps ) . getPlaylistList ( {
params : {
userId : apiClientProps.server?.userId ,
} ,
query : {
Fields : 'ChildCount, Genres, DateCreated, ParentId, Overview' ,
IncludeItemTypes : 'Playlist' ,
Limit : query.limit ,
Recursive : true ,
SearchTerm : query.searchTerm ,
SortBy : playlistListSortMap.jellyfin [ query . sortBy ] ,
SortOrder : sortOrderMap.jellyfin [ query . sortOrder ] ,
StartIndex : query.startIndex ,
} ,
} ) ;
2023-05-08 03:34:15 -07:00
2024-09-26 04:23:08 +00:00
if ( res . status !== 200 ) {
throw new Error ( 'Failed to get playlist list' ) ;
}
2023-05-08 03:34:15 -07:00
2024-09-26 04:23:08 +00:00
return {
items : res.body.Items.map ( ( item ) = > jfNormalize . playlist ( item , apiClientProps . server ) ) ,
startIndex : 0 ,
totalRecordCount : res.body.TotalRecordCount ,
} ;
} ,
getPlaylistListCount : async ( { apiClientProps , query } ) = >
JellyfinController . getPlaylistList ( {
apiClientProps ,
query : { . . . query , limit : 1 , startIndex : 0 } ,
} ) . then ( ( result ) = > result ! . totalRecordCount ! ) ,
getPlaylistSongList : async ( args ) = > {
const { query , apiClientProps } = args ;
if ( ! apiClientProps . server ? . userId ) {
throw new Error ( 'No userId found' ) ;
}
2023-05-08 03:34:15 -07:00
2024-09-26 04:23:08 +00:00
const res = await jfApiClient ( apiClientProps ) . getPlaylistSongList ( {
params : {
id : query.id ,
} ,
query : {
2025-05-17 21:35:58 -07:00
Fields : 'Genres, DateCreated, MediaSources, UserData, ParentId, People, Tags' ,
2024-09-26 04:23:08 +00:00
IncludeItemTypes : 'Audio' ,
Limit : query.limit ,
SortBy : query.sortBy ? songListSortMap . jellyfin [ query . sortBy ] : undefined ,
SortOrder : query.sortOrder ? sortOrderMap . jellyfin [ query . sortOrder ] : undefined ,
StartIndex : query.startIndex ,
UserId : apiClientProps.server?.userId ,
} ,
} ) ;
2023-05-08 03:34:15 -07:00
2024-09-26 04:23:08 +00:00
if ( res . status !== 200 ) {
throw new Error ( 'Failed to get playlist song list' ) ;
}
2023-05-08 03:34:15 -07:00
2024-09-26 04:23:08 +00:00
return {
items : res.body.Items.map ( ( item ) = > jfNormalize . song ( item , apiClientProps . server , '' ) ) ,
startIndex : query.startIndex ,
totalRecordCount : res.body.TotalRecordCount ,
} ;
} ,
getRandomSongList : async ( args ) = > {
const { query , apiClientProps } = args ;
2023-05-08 03:34:15 -07:00
2024-09-26 04:23:08 +00:00
if ( ! apiClientProps . server ? . userId ) {
throw new Error ( 'No userId found' ) ;
}
2023-05-08 03:34:15 -07:00
2024-09-26 04:23:08 +00:00
const yearsGroup = [ ] ;
if ( query . minYear && query . maxYear ) {
for ( let i = Number ( query . minYear ) ; i <= Number ( query . maxYear ) ; i += 1 ) {
yearsGroup . push ( String ( i ) ) ;
}
}
2023-05-08 03:34:15 -07:00
2024-09-26 04:23:08 +00:00
const yearsFilter = yearsGroup . length ? formatCommaDelimitedString ( yearsGroup ) : undefined ;
2023-07-01 19:10:05 -07:00
2024-09-26 04:23:08 +00:00
const res = await jfApiClient ( apiClientProps ) . getSongList ( {
2023-07-01 19:10:05 -07:00
params : {
userId : apiClientProps.server?.userId ,
} ,
2024-09-26 04:23:08 +00:00
query : {
2025-05-17 21:35:58 -07:00
Fields : 'Genres, DateCreated, MediaSources, ParentId, People, Tags' ,
2024-09-26 04:23:08 +00:00
GenreIds : query.genre ? query.genre : undefined ,
IncludeItemTypes : 'Audio' ,
IsPlayed :
query . played === Played . Never
? false
: query . played === Played . Played
? true
: undefined ,
Limit : query.limit ,
ParentId : query.musicFolderId ,
Recursive : true ,
SortBy : JFSongListSort.RANDOM ,
SortOrder : JFSortOrder.ASC ,
StartIndex : 0 ,
Years : yearsFilter ,
} ,
2023-07-01 19:10:05 -07:00
} ) ;
2024-09-26 04:23:08 +00:00
if ( res . status !== 200 ) {
throw new Error ( 'Failed to get random songs' ) ;
}
2023-05-08 03:34:15 -07:00
2024-09-26 04:23:08 +00:00
return {
items : res.body.Items.map ( ( item ) = > jfNormalize . song ( item , apiClientProps . server , '' ) ) ,
startIndex : 0 ,
totalRecordCount : res.body.Items.length || 0 ,
} ;
} ,
2025-04-23 23:27:06 -07:00
getRoles : async ( ) = > [ ] ,
2024-09-26 04:23:08 +00:00
getServerInfo : async ( args ) = > {
const { apiClientProps } = args ;
2023-07-01 19:10:05 -07:00
2024-09-26 04:23:08 +00:00
const res = await jfApiClient ( apiClientProps ) . getServerInfo ( ) ;
2023-05-08 03:34:15 -07:00
2024-09-26 04:23:08 +00:00
if ( res . status !== 200 ) {
throw new Error ( 'Failed to get server info' ) ;
}
const features = getFeatures ( VERSION_INFO , res . body . Version ) ;
return {
features ,
id : apiClientProps.server?.id ,
version : res.body.Version ,
} ;
} ,
getSimilarSongs : async ( args ) = > {
const { apiClientProps , query } = args ;
// Prefer getSimilarSongs, where possible. Fallback to InstantMix
// where no similar songs were found.
const res = await jfApiClient ( apiClientProps ) . getSimilarSongs ( {
2023-07-01 19:10:05 -07:00
params : {
2024-09-26 04:23:08 +00:00
itemId : query.songId ,
} ,
query : {
Fields : 'Genres, DateCreated, MediaSources, ParentId' ,
Limit : query.count ,
UserId : apiClientProps.server?.userId || undefined ,
2023-07-01 19:10:05 -07:00
} ,
} ) ;
2024-09-26 04:23:08 +00:00
if ( res . status === 200 && res . body . Items . length ) {
const results = res . body . Items . reduce < Song [ ] > ( ( acc , song ) = > {
if ( song . Id !== query . songId ) {
acc . push ( jfNormalize . song ( song , apiClientProps . server , '' ) ) ;
}
2023-05-08 03:34:15 -07:00
2024-09-26 04:23:08 +00:00
return acc ;
} , [ ] ) ;
2023-05-08 03:34:15 -07:00
2024-09-26 04:23:08 +00:00
if ( results . length > 0 ) {
return results ;
}
}
2023-05-08 03:34:15 -07:00
2024-09-26 04:23:08 +00:00
const mix = await jfApiClient ( apiClientProps ) . getInstantMix ( {
params : {
itemId : query.songId ,
2023-07-01 19:10:05 -07:00
} ,
2024-09-26 04:23:08 +00:00
query : {
Fields : 'Genres, DateCreated, MediaSources, ParentId' ,
Limit : query.count ,
UserId : apiClientProps.server?.userId || undefined ,
2023-07-01 19:10:05 -07:00
} ,
} ) ;
2024-09-26 04:23:08 +00:00
if ( mix . status !== 200 ) {
throw new Error ( 'Failed to get similar songs' ) ;
}
2023-07-01 19:10:05 -07:00
2024-09-26 04:23:08 +00:00
return mix . body . Items . reduce < Song [ ] > ( ( acc , song ) = > {
if ( song . Id !== query . songId ) {
acc . push ( jfNormalize . song ( song , apiClientProps . server , '' ) ) ;
}
2023-07-01 19:10:05 -07:00
2024-09-26 04:23:08 +00:00
return acc ;
} , [ ] ) ;
} ,
getSongDetail : async ( args ) = > {
const { query , apiClientProps } = args ;
2023-07-01 19:10:05 -07:00
2024-09-26 04:23:08 +00:00
const res = await jfApiClient ( apiClientProps ) . getSongDetail ( {
params : {
id : query.id ,
userId : apiClientProps.server?.userId ? ? '' ,
2023-07-01 19:10:05 -07:00
} ,
} ) ;
2024-09-26 04:23:08 +00:00
if ( res . status !== 200 ) {
throw new Error ( 'Failed to get song detail' ) ;
}
2023-05-08 03:34:15 -07:00
2024-09-26 04:23:08 +00:00
return jfNormalize . song ( res . body , apiClientProps . server , '' ) ;
} ,
getSongList : async ( args ) = > {
const { query , apiClientProps } = args ;
2023-05-08 03:34:15 -07:00
2024-09-26 04:23:08 +00:00
if ( ! apiClientProps . server ? . userId ) {
throw new Error ( 'No userId found' ) ;
}
2023-05-19 02:06:58 -07:00
2024-09-26 04:23:08 +00:00
const yearsGroup = [ ] ;
if ( query . minYear && query . maxYear ) {
for ( let i = Number ( query . minYear ) ; i <= Number ( query . maxYear ) ; i += 1 ) {
yearsGroup . push ( String ( i ) ) ;
}
}
2023-05-19 02:06:58 -07:00
2024-09-26 04:23:08 +00:00
const yearsFilter = yearsGroup . length ? formatCommaDelimitedString ( yearsGroup ) : undefined ;
const artistIdsFilter = query . artistIds
? formatCommaDelimitedString ( query . artistIds )
2025-05-06 14:43:42 -07:00
: query . albumArtistIds
? formatCommaDelimitedString ( query . albumArtistIds )
: undefined ;
2023-07-01 19:10:05 -07:00
2025-05-07 01:42:32 -07:00
let items : z.infer < typeof jfType._response.song > [ ] = [ ] ;
let totalRecordCount = 0 ;
const batchSize = 50 ;
// Handle albumIds fetches in batches to prevent HTTP 414 errors
if ( query . albumIds && query . albumIds . length > batchSize ) {
const albumIdBatches = chunk ( query . albumIds , batchSize ) ;
for ( const batch of albumIdBatches ) {
const albumIdsFilter = formatCommaDelimitedString ( batch ) ;
const res = await jfApiClient ( apiClientProps ) . getSongList ( {
params : {
userId : apiClientProps.server?.userId ,
} ,
query : {
AlbumIds : albumIdsFilter ,
ArtistIds : artistIdsFilter ,
2025-05-17 21:35:58 -07:00
Fields : 'Genres, DateCreated, MediaSources, ParentId, People, Tags' ,
2025-05-07 01:42:32 -07:00
GenreIds : query.genreIds?.join ( ',' ) ,
IncludeItemTypes : 'Audio' ,
IsFavorite : query.favorite ,
Limit : query.limit ,
ParentId : query.musicFolderId ,
Recursive : true ,
SearchTerm : query.searchTerm ,
SortBy : songListSortMap.jellyfin [ query . sortBy ] || 'Album,SortName' ,
SortOrder : sortOrderMap.jellyfin [ query . sortOrder ] ,
StartIndex : query.startIndex ,
. . . query . _custom ? . jellyfin ,
Years : yearsFilter ,
} ,
} ) ;
if ( res . status !== 200 ) {
throw new Error ( 'Failed to get song list' ) ;
}
2023-07-01 19:10:05 -07:00
2025-05-07 01:42:32 -07:00
items = [ . . . items , . . . res . body . Items ] ;
totalRecordCount += res . body . Items . length ;
}
} else {
const albumIdsFilter = query . albumIds
? formatCommaDelimitedString ( query . albumIds )
: undefined ;
2023-07-01 19:10:05 -07:00
2025-05-07 01:42:32 -07:00
const res = await jfApiClient ( apiClientProps ) . getSongList ( {
params : {
userId : apiClientProps.server?.userId ,
} ,
query : {
AlbumIds : albumIdsFilter ,
ArtistIds : artistIdsFilter ,
2025-05-17 21:35:58 -07:00
Fields : 'Genres, DateCreated, MediaSources, ParentId, People, Tags' ,
2025-05-07 01:42:32 -07:00
GenreIds : query.genreIds?.join ( ',' ) ,
IncludeItemTypes : 'Audio' ,
IsFavorite : query.favorite ,
Limit : query.limit ,
ParentId : query.musicFolderId ,
Recursive : true ,
SearchTerm : query.searchTerm ,
SortBy : songListSortMap.jellyfin [ query . sortBy ] || 'Album,SortName' ,
SortOrder : sortOrderMap.jellyfin [ query . sortOrder ] ,
StartIndex : query.startIndex ,
. . . query . _custom ? . jellyfin ,
Years : yearsFilter ,
} ,
} ) ;
2023-05-19 02:06:58 -07:00
2025-05-07 01:42:32 -07:00
if ( res . status !== 200 ) {
throw new Error ( 'Failed to get song list' ) ;
}
2023-07-01 19:10:05 -07:00
2025-05-07 01:42:32 -07:00
// Jellyfin Bodge because of code from https://github.com/jellyfin/jellyfin/blob/c566ccb63bf61f9c36743ddb2108a57c65a2519b/Emby.Server.Implementations/Data/SqliteItemRepository.cs#L3622
// If the Album ID filter is passed, Jellyfin will search for
// 1. the matching album id
// 2. An album with the name of the album.
// It is this second condition causing issues,
if ( query . albumIds ) {
const albumIdSet = new Set ( query . albumIds ) ;
items = res . body . Items . filter ( ( item ) = > albumIdSet . has ( item . AlbumId ! ) ) ;
totalRecordCount = items . length ;
} else {
items = res . body . Items ;
totalRecordCount = res . body . TotalRecordCount ;
2024-09-26 04:23:08 +00:00
}
2023-07-01 19:10:05 -07:00
}
2024-09-26 04:23:08 +00:00
return {
items : items.map ( ( item ) = >
jfNormalize . song ( item , apiClientProps . server , '' , query . imageSize ) ,
) ,
startIndex : query.startIndex ,
2025-05-07 01:42:32 -07:00
totalRecordCount ,
2024-09-26 04:23:08 +00:00
} ;
} ,
getSongListCount : async ( { apiClientProps , query } ) = >
JellyfinController . getSongList ( {
apiClientProps ,
query : { . . . query , limit : 1 , startIndex : 0 } ,
} ) . then ( ( result ) = > result ! . totalRecordCount ! ) ,
2025-05-18 09:23:52 -07:00
getTags : async ( args ) = > {
const { apiClientProps , query } = args ;
if ( ! hasFeature ( apiClientProps . server , ServerFeature . TAGS ) ) {
return { boolTags : undefined , enumTags : undefined } ;
}
const res = await jfApiClient ( apiClientProps ) . getFilterList ( {
query : {
IncludeItemTypes : query.type === LibraryItem . SONG ? 'Audio' : 'MusicAlbum' ,
ParentId : query.folder ,
UserId : apiClientProps.server?.userId ? ? '' ,
} ,
} ) ;
if ( res . status !== 200 ) {
throw new Error ( 'failed to get tags' ) ;
}
return {
boolTags : res.body.Tags?.sort ( ( a , b ) = >
a . toLocaleLowerCase ( ) . localeCompare ( b . toLocaleLowerCase ( ) ) ,
) ,
} ;
} ,
2024-09-26 04:23:08 +00:00
getTopSongs : async ( args ) = > {
const { apiClientProps , query } = args ;
if ( ! apiClientProps . server ? . userId ) {
throw new Error ( 'No userId found' ) ;
}
2023-05-19 02:06:58 -07:00
2024-09-26 04:23:08 +00:00
const res = await jfApiClient ( apiClientProps ) . getTopSongsList ( {
2023-07-01 19:10:05 -07:00
params : {
userId : apiClientProps.server?.userId ,
} ,
query : {
2024-09-26 04:23:08 +00:00
ArtistIds : query.artistId ,
2023-07-01 19:10:05 -07:00
Fields : 'Genres, DateCreated, MediaSources, ParentId' ,
IncludeItemTypes : 'Audio' ,
2024-09-26 04:23:08 +00:00
Limit : query.limit ,
2023-07-01 19:10:05 -07:00
Recursive : true ,
2024-09-26 04:23:08 +00:00
SortBy : 'PlayCount,SortName' ,
SortOrder : 'Descending' ,
2023-07-01 19:10:05 -07:00
UserId : apiClientProps.server?.userId ,
} ,
} ) ;
if ( res . status !== 200 ) {
2024-09-26 04:23:08 +00:00
throw new Error ( 'Failed to get top song list' ) ;
2023-07-01 19:10:05 -07:00
}
2024-09-26 04:23:08 +00:00
return {
items : res.body.Items.map ( ( item ) = > jfNormalize . song ( item , apiClientProps . server , '' ) ) ,
startIndex : 0 ,
totalRecordCount : res.body.TotalRecordCount ,
} ;
} ,
getTranscodingUrl : ( args ) = > {
const { base , format , bitrate } = args . query ;
let url = base . replace ( 'transcodingProtocol=hls' , 'transcodingProtocol=http' ) ;
if ( format ) {
url = url . replace ( 'audioCodec=aac' , ` audioCodec= ${ format } ` ) ;
url = url . replace ( 'transcodingContainer=ts' , ` transcodingContainer= ${ format } ` ) ;
}
if ( bitrate !== undefined ) {
url += ` &maxStreamingBitrate= ${ bitrate * 1000 } ` ;
2023-07-01 19:10:05 -07:00
}
2023-10-17 23:05:44 +00:00
2024-09-26 04:23:08 +00:00
return url ;
} ,
movePlaylistItem : async ( args ) = > {
const { apiClientProps , query } = args ;
2023-10-17 23:05:44 +00:00
2024-09-26 04:23:08 +00:00
const res = await jfApiClient ( apiClientProps ) . movePlaylistItem ( {
params : {
itemId : query.trackId ,
newIdx : query.endingIndex.toString ( ) ,
playlistId : query.playlistId ,
} ,
} ) ;
2023-10-17 23:05:44 +00:00
2024-09-26 04:23:08 +00:00
if ( res . status !== 204 ) {
throw new Error ( 'Failed to move item in playlist' ) ;
}
} ,
removeFromPlaylist : async ( args ) = > {
const { query , apiClientProps } = args ;
const chunks = chunk ( query . songId , MAX_ITEMS_PER_PLAYLIST_ADD ) ;
for ( const chunk of chunks ) {
const res = await jfApiClient ( apiClientProps ) . removeFromPlaylist ( {
params : {
id : query.id ,
} ,
query : {
EntryIds : chunk.join ( ',' ) ,
} ,
} ) ;
if ( res . status !== 204 ) {
throw new Error ( 'Failed to remove from playlist' ) ;
}
}
2023-10-17 23:05:44 +00:00
2024-09-26 04:23:08 +00:00
return null ;
} ,
scrobble : async ( args ) = > {
const { query , apiClientProps } = args ;
const position = query . position && Math . round ( query . position ) ;
if ( query . submission ) {
// Checked by jellyfin-plugin-lastfm for whether or not to send the "finished" scrobble (uses PositionTicks)
jfApiClient ( apiClientProps ) . scrobbleStopped ( {
body : {
IsPaused : true ,
ItemId : query.id ,
PositionTicks : position ,
} ,
} ) ;
return null ;
}
2024-04-22 19:44:10 -07:00
2024-09-26 04:23:08 +00:00
if ( query . event === 'start' ) {
jfApiClient ( apiClientProps ) . scrobblePlaying ( {
body : {
ItemId : query.id ,
PositionTicks : position ,
} ,
} ) ;
2024-02-01 08:17:31 -08:00
2024-09-26 04:23:08 +00:00
return null ;
}
2024-02-01 08:17:31 -08:00
2024-09-26 04:23:08 +00:00
if ( query . event === 'pause' ) {
jfApiClient ( apiClientProps ) . scrobbleProgress ( {
body : {
EventName : query.event ,
IsPaused : true ,
ItemId : query.id ,
PositionTicks : position ,
} ,
} ) ;
return null ;
}
2024-02-01 08:17:31 -08:00
2024-09-26 04:23:08 +00:00
if ( query . event === 'unpause' ) {
jfApiClient ( apiClientProps ) . scrobbleProgress ( {
body : {
EventName : query.event ,
IsPaused : false ,
ItemId : query.id ,
PositionTicks : position ,
} ,
} ) ;
return null ;
}
2024-03-03 22:15:49 -08:00
2024-09-26 04:23:08 +00:00
jfApiClient ( apiClientProps ) . scrobbleProgress ( {
body : {
ItemId : query.id ,
PositionTicks : position ,
} ,
} ) ;
2024-02-01 08:17:31 -08:00
2024-09-26 04:23:08 +00:00
return null ;
} ,
search : async ( args ) = > {
const { query , apiClientProps } = args ;
2024-02-19 08:53:50 -08:00
2024-09-26 04:23:08 +00:00
if ( ! apiClientProps . server ? . userId ) {
throw new Error ( 'No userId found' ) ;
}
2024-02-19 08:53:50 -08:00
2024-09-26 04:23:08 +00:00
let albums : z.infer < typeof jfType._response.albumList > [ 'Items' ] = [ ] ;
let albumArtists : z.infer < typeof jfType._response.albumArtistList > [ 'Items' ] = [ ] ;
let songs : z.infer < typeof jfType._response.songList > [ 'Items' ] = [ ] ;
if ( query . albumLimit ) {
const res = await jfApiClient ( apiClientProps ) . getAlbumList ( {
params : {
userId : apiClientProps.server?.userId ,
} ,
query : {
EnableTotalRecordCount : true ,
2025-05-17 21:35:58 -07:00
Fields : 'People, Tags' ,
2024-09-26 04:23:08 +00:00
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' ) ;
2024-04-08 08:49:55 -07:00
}
2024-09-26 04:23:08 +00:00
albums = res . body . Items ;
2024-04-08 08:49:55 -07:00
}
2024-09-26 04:23:08 +00:00
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' ) ;
}
2024-02-19 08:53:50 -08:00
2024-09-26 04:23:08 +00:00
albumArtists = res . body . Items ;
2024-02-19 10:09:05 -08:00
}
2024-09-26 04:23:08 +00:00
if ( query . songLimit ) {
const res = await jfApiClient ( apiClientProps ) . getSongList ( {
params : {
userId : apiClientProps.server?.userId ,
} ,
query : {
EnableTotalRecordCount : true ,
2025-05-17 21:35:58 -07:00
Fields : 'Genres, DateCreated, MediaSources, ParentId, People, Tags' ,
2024-09-26 04:23:08 +00:00
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' ) ;
}
2024-02-19 08:53:50 -08:00
2024-09-26 04:23:08 +00:00
songs = res . body . Items ;
}
2024-08-25 15:21:56 -07:00
2024-09-26 04:23:08 +00:00
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 , '' ) ) ,
} ;
} ,
updatePlaylist : async ( args ) = > {
const { query , body , apiClientProps } = args ;
2024-08-25 15:21:56 -07:00
2024-09-26 04:23:08 +00:00
if ( ! apiClientProps . server ? . userId ) {
throw new Error ( 'No userId found' ) ;
}
2024-08-25 15:21:56 -07:00
2024-09-26 04:23:08 +00:00
const res = await jfApiClient ( apiClientProps ) . updatePlaylist ( {
body : {
Genres : body.genres?.map ( ( item ) = > ( { Id : item.id , Name : item.name } ) ) || [ ] ,
IsPublic : body.public ,
MediaType : 'Audio' ,
Name : body.name ,
PremiereDate : null ,
ProviderIds : { } ,
Tags : [ ] ,
UserId : apiClientProps.server?.userId , // Required
} ,
params : {
id : query.id ,
} ,
} ) ;
2024-08-25 17:08:38 -07:00
2024-09-26 04:23:08 +00:00
if ( res . status !== 204 ) {
throw new Error ( 'Failed to update playlist' ) ;
}
2024-08-25 17:08:38 -07:00
2024-09-26 04:23:08 +00:00
return null ;
} ,
2024-09-01 08:26:30 -07:00
} ;
2024-09-26 04:23:08 +00:00
// const getArtistList = async (args: ArtistListArgs): Promise<AlbumArtistListResponse> => {
// const { query, apiClientProps } = args;
// const res = await jfApiClient(apiClientProps).getAlbumArtistList({
// query: {
// Limit: query.limit,
// ParentId: query.musicFolderId,
// Recursive: true,
// SortBy: artistListSortMap.jellyfin[query.sortBy] || 'SortName,Name',
// SortOrder: sortOrderMap.jellyfin[query.sortOrder],
// StartIndex: query.startIndex,
// },
// });
// if (res.status !== 200) {
// throw new Error('Failed to get artist list');
// }
// return {
// items: res.body.Items.map((item) => jfNormalize.albumArtist(item, apiClientProps.server)),
// startIndex: query.startIndex,
// totalRecordCount: res.body.TotalRecordCount,
// };
// };