Files
Redesnigned-Finamp/lib/services/jellyfin_api_helper.dart
Zakaria 1563409cb1
Build / Build for Android (push) Has been cancelled
Build / Build for iOS (push) Has been cancelled
first commit
2026-05-18 14:15:38 -04:00

389 lines
12 KiB
Dart

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<String> selectedMixArtistsIds = [];
// Stores the ids of albums that the user selected to mix
List<String> selectedMixAlbumIds = [];
Uri? baseUrlTemp;
final _finampUserHelper = GetIt.instance<FinampUserHelper>();
Future<List<BaseItemDto>?> 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<void> 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<List<BaseItemDto>> 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<List<MediaSourceInfo>?> getPlaybackInfo(String itemId) async {
var response = await jellyfinApi.getPlaybackInfo(
id: itemId,
userId: _finampUserHelper.currentUser!.id,
);
// getPlaybackInfo returns a PlaybackInfoResponse. We only need the List<MediaSourceInfo> in it so we convert it here and
// return that List<MediaSourceInfo>.
final PlaybackInfoResponse decodedResponse =
PlaybackInfoResponse.fromJson(response);
return decodedResponse.mediaSources;
}
/// Starts an instant mix using the data from the item provided.
Future<List<BaseItemDto>?> 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<void> reportPlaybackStart(
PlaybackProgressInfo playbackProgressInfo) async {
await jellyfinApi.startPlayback(playbackProgressInfo);
}
/// Updates player progress so that Jellyfin can track what we're listening to
Future<void> 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<void> stopPlaybackProgress(
PlaybackProgressInfo playbackProgressInfo) async {
await jellyfinApi.playbackStatusStopped(playbackProgressInfo);
}
/// Gets an item from a user's library.
Future<BaseItemDto> getItemById(String itemId) async {
var response = await jellyfinApi.getItemById(
userId: _finampUserHelper.currentUser!.id,
itemId: itemId,
);
return (BaseItemDto.fromJson(response));
}
/// Creates a new playlist.
Future<NewPlaylistResponse> createNewPlaylist(NewPlaylist newPlaylist) async {
var response = await jellyfinApi.createNewPlaylist(
newPlaylist: newPlaylist,
);
return NewPlaylistResponse.fromJson(response);
}
/// Adds items to a playlist.
Future<void> addItemstoPlaylist({
/// The playlist id.
required String playlistId,
/// Item ids to add.
List<String>? ids,
}) async {
await jellyfinApi.addItemsToPlaylist(
playlistId: playlistId,
ids: ids?.join(","),
);
}
/// Remove items to a playlist.
Future<void> removeItemsFromPlaylist({
/// The playlist id.
required String playlistId,
/// Item ids to add.
List<String>? entryIds,
}) async {
await jellyfinApi.removeItemsFromPlaylist(
playlistId: playlistId,
entryIds: entryIds?.join(","),
);
}
/// Updates an item.
Future<void> 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<UserItemDataDto> 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<UserItemDataDto> 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<List<BaseItemDto>?> getArtistMix(List<String> 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<List<BaseItemDto>?> getAlbumMix(List<String> 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<void> 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<String> builtPath = List<String>.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(),
});
}
}