389 lines
12 KiB
Dart
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(),
|
|
});
|
|
}
|
|
}
|