2023-05-08 03:34:15 -07:00
import {
2023-07-01 19:10:05 -07:00
AuthenticationResponse ,
MusicFolderListArgs ,
MusicFolderListResponse ,
GenreListArgs ,
AlbumArtistDetailArgs ,
AlbumArtistListArgs ,
albumArtistListSortMap ,
sortOrderMap ,
ArtistListArgs ,
artistListSortMap ,
AlbumDetailArgs ,
AlbumListArgs ,
albumListSortMap ,
TopSongListArgs ,
SongListArgs ,
songListSortMap ,
AddToPlaylistArgs ,
RemoveFromPlaylistArgs ,
PlaylistDetailArgs ,
PlaylistSongListArgs ,
PlaylistListArgs ,
playlistListSortMap ,
CreatePlaylistArgs ,
CreatePlaylistResponse ,
UpdatePlaylistArgs ,
UpdatePlaylistResponse ,
DeletePlaylistArgs ,
FavoriteArgs ,
FavoriteResponse ,
ScrobbleArgs ,
ScrobbleResponse ,
GenreListResponse ,
AlbumArtistDetailResponse ,
AlbumArtistListResponse ,
AlbumDetailResponse ,
AlbumListResponse ,
SongListResponse ,
AddToPlaylistResponse ,
RemoveFromPlaylistResponse ,
PlaylistDetailResponse ,
PlaylistListResponse ,
SearchArgs ,
SearchResponse ,
RandomSongListResponse ,
RandomSongListArgs ,
LyricsArgs ,
LyricsResponse ,
2023-07-31 17:16:48 -07:00
genreListSortMap ,
2023-10-17 23:05:44 +00:00
SongDetailArgs ,
SongDetailResponse ,
2024-02-01 08:17:31 -08:00
ServerInfo ,
ServerInfoArgs ,
2024-02-19 08:53:50 -08:00
SimilarSongsArgs ,
Song ,
2024-08-25 15:21:56 -07:00
MoveItemArgs ,
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' ;
import { VersionInfo , getFeatures } 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
} ;
const authenticate = async (
2023-07-01 19:10:05 -07:00
url : string ,
2023-05-08 03:34:15 -07:00
body : {
2023-07-01 19:10:05 -07:00
password : string ;
username : string ;
2023-05-09 05:06:32 -07:00
} ,
2023-07-01 19:10:05 -07:00
) : Promise < AuthenticationResponse > = > {
const cleanServerUrl = url . replace ( /\/$/ , '' ) ;
const res = await jfApiClient ( { server : null , url : cleanServerUrl } ) . authenticate ( {
body : {
Pw : body.password ,
Username : body.username ,
} ,
} ) ;
2023-05-08 03:34:15 -07:00
2023-07-01 19:10:05 -07:00
if ( res . status !== 200 ) {
throw new Error ( 'Failed to authenticate' ) ;
}
2023-05-08 03:34:15 -07:00
2023-07-01 19:10:05 -07:00
return {
credential : res.body.AccessToken ,
userId : res.body.User.Id ,
username : res.body.User.Name ,
} ;
2023-05-08 03:34:15 -07:00
} ;
const getMusicFolderList = async ( args : MusicFolderListArgs ) : Promise < MusicFolderListResponse > = > {
2023-07-01 19:10:05 -07:00
const { apiClientProps } = args ;
const userId = apiClientProps . server ? . userId ;
2023-05-08 03:34:15 -07:00
2023-07-01 19:10:05 -07:00
if ( ! userId ) throw new Error ( 'No userId found' ) ;
2023-05-08 03:34:15 -07:00
2023-07-01 19:10:05 -07:00
const res = await jfApiClient ( apiClientProps ) . getMusicFolderList ( {
params : {
userId ,
} ,
} ) ;
2023-05-08 03:34:15 -07:00
2023-07-01 19:10:05 -07:00
if ( res . status !== 200 ) {
throw new Error ( 'Failed to get genre list' ) ;
}
2023-05-08 03:34:15 -07:00
2023-07-01 19:10:05 -07:00
const musicFolders = res . body . Items . filter (
( folder ) = > folder . CollectionType === jfType . _enum . collection . MUSIC ,
) ;
2023-05-08 03:34:15 -07:00
2023-07-01 19:10:05 -07:00
return {
items : musicFolders.map ( jfNormalize . musicFolder ) ,
startIndex : 0 ,
totalRecordCount : musicFolders?.length || 0 ,
} ;
2023-05-08 03:34:15 -07:00
} ;
const getGenreList = async ( args : GenreListArgs ) : Promise < GenreListResponse > = > {
2023-07-31 17:16:48 -07:00
const { apiClientProps , query } = args ;
2023-08-08 09:18:43 -07:00
if ( ! apiClientProps . server ? . userId ) {
throw new Error ( 'No userId found' ) ;
}
2023-07-31 17:16:48 -07:00
const res = await jfApiClient ( apiClientProps ) . getGenreList ( {
query : {
2023-08-08 09:18:43 -07:00
Fields : 'ItemCounts' ,
ParentId : query?.musicFolderId ,
Recursive : true ,
2023-07-31 17:16:48 -07:00
SearchTerm : query?.searchTerm ,
2023-08-08 09:18:43 -07:00
SortBy : genreListSortMap.jellyfin [ query . sortBy ] || 'SortName' ,
2023-07-31 17:16:48 -07:00
SortOrder : sortOrderMap.jellyfin [ query . sortOrder ] ,
StartIndex : query.startIndex ,
2023-08-08 09:18:43 -07:00
UserId : apiClientProps.server?.userId ,
2023-07-31 17:16:48 -07:00
} ,
} ) ;
2023-05-08 03:34:15 -07:00
2023-07-01 19:10:05 -07:00
if ( res . status !== 200 ) {
throw new Error ( 'Failed to get genre list' ) ;
}
2023-05-08 03:34:15 -07:00
2023-07-01 19:10:05 -07:00
return {
2023-08-08 09:18:43 -07:00
items : res.body.Items.map ( ( item ) = > jfNormalize . genre ( item , apiClientProps . server ) ) ,
2023-07-31 17:16:48 -07:00
startIndex : query.startIndex || 0 ,
totalRecordCount : res.body?.TotalRecordCount || 0 ,
2023-07-01 19:10:05 -07:00
} ;
2023-05-08 03:34:15 -07:00
} ;
const getAlbumArtistDetail = async (
2023-07-01 19:10:05 -07:00
args : AlbumArtistDetailArgs ,
2023-05-08 03:34:15 -07:00
) : Promise < AlbumArtistDetailResponse > = > {
2023-07-01 19:10:05 -07:00
const { query , apiClientProps } = args ;
2023-05-08 03:34:15 -07:00
2023-07-01 19:10:05 -07:00
if ( ! apiClientProps . server ? . userId ) {
throw new Error ( 'No userId found' ) ;
}
2023-05-08 03:34:15 -07:00
2023-07-01 19:10:05 -07:00
const res = await jfApiClient ( apiClientProps ) . getAlbumArtistDetail ( {
params : {
id : query.id ,
userId : apiClientProps.server?.userId ,
} ,
query : {
Fields : 'Genres, Overview' ,
} ,
} ) ;
2023-05-08 03:34:15 -07:00
2023-07-01 19:10:05 -07:00
const similarArtistsRes = await jfApiClient ( apiClientProps ) . getSimilarArtistList ( {
params : {
id : query.id ,
} ,
query : {
Limit : 10 ,
} ,
} ) ;
2023-05-08 03:34:15 -07:00
2023-07-01 19:10:05 -07:00
if ( res . status !== 200 || similarArtistsRes . status !== 200 ) {
throw new Error ( 'Failed to get album artist detail' ) ;
}
2023-05-08 03:34:15 -07:00
2023-07-01 19:10:05 -07:00
return jfNormalize . albumArtist (
{ . . . res . body , similarArtists : similarArtistsRes.body } ,
apiClientProps . server ,
) ;
2023-05-08 03:34:15 -07:00
} ;
const getAlbumArtistList = async ( args : AlbumArtistListArgs ) : Promise < AlbumArtistListResponse > = > {
2023-07-01 19:10:05 -07:00
const { query , apiClientProps } = args ;
2023-05-08 03:34:15 -07:00
2023-07-01 19:10:05 -07: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 ,
2024-04-19 23:11:26 -07:00
SortBy : albumArtistListSortMap.jellyfin [ query . sortBy ] || 'SortName,Name' ,
2023-07-01 19:10:05 -07:00
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' ) ;
}
2023-05-08 03:34:15 -07:00
2023-07-01 19:10:05 -07:00
return {
items : res.body.Items.map ( ( item ) = > jfNormalize . albumArtist ( item , apiClientProps . server ) ) ,
startIndex : query.startIndex ,
totalRecordCount : res.body.TotalRecordCount ,
} ;
2023-05-08 03:34:15 -07:00
} ;
const getArtistList = async ( args : ArtistListArgs ) : Promise < AlbumArtistListResponse > = > {
2023-07-01 19:10:05 -07:00
const { query , apiClientProps } = args ;
2023-05-08 03:34:15 -07:00
2023-07-01 19:10:05 -07:00
const res = await jfApiClient ( apiClientProps ) . getAlbumArtistList ( {
query : {
Limit : query.limit ,
ParentId : query.musicFolderId ,
Recursive : true ,
2024-04-19 23:11:26 -07:00
SortBy : artistListSortMap.jellyfin [ query . sortBy ] || 'SortName,Name' ,
2023-07-01 19:10:05 -07:00
SortOrder : sortOrderMap.jellyfin [ query . sortOrder ] ,
StartIndex : query.startIndex ,
} ,
} ) ;
if ( res . status !== 200 ) {
throw new Error ( 'Failed to get artist list' ) ;
}
2023-05-08 03:34:15 -07:00
2023-07-01 19:10:05 -07:00
return {
items : res.body.Items.map ( ( item ) = > jfNormalize . albumArtist ( item , apiClientProps . server ) ) ,
startIndex : query.startIndex ,
totalRecordCount : res.body.TotalRecordCount ,
} ;
2023-05-08 03:34:15 -07:00
} ;
const getAlbumDetail = async ( args : AlbumDetailArgs ) : Promise < AlbumDetailResponse > = > {
2023-07-01 19:10:05 -07:00
const { query , apiClientProps } = args ;
2023-05-08 03:34:15 -07:00
2023-07-01 19:10:05 -07:00
if ( ! apiClientProps . server ? . userId ) {
throw new Error ( 'No userId found' ) ;
}
2023-05-08 03:34:15 -07:00
2023-07-01 19:10:05 -07:00
const res = await jfApiClient ( apiClientProps ) . getAlbumDetail ( {
params : {
id : query.id ,
userId : apiClientProps.server.userId ,
} ,
query : {
Fields : 'Genres, DateCreated, ChildCount' ,
} ,
} ) ;
2023-05-08 03:34:15 -07:00
2023-07-01 19:10:05 -07:00
const songsRes = await jfApiClient ( apiClientProps ) . getSongList ( {
params : {
userId : apiClientProps.server.userId ,
} ,
query : {
Fields : 'Genres, DateCreated, MediaSources, ParentId' ,
IncludeItemTypes : 'Audio' ,
ParentId : query.id ,
2023-08-04 13:26:33 -07:00
SortBy : 'ParentIndexNumber,IndexNumber,SortName' ,
2023-07-01 19:10:05 -07:00
} ,
} ) ;
2023-05-08 03:34:15 -07:00
2023-07-01 19:10:05 -07:00
if ( res . status !== 200 || songsRes . status !== 200 ) {
throw new Error ( 'Failed to get album detail' ) ;
}
2023-05-08 03:34:15 -07:00
2023-07-01 19:10:05 -07:00
return jfNormalize . album ( { . . . res . body , Songs : songsRes.body.Items } , apiClientProps . server ) ;
2023-05-08 03:34:15 -07:00
} ;
const getAlbumList = async ( args : AlbumListArgs ) : Promise < AlbumListResponse > = > {
2023-07-01 19:10:05 -07:00
const { query , apiClientProps } = args ;
2023-05-08 03:34:15 -07:00
2023-07-01 19:10:05 -07:00
if ( ! apiClientProps . server ? . userId ) {
throw new Error ( 'No userId found' ) ;
}
2023-05-08 03:34:15 -07:00
2023-07-01 19:10:05 -07:00
const yearsGroup = [ ] ;
if ( query . _custom ? . jellyfin ? . minYear && query . _custom ? . jellyfin ? . maxYear ) {
for (
let i = Number ( query . _custom ? . jellyfin ? . minYear ) ;
i <= Number ( query . _custom ? . jellyfin ? . maxYear ) ;
i += 1
) {
yearsGroup . push ( String ( i ) ) ;
}
2023-05-08 03:34:15 -07:00
}
2023-07-01 19:10:05 -07:00
const yearsFilter = yearsGroup . length ? yearsGroup . join ( ',' ) : undefined ;
2023-05-08 03:34:15 -07:00
2023-07-01 19:10:05 -07:00
const res = await jfApiClient ( apiClientProps ) . getAlbumList ( {
params : {
userId : apiClientProps.server?.userId ,
} ,
query : {
AlbumArtistIds : query.artistIds
? formatCommaDelimitedString ( query . artistIds )
: undefined ,
IncludeItemTypes : 'MusicAlbum' ,
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 ,
} ,
} ) ;
2023-05-08 03:34:15 -07:00
2023-07-01 19:10:05 -07:00
if ( res . status !== 200 ) {
throw new Error ( 'Failed to get album list' ) ;
}
2023-05-08 03:34:15 -07:00
2023-07-01 19:10:05 -07:00
return {
items : res.body.Items.map ( ( item ) = > jfNormalize . album ( item , apiClientProps . server ) ) ,
startIndex : query.startIndex ,
totalRecordCount : res.body.TotalRecordCount ,
} ;
2023-05-08 03:34:15 -07:00
} ;
const getTopSongList = async ( args : TopSongListArgs ) : Promise < SongListResponse > = > {
2023-07-01 19:10:05 -07:00
const { apiClientProps , query } = args ;
2023-05-08 03:34:15 -07:00
2023-07-01 19:10:05 -07:00
if ( ! apiClientProps . server ? . userId ) {
throw new Error ( 'No userId found' ) ;
}
2023-05-08 03:34:15 -07:00
2023-07-01 19:10:05 -07:00
const res = await jfApiClient ( apiClientProps ) . getTopSongsList ( {
params : {
userId : apiClientProps.server?.userId ,
} ,
query : {
ArtistIds : query.artistId ,
Fields : 'Genres, DateCreated, MediaSources, ParentId' ,
IncludeItemTypes : 'Audio' ,
Limit : query.limit ,
Recursive : true ,
2024-02-03 21:21:15 +00:00
SortBy : 'PlayCount,SortName' ,
2023-07-01 19:10:05 -07:00
SortOrder : 'Descending' ,
UserId : apiClientProps.server?.userId ,
} ,
} ) ;
2023-05-08 03:34:15 -07:00
2023-07-01 19:10:05 -07:00
if ( res . status !== 200 ) {
throw new Error ( 'Failed to get top song list' ) ;
}
2023-05-08 03:34:15 -07:00
2023-07-01 19:10:05 -07:00
return {
items : res.body.Items.map ( ( item ) = > jfNormalize . song ( item , apiClientProps . server , '' ) ) ,
startIndex : 0 ,
totalRecordCount : res.body.TotalRecordCount ,
} ;
2023-05-08 03:34:15 -07:00
} ;
const getSongList = async ( args : SongListArgs ) : Promise < SongListResponse > = > {
2023-07-01 19:10:05 -07:00
const { query , apiClientProps } = args ;
if ( ! apiClientProps . server ? . userId ) {
throw new Error ( 'No userId found' ) ;
}
const yearsGroup = [ ] ;
if ( query . _custom ? . jellyfin ? . minYear && query . _custom ? . jellyfin ? . maxYear ) {
for (
let i = Number ( query . _custom ? . jellyfin ? . minYear ) ;
i <= Number ( query . _custom ? . jellyfin ? . maxYear ) ;
i += 1
) {
yearsGroup . push ( String ( i ) ) ;
}
}
const yearsFilter = yearsGroup . length ? formatCommaDelimitedString ( yearsGroup ) : undefined ;
const albumIdsFilter = query . albumIds ? formatCommaDelimitedString ( query . albumIds ) : undefined ;
const artistIdsFilter = query . artistIds
? formatCommaDelimitedString ( query . artistIds )
: undefined ;
2023-05-08 03:34:15 -07:00
2023-07-01 19:10:05 -07:00
const res = await jfApiClient ( apiClientProps ) . getSongList ( {
params : {
userId : apiClientProps.server?.userId ,
} ,
query : {
AlbumIds : albumIdsFilter ,
ArtistIds : artistIdsFilter ,
Fields : 'Genres, DateCreated, MediaSources, ParentId' ,
IncludeItemTypes : 'Audio' ,
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-05-08 03:34:15 -07:00
2024-04-13 16:28:36 -07:00
let items : z.infer < typeof jfType._response.song > [ ] ;
// 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 ) ) ;
if ( items . length < res . body . Items . length ) {
res . body . TotalRecordCount -= res . body . Items . length - items . length ;
}
} else {
items = res . body . Items ;
}
2023-07-01 19:10:05 -07:00
return {
2024-04-13 16:28:36 -07:00
items : items.map ( ( item ) = >
2023-09-23 15:36:57 -07:00
jfNormalize . song ( item , apiClientProps . server , '' , query . imageSize ) ,
) ,
2023-07-01 19:10:05 -07:00
startIndex : query.startIndex ,
totalRecordCount : res.body.TotalRecordCount ,
} ;
2023-05-08 03:34:15 -07:00
} ;
2024-05-02 18:42:49 -07: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
const addToPlaylist = async ( args : AddToPlaylistArgs ) : Promise < AddToPlaylistResponse > = > {
2023-07-01 19:10:05 -07:00
const { query , body , apiClientProps } = args ;
2023-05-08 03:34:15 -07:00
2023-07-01 19:10:05 -07:00
if ( ! apiClientProps . server ? . userId ) {
throw new Error ( 'No userId found' ) ;
}
2023-05-08 03:34:15 -07:00
2024-05-02 18:42:49 -07: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-07-01 19:10:05 -07:00
}
2023-05-08 03:34:15 -07:00
2023-07-01 19:10:05 -07:00
return null ;
2023-05-08 03:34:15 -07:00
} ;
const removeFromPlaylist = async (
2023-07-01 19:10:05 -07:00
args : RemoveFromPlaylistArgs ,
2023-05-08 03:34:15 -07:00
) : Promise < RemoveFromPlaylistResponse > = > {
2023-07-01 19:10:05 -07:00
const { query , apiClientProps } = args ;
2024-05-02 18:42:49 -07:00
const chunks = chunk ( query . songId , MAX_ITEMS_PER_PLAYLIST_ADD ) ;
for ( const chunk of chunks ) {
const res = await jfApiClient ( apiClientProps ) . removeFromPlaylist ( {
body : null ,
params : {
id : query.id ,
} ,
query : {
EntryIds : chunk.join ( ',' ) ,
} ,
} ) ;
if ( res . status !== 204 ) {
throw new Error ( 'Failed to remove from playlist' ) ;
}
2023-07-01 19:10:05 -07:00
}
2023-05-08 03:34:15 -07:00
2023-07-01 19:10:05 -07:00
return null ;
2023-05-08 03:34:15 -07:00
} ;
const getPlaylistDetail = async ( args : PlaylistDetailArgs ) : Promise < PlaylistDetailResponse > = > {
2023-07-01 19:10:05 -07:00
const { query , apiClientProps } = args ;
2023-05-08 03:34:15 -07:00
2023-07-01 19:10:05 -07:00
if ( ! apiClientProps . server ? . userId ) {
throw new Error ( 'No userId found' ) ;
}
2023-05-08 03:34:15 -07:00
2023-07-01 19:10:05 -07: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
2023-07-01 19:10:05 -07:00
if ( res . status !== 200 ) {
throw new Error ( 'Failed to get playlist detail' ) ;
}
2023-05-08 03:34:15 -07:00
2023-07-01 19:10:05 -07:00
return jfNormalize . playlist ( res . body , apiClientProps . server ) ;
2023-05-08 03:34:15 -07:00
} ;
const getPlaylistSongList = async ( args : PlaylistSongListArgs ) : Promise < SongListResponse > = > {
2023-07-01 19:10:05 -07:00
const { query , apiClientProps } = args ;
2023-05-08 03:34:15 -07:00
2023-07-01 19:10:05 -07:00
if ( ! apiClientProps . server ? . userId ) {
throw new Error ( 'No userId found' ) ;
}
2023-05-08 03:34:15 -07:00
2023-07-01 19:10:05 -07:00
const res = await jfApiClient ( apiClientProps ) . getPlaylistSongList ( {
params : {
id : query.id ,
} ,
query : {
Fields : 'Genres, DateCreated, MediaSources, UserData, ParentId' ,
IncludeItemTypes : 'Audio' ,
Limit : query.limit ,
SortBy : query.sortBy ? songListSortMap . jellyfin [ query . sortBy ] : undefined ,
SortOrder : query.sortOrder ? sortOrderMap . jellyfin [ query . sortOrder ] : undefined ,
2024-01-22 04:46:34 +01:00
StartIndex : query.startIndex ,
2023-07-01 19:10:05 -07:00
UserId : apiClientProps.server?.userId ,
} ,
} ) ;
2023-05-08 03:34:15 -07:00
2023-07-01 19:10:05 -07:00
if ( res . status !== 200 ) {
throw new Error ( 'Failed to get playlist song list' ) ;
}
2023-05-08 03:34:15 -07:00
2023-07-01 19:10:05 -07:00
return {
items : res.body.Items.map ( ( item ) = > jfNormalize . song ( item , apiClientProps . server , '' ) ) ,
startIndex : query.startIndex ,
totalRecordCount : res.body.TotalRecordCount ,
} ;
2023-05-08 03:34:15 -07:00
} ;
const getPlaylistList = async ( args : PlaylistListArgs ) : Promise < PlaylistListResponse > = > {
2023-07-01 19:10:05 -07:00
const { query , apiClientProps } = args ;
2023-05-08 03:34:15 -07:00
2023-07-01 19:10:05 -07:00
if ( ! apiClientProps . server ? . userId ) {
throw new Error ( 'No userId found' ) ;
}
2023-05-08 03:34:15 -07:00
2023-07-01 19:10:05 -07:00
const res = await jfApiClient ( apiClientProps ) . getPlaylistList ( {
params : {
userId : apiClientProps.server?.userId ,
} ,
query : {
Fields : 'ChildCount, Genres, DateCreated, ParentId, Overview' ,
IncludeItemTypes : 'Playlist' ,
Limit : query.limit ,
2023-10-22 16:00:41 -07:00
MediaTypes : 'Audio' ,
Recursive : true ,
2023-07-19 01:32:09 -07:00
SearchTerm : query.searchTerm ,
2023-07-01 19:10:05 -07:00
SortBy : playlistListSortMap.jellyfin [ query . sortBy ] ,
SortOrder : sortOrderMap.jellyfin [ query . sortOrder ] ,
StartIndex : query.startIndex ,
} ,
} ) ;
2023-05-08 03:34:15 -07:00
2023-07-01 19:10:05 -07:00
if ( res . status !== 200 ) {
throw new Error ( 'Failed to get playlist list' ) ;
}
2023-05-08 03:34:15 -07:00
2023-07-01 19:10:05 -07:00
return {
items : res.body.Items.map ( ( item ) = > jfNormalize . playlist ( item , apiClientProps . server ) ) ,
startIndex : 0 ,
totalRecordCount : res.body.TotalRecordCount ,
} ;
2023-05-08 03:34:15 -07:00
} ;
const createPlaylist = async ( args : CreatePlaylistArgs ) : Promise < CreatePlaylistResponse > = > {
2023-07-01 19:10:05 -07:00
const { body , apiClientProps } = args ;
2023-05-08 03:34:15 -07:00
2023-07-01 19:10:05 -07:00
if ( ! apiClientProps . server ? . userId ) {
throw new Error ( 'No userId found' ) ;
}
2023-05-08 03:34:15 -07:00
2023-07-01 19:10:05 -07:00
const res = await jfApiClient ( apiClientProps ) . createPlaylist ( {
body : {
MediaType : 'Audio' ,
Name : body.name ,
Overview : body.comment || '' ,
UserId : apiClientProps.server.userId ,
} ,
} ) ;
2023-05-08 03:34:15 -07:00
2023-07-01 19:10:05 -07:00
if ( res . status !== 200 ) {
throw new Error ( 'Failed to create playlist' ) ;
}
2023-05-08 03:34:15 -07:00
2023-07-01 19:10:05 -07:00
return {
id : res.body.Id ,
} ;
2023-05-08 03:34:15 -07:00
} ;
const updatePlaylist = async ( args : UpdatePlaylistArgs ) : Promise < UpdatePlaylistResponse > = > {
2023-07-01 19:10:05 -07:00
const { query , body , apiClientProps } = args ;
2023-05-08 03:34:15 -07:00
2023-07-01 19:10:05 -07:00
if ( ! apiClientProps . server ? . userId ) {
throw new Error ( 'No userId found' ) ;
}
2023-05-08 03:34:15 -07:00
2023-07-01 19:10:05 -07:00
const res = await jfApiClient ( apiClientProps ) . updatePlaylist ( {
body : {
Genres : body.genres?.map ( ( item ) = > ( { Id : item.id , Name : item.name } ) ) || [ ] ,
MediaType : 'Audio' ,
Name : body.name ,
Overview : body.comment || '' ,
PremiereDate : null ,
ProviderIds : { } ,
Tags : [ ] ,
UserId : apiClientProps.server?.userId , // Required
} ,
params : {
id : query.id ,
} ,
} ) ;
2023-05-08 03:34:15 -07:00
2023-07-01 19:10:05 -07:00
if ( res . status !== 200 ) {
throw new Error ( 'Failed to update playlist' ) ;
}
2023-05-08 03:34:15 -07:00
2023-07-01 19:10:05 -07:00
return null ;
2023-05-08 03:34:15 -07:00
} ;
const deletePlaylist = async ( args : DeletePlaylistArgs ) : Promise < null > = > {
2023-07-01 19:10:05 -07:00
const { query , apiClientProps } = args ;
2023-05-08 03:34:15 -07:00
2023-07-01 19:10:05 -07:00
const res = await jfApiClient ( apiClientProps ) . deletePlaylist ( {
body : null ,
params : {
id : query.id ,
} ,
} ) ;
2023-05-08 03:34:15 -07:00
2023-07-01 19:10:05 -07:00
if ( res . status !== 204 ) {
throw new Error ( 'Failed to delete playlist' ) ;
}
2023-05-08 03:34:15 -07:00
2023-07-01 19:10:05 -07:00
return null ;
2023-05-08 03:34:15 -07:00
} ;
const createFavorite = async ( args : FavoriteArgs ) : Promise < FavoriteResponse > = > {
2023-07-01 19:10:05 -07:00
const { query , apiClientProps } = args ;
2023-05-08 03:34:15 -07:00
2023-07-01 19:10:05 -07:00
if ( ! apiClientProps . server ? . userId ) {
throw new Error ( 'No userId found' ) ;
}
for ( const id of query . id ) {
await jfApiClient ( apiClientProps ) . createFavorite ( {
body : { } ,
params : {
id ,
userId : apiClientProps.server?.userId ,
} ,
} ) ;
}
return null ;
2023-05-08 03:34:15 -07:00
} ;
const deleteFavorite = async ( args : FavoriteArgs ) : Promise < FavoriteResponse > = > {
2023-07-01 19:10:05 -07:00
const { query , apiClientProps } = args ;
if ( ! apiClientProps . server ? . userId ) {
throw new Error ( 'No userId found' ) ;
}
2023-05-08 03:34:15 -07:00
2023-07-01 19:10:05 -07:00
for ( const id of query . id ) {
await jfApiClient ( apiClientProps ) . removeFavorite ( {
body : { } ,
params : {
id ,
userId : apiClientProps.server?.userId ,
} ,
} ) ;
}
return null ;
2023-05-08 03:34:15 -07:00
} ;
const scrobble = async ( args : ScrobbleArgs ) : Promise < ScrobbleResponse > = > {
2023-07-01 19:10:05 -07:00
const { query , apiClientProps } = args ;
2023-05-08 03:34:15 -07:00
2023-07-01 19:10:05 -07:00
const position = query . position && Math . round ( query . position ) ;
2023-05-08 03:34:15 -07:00
2023-07-01 19:10:05 -07:00
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 ,
} ,
} ) ;
2023-05-08 03:34:15 -07:00
2023-07-01 19:10:05 -07:00
return null ;
}
2023-05-08 03:34:15 -07:00
2023-07-01 19:10:05 -07:00
if ( query . event === 'start' ) {
jfApiClient ( apiClientProps ) . scrobblePlaying ( {
body : {
ItemId : query.id ,
PositionTicks : position ,
} ,
} ) ;
return null ;
}
if ( query . event === 'pause' ) {
jfApiClient ( apiClientProps ) . scrobbleProgress ( {
body : {
EventName : query.event ,
IsPaused : true ,
ItemId : query.id ,
PositionTicks : position ,
} ,
} ) ;
return null ;
}
if ( query . event === 'unpause' ) {
jfApiClient ( apiClientProps ) . scrobbleProgress ( {
body : {
EventName : query.event ,
IsPaused : false ,
ItemId : query.id ,
PositionTicks : position ,
} ,
} ) ;
return null ;
}
2023-05-08 03:34:15 -07:00
jfApiClient ( apiClientProps ) . scrobbleProgress ( {
2023-07-01 19:10:05 -07:00
body : {
ItemId : query.id ,
PositionTicks : position ,
} ,
2023-05-08 03:34:15 -07:00
} ) ;
return null ;
} ;
2023-05-19 02:06:58 -07:00
const search = async ( args : SearchArgs ) : Promise < SearchResponse > = > {
2023-07-01 19:10:05 -07:00
const { query , apiClientProps } = args ;
2023-05-19 02:06:58 -07:00
2023-07-01 19:10:05 -07:00
if ( ! apiClientProps . server ? . userId ) {
throw new Error ( 'No userId found' ) ;
2023-05-19 02:06:58 -07:00
}
2023-07-01 19:10:05 -07: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 ,
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 ;
2023-05-19 02:06:58 -07:00
}
2023-07-01 19:10:05 -07: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' ) ;
}
albumArtists = res . body . Items ;
2023-05-19 02:06:58 -07:00
}
2023-07-01 19:10:05 -07:00
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 ;
}
2023-05-19 02:06:58 -07:00
2023-07-01 19:10:05 -07: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 , '' ) ) ,
} ;
2023-05-19 02:06:58 -07:00
} ;
2023-05-21 07:30:28 -07:00
const getRandomSongList = async ( args : RandomSongListArgs ) : Promise < RandomSongListResponse > = > {
2023-07-01 19:10:05 -07:00
const { query , apiClientProps } = args ;
2023-05-21 07:30:28 -07:00
2023-07-01 19:10:05 -07:00
if ( ! apiClientProps . server ? . userId ) {
throw new Error ( 'No userId found' ) ;
}
2023-05-21 07:30:28 -07:00
2023-07-01 19:10:05 -07: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-21 07:30:28 -07:00
}
2023-07-01 19:10:05 -07:00
const yearsFilter = yearsGroup . length ? formatCommaDelimitedString ( yearsGroup ) : undefined ;
2023-05-21 07:30:28 -07:00
2023-07-01 19:10:05 -07:00
const res = await jfApiClient ( apiClientProps ) . getSongList ( {
params : {
userId : apiClientProps.server?.userId ,
} ,
query : {
Fields : 'Genres, DateCreated, MediaSources, ParentId' ,
GenreIds : query.genre ? query.genre : undefined ,
IncludeItemTypes : 'Audio' ,
Limit : query.limit ,
ParentId : query.musicFolderId ,
Recursive : true ,
SortBy : JFSongListSort.RANDOM ,
SortOrder : JFSortOrder.ASC ,
StartIndex : 0 ,
Years : yearsFilter ,
} ,
} ) ;
2023-05-21 07:30:28 -07:00
2023-07-01 19:10:05 -07:00
if ( res . status !== 200 ) {
throw new Error ( 'Failed to get random songs' ) ;
}
2023-05-21 07:30:28 -07:00
2023-07-01 19:10:05 -07:00
return {
items : res.body.Items.map ( ( item ) = > jfNormalize . song ( item , apiClientProps . server , '' ) ) ,
startIndex : 0 ,
totalRecordCount : res.body.Items.length || 0 ,
} ;
2023-05-21 07:30:28 -07:00
} ;
2023-06-02 23:54:34 -07:00
2023-06-03 00:39:50 -07:00
const getLyrics = async ( args : LyricsArgs ) : Promise < LyricsResponse > = > {
2023-07-01 19:10:05 -07:00
const { query , apiClientProps } = args ;
2023-06-02 23:54:34 -07:00
2023-07-01 19:10:05 -07:00
if ( ! apiClientProps . server ? . userId ) {
throw new Error ( 'No userId found' ) ;
}
2023-06-02 23:54:34 -07:00
2023-07-01 19:10:05 -07:00
const res = await jfApiClient ( apiClientProps ) . getSongLyrics ( {
params : {
id : query.songId ,
} ,
} ) ;
2023-06-02 23:54:34 -07:00
2023-07-01 19:10:05 -07:00
if ( res . status !== 200 ) {
throw new Error ( 'Failed to get lyrics' ) ;
}
2023-06-02 23:54:34 -07:00
2023-07-01 19:10:05 -07:00
if ( res . body . Lyrics . length > 0 && res . body . Lyrics [ 0 ] . Start === undefined ) {
2023-10-05 05:02:42 +00:00
return res . body . Lyrics . map ( ( lyric ) = > lyric . Text ) . join ( '\n' ) ;
2023-07-01 19:10:05 -07:00
}
2023-06-03 00:39:50 -07:00
2023-07-01 19:10:05 -07:00
return res . body . Lyrics . map ( ( lyric ) = > [ lyric . Start ! / 1 e 4 , l y r i c . T e x t ] ) ;
2023-06-02 23:54:34 -07:00
} ;
2023-10-17 23:05:44 +00:00
const getSongDetail = async ( args : SongDetailArgs ) : Promise < SongDetailResponse > = > {
const { query , apiClientProps } = args ;
const res = await jfApiClient ( apiClientProps ) . getSongDetail ( {
params : {
id : query.id ,
userId : apiClientProps.server?.userId ? ? '' ,
} ,
} ) ;
if ( res . status !== 200 ) {
throw new Error ( 'Failed to get song detail' ) ;
}
return jfNormalize . song ( res . body , apiClientProps . server , '' ) ;
} ;
2024-04-22 19:44:10 -07:00
const VERSION_INFO : VersionInfo = [ [ '10.9.0' , { [ ServerFeature . LYRICS_SINGLE_STRUCTURED ] : [ 1 ] } ] ] ;
2024-02-01 08:17:31 -08:00
const getServerInfo = async ( args : ServerInfoArgs ) : Promise < ServerInfo > = > {
const { apiClientProps } = args ;
const res = await jfApiClient ( apiClientProps ) . getServerInfo ( ) ;
if ( res . status !== 200 ) {
2024-02-03 21:22:03 -08:00
throw new Error ( 'Failed to get server info' ) ;
2024-02-01 08:17:31 -08:00
}
2024-04-22 19:44:10 -07:00
const features = getFeatures ( VERSION_INFO , res . body . Version ) ;
2024-03-03 22:15:49 -08:00
return {
features ,
id : apiClientProps.server?.id ,
version : res.body.Version ,
} ;
2024-02-01 08:17:31 -08:00
} ;
2024-02-19 08:53:50 -08:00
const getSimilarSongs = async ( args : SimilarSongsArgs ) : Promise < Song [ ] > = > {
const { apiClientProps , query } = args ;
2024-04-08 08:49:55 -07:00
// Prefer getSimilarSongs, where possible. Fallback to InstantMix
// where no similar songs were found.
2024-02-19 08:53:50 -08:00
const res = await jfApiClient ( apiClientProps ) . getSimilarSongs ( {
params : {
2024-02-19 09:55:37 -08:00
itemId : query.songId ,
2024-02-19 08:53:50 -08:00
} ,
query : {
Fields : 'Genres, DateCreated, MediaSources, ParentId' ,
Limit : query.count ,
UserId : apiClientProps.server?.userId || undefined ,
} ,
} ) ;
2024-04-08 08:49:55 -07: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 , '' ) ) ;
}
return acc ;
} , [ ] ) ;
if ( results . length > 0 ) {
return results ;
}
}
const mix = await jfApiClient ( apiClientProps ) . getInstantMix ( {
params : {
itemId : query.songId ,
} ,
query : {
Fields : 'Genres, DateCreated, MediaSources, ParentId' ,
Limit : query.count ,
UserId : apiClientProps.server?.userId || undefined ,
} ,
} ) ;
if ( mix . status !== 200 ) {
2024-02-19 08:56:06 -08:00
throw new Error ( 'Failed to get similar songs' ) ;
2024-02-19 08:53:50 -08:00
}
2024-04-08 08:49:55 -07:00
return mix . body . Items . reduce < Song [ ] > ( ( acc , song ) = > {
2024-02-19 10:09:05 -08:00
if ( song . Id !== query . songId ) {
acc . push ( jfNormalize . song ( song , apiClientProps . server , '' ) ) ;
}
return acc ;
} , [ ] ) ;
2024-02-19 08:53:50 -08:00
} ;
2024-08-25 15:21:56 -07:00
const movePlaylistItem = async ( args : MoveItemArgs ) : Promise < void > = > {
const { apiClientProps , query } = args ;
const res = await jfApiClient ( apiClientProps ) . movePlaylistItem ( {
body : null ,
params : {
itemId : query.trackId ,
newIdx : query.endingIndex.toString ( ) ,
playlistId : query.playlistId ,
} ,
} ) ;
if ( res . status !== 204 ) {
throw new Error ( 'Failed to move item in playlist' ) ;
}
} ;
2023-05-08 03:34:15 -07:00
export const jfController = {
2023-07-01 19:10:05 -07:00
addToPlaylist ,
authenticate ,
createFavorite ,
createPlaylist ,
deleteFavorite ,
deletePlaylist ,
getAlbumArtistDetail ,
getAlbumArtistList ,
getAlbumDetail ,
getAlbumList ,
getArtistList ,
getGenreList ,
getLyrics ,
getMusicFolderList ,
getPlaylistDetail ,
getPlaylistList ,
getPlaylistSongList ,
getRandomSongList ,
2024-02-01 08:17:31 -08:00
getServerInfo ,
2024-02-19 08:53:50 -08:00
getSimilarSongs ,
2023-10-17 23:05:44 +00:00
getSongDetail ,
2023-07-01 19:10:05 -07:00
getSongList ,
getTopSongList ,
2024-08-25 15:21:56 -07:00
movePlaylistItem ,
2023-07-01 19:10:05 -07:00
removeFromPlaylist ,
scrobble ,
search ,
updatePlaylist ,
2023-05-08 03:34:15 -07:00
} ;