import 'package:chopper/chopper.dart'; import 'package:get_it/get_it.dart'; import 'package:logging/logging.dart'; import 'finamp_user_helper.dart'; import 'jellyfin_api.dart'; import '../models/finamp_models.dart'; import '../models/jellyfin_models.dart'; class JellyfinApiHelper { final jellyfinApi = JellyfinApi.create(); final _jellyfinApiHelperLogger = Logger("JellyfinApiHelper"); // Stores the ids of the artists that the user selected to mix List selectedMixArtistsIds = []; // Stores the ids of albums that the user selected to mix List selectedMixAlbumIds = []; Uri? baseUrlTemp; final _finampUserHelper = GetIt.instance(); Future?> getItems({ BaseItemDto? parentItem, String? includeItemTypes, String? sortBy, String? sortOrder, String? searchTerm, required bool isGenres, String? filters, /// The record index to start at. All items with a lower index will be /// dropped from the results. int? startIndex, /// The maximum number of records to return. int? limit, }) async { final currentUser = _finampUserHelper.currentUser; if (currentUser == null) { // When logging out, this request causes errors since currentUser is // required sometimes. We just return an empty list since this error // usually happens becuase the listeners on MusicScreenTabView update // milliseconds before the page is popped. This shouldn't happen in normal // use. return []; } var response; // We send a different request for playlists so that we get them back in the // right order. Doing this in the same function makes sense since they both // return the same thing. It also means we can easily share album widgets // with playlists. if (parentItem?.type == "Playlist") { response = await jellyfinApi.getPlaylistItems( playlistId: parentItem!.id, userId: currentUser.id, parentId: parentItem.id, includeItemTypes: includeItemTypes, recursive: true, ); } else if (includeItemTypes == "MusicArtist") { // For artists, we need to use a different endpoint response = await jellyfinApi.getAlbumArtists( parentId: parentItem?.id, recursive: true, sortBy: sortBy, sortOrder: sortOrder, searchTerm: searchTerm, filters: filters, startIndex: startIndex, limit: limit, userId: currentUser.id, ); } else if (parentItem?.type == "MusicArtist") { // For getting the children of artists, we need to use albumArtistIds // instead of parentId response = await jellyfinApi.getItems( userId: currentUser.id, albumArtistIds: parentItem?.id, includeItemTypes: includeItemTypes, recursive: true, sortBy: sortBy, sortOrder: sortOrder, searchTerm: searchTerm, filters: filters, startIndex: startIndex, limit: limit, ); } else if (includeItemTypes == "MusicGenre") { response = await jellyfinApi.getGenres( parentId: parentItem?.id, // includeItemTypes: includeItemTypes, searchTerm: searchTerm, startIndex: startIndex, limit: limit, ); } else if (parentItem?.type == "MusicGenre") { response = await jellyfinApi.getItems( userId: currentUser.id, genreIds: parentItem?.id, includeItemTypes: includeItemTypes, recursive: true, sortBy: sortBy, sortOrder: sortOrder, searchTerm: searchTerm, filters: filters, startIndex: startIndex, limit: limit, ); } else { // This will be run when getting albums, songs in albums, and stuff like // that. response = await jellyfinApi.getItems( userId: currentUser.id, parentId: parentItem?.id, includeItemTypes: includeItemTypes, recursive: true, sortBy: sortBy, sortOrder: sortOrder, searchTerm: searchTerm, filters: filters, startIndex: startIndex, limit: limit, ); } return (QueryResult_BaseItemDto.fromJson(response).items); } /// Authenticates a user and saves the login details Future authenticateViaName({ required String username, String? password, }) async { var response; // Some users won't have a password. if (password == null) { response = await jellyfinApi.authenticateViaName({"Username": username}); } else { response = await jellyfinApi .authenticateViaName({"Username": username, "Pw": password}); } AuthenticationResult newUserAuthenticationResult = AuthenticationResult.fromJson(response); FinampUser newUser = FinampUser( id: newUserAuthenticationResult.user!.id, baseUrl: baseUrlTemp!.toString(), accessToken: newUserAuthenticationResult.accessToken!, serverId: newUserAuthenticationResult.serverId!, views: {}, ); await _finampUserHelper.saveUser(newUser); } /// Gets all the user's views. Future> getViews() async { var response = await jellyfinApi.getViews(_finampUserHelper.currentUser!.id); return QueryResult_BaseItemDto.fromJson(response).items!; } /// Gets the playback info for an item, such as format and bitrate. Usually, I'd require a BaseItemDto as an argument /// but since this will be run inside of [MusicPlayerBackgroundTask], I've just set the raw id as an argument. Future?> getPlaybackInfo(String itemId) async { var response = await jellyfinApi.getPlaybackInfo( id: itemId, userId: _finampUserHelper.currentUser!.id, ); // getPlaybackInfo returns a PlaybackInfoResponse. We only need the List in it so we convert it here and // return that List. final PlaybackInfoResponse decodedResponse = PlaybackInfoResponse.fromJson(response); return decodedResponse.mediaSources; } /// Starts an instant mix using the data from the item provided. Future?> getInstantMix(BaseItemDto? parentItem) async { var response = await jellyfinApi.getInstantMix( id: parentItem!.id, userId: _finampUserHelper.currentUser!.id, limit: 200); return (QueryResult_BaseItemDto.fromJson(response).items); } /// Tells the Jellyfin server that playback has started Future reportPlaybackStart( PlaybackProgressInfo playbackProgressInfo) async { await jellyfinApi.startPlayback(playbackProgressInfo); } /// Updates player progress so that Jellyfin can track what we're listening to Future updatePlaybackProgress( PlaybackProgressInfo playbackProgressInfo) async { await jellyfinApi.playbackStatusUpdate(playbackProgressInfo); } /// Tells Jellyfin that we've stopped listening to music (called when the audio service is stopped) Future stopPlaybackProgress( PlaybackProgressInfo playbackProgressInfo) async { await jellyfinApi.playbackStatusStopped(playbackProgressInfo); } /// Gets an item from a user's library. Future getItemById(String itemId) async { var response = await jellyfinApi.getItemById( userId: _finampUserHelper.currentUser!.id, itemId: itemId, ); return (BaseItemDto.fromJson(response)); } /// Creates a new playlist. Future createNewPlaylist(NewPlaylist newPlaylist) async { var response = await jellyfinApi.createNewPlaylist( newPlaylist: newPlaylist, ); return NewPlaylistResponse.fromJson(response); } /// Adds items to a playlist. Future addItemstoPlaylist({ /// The playlist id. required String playlistId, /// Item ids to add. List? ids, }) async { await jellyfinApi.addItemsToPlaylist( playlistId: playlistId, ids: ids?.join(","), ); } /// Remove items to a playlist. Future removeItemsFromPlaylist({ /// The playlist id. required String playlistId, /// Item ids to add. List? entryIds, }) async { await jellyfinApi.removeItemsFromPlaylist( playlistId: playlistId, entryIds: entryIds?.join(","), ); } /// Updates an item. Future updateItem({ /// The item id. required String itemId, /// What to update the item with. You should give a BaseItemDto with only /// changed values. required BaseItemDto newItem, }) async { await jellyfinApi.updateItem(itemId: itemId, newItem: newItem); } /// Marks an item as a favorite. Future addFavourite(String itemId) async { var response = await jellyfinApi.addFavourite( userId: _finampUserHelper.currentUser!.id, itemId: itemId); return UserItemDataDto.fromJson(response); } /// Unmarks item as a favorite. Future removeFavourite(String itemId) async { var response = await jellyfinApi.removeFavourite( userId: _finampUserHelper.currentUser!.id, itemId: itemId); return UserItemDataDto.fromJson(response); } void addArtistToMixBuilderList(BaseItemDto item) { selectedMixArtistsIds.add(item.id); } void removeArtistFromBuilderList(BaseItemDto item) { selectedMixArtistsIds.remove(item.id); } void addAlbumToMixBuilderList(BaseItemDto item) { selectedMixAlbumIds.add(item.id); } void removeAlbumFromBuilderList(BaseItemDto item) { selectedMixAlbumIds.remove(item.id); } Future?> getArtistMix(List artistIds) async { var response = await jellyfinApi.getItems( userId: _finampUserHelper.currentUser!.id, artistIds: artistIds.join(","), filters: "IsNotFolder", recursive: true, sortBy: "Random", limit: 300, fields: "Chapters"); return (QueryResult_BaseItemDto.fromJson(response).items); } Future?> getAlbumMix(List albumIds) async { var response = await jellyfinApi.getItems( userId: _finampUserHelper.currentUser!.id, albumIds: albumIds.join(","), filters: "IsNotFolder", recursive: true, sortBy: "Random", limit: 300, fields: "Chapters"); return (QueryResult_BaseItemDto.fromJson(response).items); } /// Removes the current user from the DB and revokes the token on Jellyfin Future logoutCurrentUser() async { // We put this in a try-catch loop that basically ignores errors so that the // user can still log out during scenarios like wrong IP, no internet etc. try { await jellyfinApi.logout().timeout( const Duration(seconds: 3), onTimeout: () => _jellyfinApiHelperLogger.warning( "Logout request timed out. Logging out anyway, but be aware that Jellyfin may have not got the signal."), ); } catch (e) { _jellyfinApiHelperLogger.warning( "Jellyfin logout failed. Logging out anyway, but be aware that Jellyfin may have not got the signal.", e); } finally { // If we're unauthorised, the logout command will fail but we're already // basically logged out so we shouldn't fail. _finampUserHelper.removeUser(_finampUserHelper.currentUser!.id); } } /// Returns the correct image URL for the given item, or null if there is no /// image. Uses [getImageId] to get the actual id. [maxWidth] and [maxHeight] /// can be specified to return a smaller image. [quality] can be modified to /// get a higher/lower quality image. Uri? getImageUrl({ required BaseItemDto item, int? maxWidth, int? maxHeight, int? quality = 90, String? format = "jpg", }) { if (item.imageId == null) { return null; } final parsedBaseUrl = Uri.parse(_finampUserHelper.currentUser!.baseUrl); List builtPath = List.from(parsedBaseUrl.pathSegments); builtPath.addAll([ "Items", item.imageId!, "Images", "Primary", ]); return Uri( host: parsedBaseUrl.host, port: parsedBaseUrl.port, scheme: parsedBaseUrl.scheme, userInfo: parsedBaseUrl.userInfo, pathSegments: builtPath, queryParameters: { if (format != null) "format": format, if (quality != null) "quality": quality.toString(), if (maxWidth != null) "MaxWidth": maxWidth.toString(), if (maxHeight != null) "MaxHeight": maxHeight.toString(), }); } }