457 lines
16 KiB
Dart
457 lines
16 KiB
Dart
import 'dart:io' show Platform;
|
|
|
|
import 'package:android_id/android_id.dart';
|
|
import 'package:chopper/chopper.dart';
|
|
import 'package:device_info_plus/device_info_plus.dart';
|
|
import 'package:finamp/services/http_aggregate_logging_interceptor.dart';
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:get_it/get_it.dart';
|
|
import 'package:package_info_plus/package_info_plus.dart';
|
|
|
|
import '../models/jellyfin_models.dart';
|
|
import 'finamp_user_helper.dart';
|
|
import 'jellyfin_api_helper.dart';
|
|
|
|
part 'jellyfin_api.chopper.dart';
|
|
|
|
const String defaultFields =
|
|
"ChildCount,DateCreated,DateLastMediaAdded,Etag,Genres,ParentId,ProviderIds,Tags";
|
|
|
|
@ChopperApi()
|
|
abstract class JellyfinApi extends ChopperService {
|
|
@FactoryConverter(
|
|
request: JsonConverter.requestFactory,
|
|
response: JsonConverter.responseFactory,
|
|
)
|
|
@Get(path: "/Users/Public")
|
|
Future<dynamic> getPublicUsers();
|
|
|
|
@FactoryConverter(
|
|
request: JsonConverter.requestFactory,
|
|
response: JsonConverter.responseFactory,
|
|
)
|
|
@Post(path: "/Users/AuthenticateByName")
|
|
Future<dynamic> authenticateViaName(
|
|
@Body() Map<String, String> usernameAndPassword);
|
|
|
|
@FactoryConverter(
|
|
request: JsonConverter.requestFactory,
|
|
response: JsonConverter.responseFactory,
|
|
)
|
|
@Get(path: "/Items/{id}/Images/Primary")
|
|
Future<dynamic> getAlbumPrimaryImage({
|
|
@Path() required String id,
|
|
@Query() String format = "webp",
|
|
});
|
|
|
|
@FactoryConverter(
|
|
request: JsonConverter.requestFactory,
|
|
response: JsonConverter.responseFactory,
|
|
)
|
|
@Get(path: "/Users/{id}/Views")
|
|
Future<dynamic> getViews(@Path() String id);
|
|
|
|
@FactoryConverter(
|
|
request: JsonConverter.requestFactory,
|
|
response: JsonConverter.responseFactory,
|
|
)
|
|
@Get(path: "/Users/{userId}/Items")
|
|
Future<dynamic> getItems({
|
|
/// The user id supplied as query parameter.
|
|
@Path() required String userId,
|
|
|
|
/// Optional. If specified, results will be filtered based on the item type.
|
|
/// This allows multiple, comma delimeted.
|
|
@Query("IncludeItemTypes") String? includeItemTypes,
|
|
|
|
/// Specify this to localize the search to a specific item or folder. Omit
|
|
/// to use the root.
|
|
@Query("ParentId") String? parentId,
|
|
|
|
/// Optional. If specified, results will be filtered to include only those
|
|
/// containing the specified album artist id.
|
|
@Query("AlbumArtistIds") String? albumArtistIds,
|
|
|
|
/// Optional. If specified, results will be filtered to include only those
|
|
/// containing the specified artist id.
|
|
@Query("ArtistIds") String? artistIds,
|
|
|
|
/// Optional. If specified, results will be filtered to include only those
|
|
/// containing the specified album id.
|
|
@Query("AlbumIds") String? albumIds,
|
|
|
|
/// When searching within folders, this determines whether or not the search
|
|
/// will be recursive. true/false.
|
|
@Query("Recursive") bool? recursive,
|
|
|
|
/// Optional. Specify one or more sort orders, comma delimited. Options:
|
|
/// Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating,
|
|
/// DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear,
|
|
/// SortName, Random, Revenue, Runtime.
|
|
@Query("SortBy") String? sortBy,
|
|
|
|
/// Items Enum: "Ascending" "Descending"
|
|
/// Sort Order - Ascending,Descending.
|
|
@Query("SortOrder") String? sortOrder,
|
|
|
|
/// Items Enum: "AirTime" "CanDelete" "CanDownload" "ChannelInfo" "Chapters"
|
|
/// "ChildCount" "CumulativeRunTimeTicks" "CustomRating" "DateCreated"
|
|
/// "DateLastMediaAdded" "DisplayPreferencesId" "Etag" "ExternalUrls"
|
|
/// "Genres" "HomePageUrl" "ItemCounts" "MediaSourceCount" "MediaSources"
|
|
/// "OriginalTitle" "Overview" "ParentId" "Path" "People" "PlayAccess"
|
|
/// "ProductionLocations" "ProviderIds" "PrimaryImageAspectRatio"
|
|
/// "RecursiveItemCount" "Settings" "ScreenshotImageTags"
|
|
/// "SeriesPrimaryImage" "SeriesStudio" "SortName" "SpecialEpisodeNumbers"
|
|
/// "Studios" "BasicSyncInfo" "SyncInfo" "Taglines" "Tags" "RemoteTrailers"
|
|
/// "MediaStreams" "SeasonUserData" "ServiceName" "ThemeSongIds"
|
|
/// "ThemeVideoIds" "ExternalEtag" "PresentationUniqueKey"
|
|
/// "InheritedParentalRatingValue" "ExternalSeriesId"
|
|
/// "SeriesPresentationUniqueKey" "DateLastRefreshed" "DateLastSaved"
|
|
/// "RefreshState" "ChannelImage" "EnableMediaSourceDisplay" "Width"
|
|
/// "Height" "ExtraIds" "LocalTrailerCount" "IsHD" "SpecialFeatureCount"
|
|
@Query("Fields") String? fields = defaultFields,
|
|
|
|
/// Optional. Filter based on a search term.
|
|
@Query("SearchTerm") String? searchTerm,
|
|
|
|
/// Optional. If specified, results will be filtered based on genre id. This
|
|
/// allows multiple, pipe delimited.
|
|
@Query("GenreIds") String? genreIds,
|
|
|
|
/// Items Enum: "IsFolder" "IsNotFolder" "IsUnplayed" "IsPlayed"
|
|
/// "IsFavorite" "IsResumable" "Likes" "Dislikes" "IsFavoriteOrLikes"
|
|
/// Optional. Specify additional filters to apply. This allows multiple,
|
|
/// comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed,
|
|
/// IsFavorite, IsResumable, Likes, Dislikes.
|
|
@Query("Filters") String? filters,
|
|
|
|
/// Optional. The record index to start at. All items with a lower index
|
|
/// will be dropped from the results.
|
|
@Query("StartIndex") int? startIndex,
|
|
|
|
/// Optional. The maximum number of records to return.
|
|
@Query("Limit") int? limit,
|
|
});
|
|
|
|
@FactoryConverter(
|
|
request: JsonConverter.requestFactory,
|
|
response: JsonConverter.responseFactory,
|
|
)
|
|
@Get(path: "/Items/{id}/InstantMix")
|
|
Future<dynamic> getInstantMix({
|
|
@Path() required String id,
|
|
@Query() required String userId,
|
|
@Query() required int limit,
|
|
});
|
|
|
|
@FactoryConverter(
|
|
request: JsonConverter.requestFactory,
|
|
response: JsonConverter.responseFactory,
|
|
)
|
|
@Get(path: "/Users/{userId}/Items/{itemId}")
|
|
Future<dynamic> getItemById({
|
|
/// User id.
|
|
@Path() required String userId,
|
|
|
|
/// Item id.
|
|
@Path() required String itemId,
|
|
});
|
|
|
|
@FactoryConverter(
|
|
request: JsonConverter.requestFactory,
|
|
response: JsonConverter.responseFactory,
|
|
)
|
|
@Get(path: "/Items/{id}/PlaybackInfo")
|
|
Future<dynamic> getPlaybackInfo({
|
|
@Path() required String id,
|
|
@Query() required String userId,
|
|
});
|
|
|
|
@FactoryConverter(
|
|
request: JsonConverter.requestFactory,
|
|
)
|
|
@Post(path: "/Items/{itemId}")
|
|
Future<dynamic> updateItem({
|
|
/// The item id.
|
|
@Path() required String itemId,
|
|
@Body() required BaseItemDto newItem,
|
|
});
|
|
|
|
@FactoryConverter(request: JsonConverter.requestFactory)
|
|
@Post(path: "/Sessions/Playing")
|
|
Future<dynamic> startPlayback(
|
|
@Body() PlaybackProgressInfo playbackProgressInfo);
|
|
|
|
@FactoryConverter(request: JsonConverter.requestFactory)
|
|
@Post(path: "/Sessions/Playing/Progress")
|
|
Future<dynamic> playbackStatusUpdate(
|
|
@Body() PlaybackProgressInfo playbackProgressInfo);
|
|
|
|
@FactoryConverter(request: JsonConverter.requestFactory)
|
|
@Post(path: "/Sessions/Playing/Stopped")
|
|
Future<dynamic> playbackStatusStopped(
|
|
@Body() PlaybackProgressInfo playbackProgressInfo);
|
|
|
|
@FactoryConverter(
|
|
request: JsonConverter.requestFactory,
|
|
response: JsonConverter.responseFactory,
|
|
)
|
|
@Get(path: "/Playlists/{playlistId}/Items")
|
|
Future<dynamic> getPlaylistItems({
|
|
@Path() required String playlistId,
|
|
@Query("UserId") required String userId,
|
|
@Query("IncludeItemTypes") String? includeItemTypes,
|
|
@Query("ParentId") String? parentId,
|
|
@Query("Recursive") bool? recursive,
|
|
@Query("Fields") String? fields = defaultFields,
|
|
});
|
|
|
|
/// Creates a new playlist.
|
|
@FactoryConverter(
|
|
request: JsonConverter.requestFactory,
|
|
response: JsonConverter.responseFactory,
|
|
)
|
|
@Post(path: "/Playlists")
|
|
Future<dynamic> createNewPlaylist({
|
|
/// The create playlist payload.
|
|
@Body() required NewPlaylist newPlaylist,
|
|
});
|
|
|
|
/// Adds items to a playlist.
|
|
@FactoryConverter(request: JsonConverter.requestFactory)
|
|
@Post(path: "/Playlists/{playlistId}/Items", optionalBody: true)
|
|
Future<Response> addItemsToPlaylist({
|
|
/// The playlist id.
|
|
@Path() required String playlistId,
|
|
|
|
/// Item id, comma delimited.
|
|
@Query() String? ids,
|
|
|
|
/// The userId.
|
|
@Query() String? userId,
|
|
});
|
|
|
|
/// Remove items from a playlist.
|
|
@FactoryConverter(request: JsonConverter.requestFactory)
|
|
@Delete(path: "/Playlists/{playlistId}/Items", optionalBody: true)
|
|
Future<Response> removeItemsFromPlaylist({
|
|
/// The playlist id.
|
|
@Path() required String playlistId,
|
|
|
|
/// Item id, comma delimited.
|
|
@Query() String? entryIds,
|
|
});
|
|
|
|
@FactoryConverter(
|
|
request: JsonConverter.requestFactory,
|
|
response: JsonConverter.responseFactory,
|
|
)
|
|
@Get(path: "/Artists/AlbumArtists")
|
|
Future<dynamic> getAlbumArtists({
|
|
@Query("IncludeItemTypes") String? includeItemTypes,
|
|
@Query("ParentId") String? parentId,
|
|
@Query("Recursive") bool? recursive,
|
|
|
|
/// Optional. Specify one or more sort orders, comma delimited. Options:
|
|
/// Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating,
|
|
/// DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear,
|
|
/// SortName, Random, Revenue, Runtime.
|
|
@Query("SortBy") String? sortBy,
|
|
|
|
/// Items Enum: "Ascending" "Descending"
|
|
/// Sort Order - Ascending,Descending.
|
|
@Query("SortOrder") String? sortOrder,
|
|
@Query("Fields") String? fields = defaultFields,
|
|
@Query("SearchTerm") String? searchTerm,
|
|
@Query("EnableUserData") bool enableUserData = true,
|
|
|
|
/// Items Enum: "IsFolder" "IsNotFolder" "IsUnplayed" "IsPlayed"
|
|
/// "IsFavorite" "IsResumable" "Likes" "Dislikes" "IsFavoriteOrLikes"
|
|
/// Optional. Specify additional filters to apply.
|
|
@Query("Filters") String? filters,
|
|
|
|
/// Optional. The record index to start at. All items with a lower index
|
|
/// will be dropped from the results.
|
|
@Query("StartIndex") int? startIndex,
|
|
|
|
/// Optional. The maximum number of records to return.
|
|
@Query("Limit") int? limit,
|
|
|
|
/// User id. Technically nullable in the Jellyfin API docs, but getting
|
|
/// favourited artists will break if this is not given.
|
|
@Query("UserId") required String userId,
|
|
});
|
|
|
|
/// Gets all genres from a given item, folder, or the entire library.
|
|
@FactoryConverter(
|
|
request: JsonConverter.requestFactory,
|
|
response: JsonConverter.responseFactory,
|
|
)
|
|
@Get(path: "/Genres")
|
|
Future<dynamic> getGenres({
|
|
/// Optional. If specified, results will be filtered based on the item type.
|
|
/// This allows multiple, comma delimeted.
|
|
@Query("IncludeItemTypes") String? includeItemTypes,
|
|
|
|
/// Specify this to localize the search to a specific item or folder. Omit
|
|
/// to use the root.
|
|
@Query("ParentId") String? parentId,
|
|
|
|
/// Items Enum: "AirTime" "CanDelete" "CanDownload" "ChannelInfo" "Chapters"
|
|
/// "ChildCount" "CumulativeRunTimeTicks" "CustomRating" "DateCreated"
|
|
/// "DateLastMediaAdded" "DisplayPreferencesId" "Etag" "ExternalUrls"
|
|
/// "Genres" "HomePageUrl" "ItemCounts" "MediaSourceCount" "MediaSources"
|
|
/// "OriginalTitle" "Overview" "ParentId" "Path" "People" "PlayAccess"
|
|
/// "ProductionLocations" "ProviderIds" "PrimaryImageAspectRatio"
|
|
/// "RecursiveItemCount" "Settings" "ScreenshotImageTags"
|
|
/// "SeriesPrimaryImage" "SeriesStudio" "SortName" "SpecialEpisodeNumbers"
|
|
/// "Studios" "BasicSyncInfo" "SyncInfo" "Taglines" "Tags" "RemoteTrailers"
|
|
/// "MediaStreams" "SeasonUserData" "ServiceName" "ThemeSongIds"
|
|
/// "ThemeVideoIds" "ExternalEtag" "PresentationUniqueKey"
|
|
/// "InheritedParentalRatingValue" "ExternalSeriesId"
|
|
/// "SeriesPresentationUniqueKey" "DateLastRefreshed" "DateLastSaved"
|
|
/// "RefreshState" "ChannelImage" "EnableMediaSourceDisplay" "Width"
|
|
/// "Height" "ExtraIds" "LocalTrailerCount" "IsHD" "SpecialFeatureCount"
|
|
@Query("Fields") String? fields = defaultFields,
|
|
|
|
/// Optional. Filter based on a search term.
|
|
@Query("SearchTerm") String? searchTerm,
|
|
|
|
/// Optional. The record index to start at. All items with a lower index
|
|
/// will be dropped from the results.
|
|
@Query("StartIndex") int? startIndex,
|
|
|
|
/// Optional. The maximum number of records to return.
|
|
@Query("Limit") int? limit,
|
|
});
|
|
|
|
/// Marks an item as a favorite.
|
|
@FactoryConverter(
|
|
request: JsonConverter.requestFactory,
|
|
response: JsonConverter.responseFactory,
|
|
)
|
|
@Post(path: "/Users/{userId}/FavoriteItems/{itemId}", optionalBody: true)
|
|
Future<dynamic> addFavourite({
|
|
/// User id.
|
|
@Path() required String userId,
|
|
|
|
/// Item id.
|
|
@Path() required String itemId,
|
|
});
|
|
|
|
/// Unmarks item as a favorite.
|
|
@FactoryConverter(
|
|
request: JsonConverter.requestFactory,
|
|
response: JsonConverter.responseFactory,
|
|
)
|
|
@Delete(path: "/Users/{userId}/FavoriteItems/{itemId}")
|
|
Future<dynamic> removeFavourite({
|
|
/// User id.
|
|
@Path() required String userId,
|
|
|
|
/// Item id.
|
|
@Path() required String itemId,
|
|
});
|
|
|
|
/// Reports that a session has ended.
|
|
@FactoryConverter(
|
|
request: JsonConverter.requestFactory,
|
|
)
|
|
@Post(path: "/Sessions/Logout", optionalBody: true)
|
|
Future<dynamic> logout();
|
|
|
|
static JellyfinApi create() {
|
|
final client = ChopperClient(
|
|
// The first part of the URL is now here
|
|
services: [
|
|
// The generated implementation
|
|
_$JellyfinApi(),
|
|
],
|
|
// Converts data to & from JSON and adds the application/json header.
|
|
// converter: JsonConverter(),
|
|
interceptors: [
|
|
/// Gets baseUrl from SharedPreferences.
|
|
(Request request) async {
|
|
final jellyfinApiHelper = GetIt.instance<JellyfinApiHelper>();
|
|
final finampUserHelper = GetIt.instance<FinampUserHelper>();
|
|
|
|
String authHeader = await getAuthHeader();
|
|
|
|
// If baseUrlTemp is null, use the baseUrl of the current user.
|
|
// If baseUrlTemp is set, we're setting up a new user and should use it instead.
|
|
Uri baseUri = jellyfinApiHelper.baseUrlTemp ??
|
|
Uri.parse(finampUserHelper.currentUser!.baseUrl);
|
|
|
|
// Add the request path on to the baseUrl
|
|
baseUri = baseUri.replace(
|
|
pathSegments:
|
|
baseUri.pathSegments.followedBy(request.uri.pathSegments));
|
|
|
|
return request.copyWith(
|
|
uri: baseUri,
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"Authorization": authHeader,
|
|
},
|
|
);
|
|
},
|
|
|
|
HttpAggregateLoggingInterceptor(),
|
|
],
|
|
);
|
|
|
|
// The generated class with the ChopperClient passed in
|
|
return _$JellyfinApi(client);
|
|
}
|
|
}
|
|
|
|
/// Creates the Authorization header
|
|
Future<String> getAuthHeader() async {
|
|
final notAsciiRegex = RegExp(r'[^\x00-\x7F]+');
|
|
|
|
final finampUserHelper = GetIt.instance<FinampUserHelper>();
|
|
|
|
String authHeader = "MediaBrowser ";
|
|
|
|
if (finampUserHelper.currentUser != null) {
|
|
authHeader = '${authHeader}UserId="${finampUserHelper.currentUser!.id}", ';
|
|
}
|
|
|
|
if (finampUserHelper.currentUser?.accessToken != null) {
|
|
authHeader =
|
|
'${authHeader}Token="${finampUserHelper.currentUser!.accessToken}", ';
|
|
}
|
|
|
|
authHeader = '${authHeader}Client="Finamp", ';
|
|
if (kIsWeb) {
|
|
authHeader = '${authHeader}Device="Web Browser", ';
|
|
authHeader = '${authHeader}DeviceId="finamp-web", ';
|
|
} else if (Platform.isAndroid) {
|
|
DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
|
|
AndroidDeviceInfo androidDeviceInfo = await deviceInfo.androidInfo;
|
|
authHeader = '${authHeader}Device="${androidDeviceInfo.model}", ';
|
|
final androidId = await const AndroidId().getId();
|
|
authHeader = '${authHeader}DeviceId="$androidId", ';
|
|
} else if (Platform.isIOS) {
|
|
DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
|
|
IosDeviceInfo iosDeviceInfo = await deviceInfo.iosInfo;
|
|
authHeader = '${authHeader}Device="${iosDeviceInfo.name}", ';
|
|
authHeader =
|
|
'${authHeader}DeviceId="${iosDeviceInfo.identifierForVendor}", ';
|
|
} else {
|
|
final deviceName = "Finamp ${Platform.operatingSystem}";
|
|
final deviceId =
|
|
"finamp-${Platform.operatingSystem}-${Platform.localHostname}";
|
|
|
|
authHeader = '${authHeader}Device="$deviceName", ';
|
|
authHeader = '${authHeader}DeviceId="$deviceId", ';
|
|
}
|
|
|
|
PackageInfo packageInfo = await PackageInfo.fromPlatform();
|
|
authHeader = '${authHeader}Version="${packageInfo.version}"';
|
|
|
|
// In some cases non-ASCII characters can end up in the header, usually via
|
|
// iOS device name
|
|
return authHeader.replaceAll(notAsciiRegex, "_");
|
|
}
|