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: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 getPublicUsers(); @FactoryConverter( request: JsonConverter.requestFactory, response: JsonConverter.responseFactory, ) @Post(path: "/Users/AuthenticateByName") Future authenticateViaName( @Body() Map usernameAndPassword); @FactoryConverter( request: JsonConverter.requestFactory, response: JsonConverter.responseFactory, ) @Get(path: "/Items/{id}/Images/Primary") Future getAlbumPrimaryImage({ @Path() required String id, @Query() String format = "webp", }); @FactoryConverter( request: JsonConverter.requestFactory, response: JsonConverter.responseFactory, ) @Get(path: "/Users/{id}/Views") Future getViews(@Path() String id); @FactoryConverter( request: JsonConverter.requestFactory, response: JsonConverter.responseFactory, ) @Get(path: "/Users/{userId}/Items") Future 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 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 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 getPlaybackInfo({ @Path() required String id, @Query() required String userId, }); @FactoryConverter( request: JsonConverter.requestFactory, ) @Post(path: "/Items/{itemId}") Future updateItem({ /// The item id. @Path() required String itemId, @Body() required BaseItemDto newItem, }); @FactoryConverter(request: JsonConverter.requestFactory) @Post(path: "/Sessions/Playing") Future startPlayback( @Body() PlaybackProgressInfo playbackProgressInfo); @FactoryConverter(request: JsonConverter.requestFactory) @Post(path: "/Sessions/Playing/Progress") Future playbackStatusUpdate( @Body() PlaybackProgressInfo playbackProgressInfo); @FactoryConverter(request: JsonConverter.requestFactory) @Post(path: "/Sessions/Playing/Stopped") Future playbackStatusStopped( @Body() PlaybackProgressInfo playbackProgressInfo); @FactoryConverter( request: JsonConverter.requestFactory, response: JsonConverter.responseFactory, ) @Get(path: "/Playlists/{playlistId}/Items") Future 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 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 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 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 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 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 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 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 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(); final finampUserHelper = GetIt.instance(); 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 getAuthHeader() async { final notAsciiRegex = RegExp(r'[^\x00-\x7F]+'); final finampUserHelper = GetIt.instance(); 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", '; DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); if (Platform.isAndroid) { AndroidDeviceInfo androidDeviceInfo = await deviceInfo.androidInfo; authHeader = '${authHeader}Device="${androidDeviceInfo.model}", '; final androidId = await const AndroidId().getId(); authHeader = '${authHeader}DeviceId="$androidId", '; } else if (Platform.isIOS) { IosDeviceInfo iosDeviceInfo = await deviceInfo.iosInfo; authHeader = '${authHeader}Device="${iosDeviceInfo.name}", '; authHeader = '${authHeader}DeviceId="${iosDeviceInfo.identifierForVendor}", '; } else { throw "getAuthHeader() only supports Android and iOS"; } 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, "_"); }