Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions packages/server_core/lib/server_core.dart
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,18 @@ export 'src/diagnostics/server_log_sink.dart';
export 'src/network/configure_server_dio.dart';
export 'src/network/auth_header.dart';
export 'src/network/redirect_interceptor.dart';
export 'src/network/server_interceptors.dart';
export 'src/network/server_probe.dart';
export 'src/server_dialect.dart';
export 'src/impl/server_auth_api.dart';
export 'src/impl/server_display_preferences_api.dart';
export 'src/impl/server_image_api.dart';
export 'src/impl/server_instant_mix_api.dart';
export 'src/impl/server_items_api.dart';
export 'src/impl/server_live_tv_api.dart';
export 'src/impl/server_playback_api.dart';
export 'src/impl/server_session_api.dart';
export 'src/impl/server_system_api.dart';
export 'src/impl/server_user_library_api.dart';
export 'src/impl/server_user_views_api.dart';
export 'src/impl/server_users_api.dart';
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
import 'package:dio/dio.dart';
import 'package:server_core/server_core.dart';

class JellyfinAuthApi implements AuthApi {
import '../api/auth_api.dart';
import '../server_dialect.dart';

class ServerAuthApi implements AuthApi {
final Dio _dio;
final ServerDialect _dialect;

ServerAuthApi(this._dio, this._dialect);

JellyfinAuthApi(this._dio);
void _requireQuickConnect() {
if (!_dialect.supportsQuickConnect) {
throw UnsupportedError(
'QuickConnect is not supported on this server',
);
}
}

@override
Future<Map<String, dynamic>> authenticateByName(
Expand All @@ -20,12 +31,14 @@ class JellyfinAuthApi implements AuthApi {

@override
Future<Map<String, dynamic>> initiateQuickConnect() async {
_requireQuickConnect();
final response = await _dio.post('/QuickConnect/Initiate');
return response.data as Map<String, dynamic>;
}

@override
Future<Map<String, dynamic>> checkQuickConnect(String secret) async {
_requireQuickConnect();
final response = await _dio.get(
'/QuickConnect/Connect',
queryParameters: {'Secret': secret},
Expand All @@ -35,6 +48,7 @@ class JellyfinAuthApi implements AuthApi {

@override
Future<bool> authorizeQuickConnect(String code, {String? userId}) async {
_requireQuickConnect();
final response = await _dio.post(
'/QuickConnect/Authorize',
queryParameters: {
Expand All @@ -49,6 +63,7 @@ class JellyfinAuthApi implements AuthApi {
Future<Map<String, dynamic>> authenticateWithQuickConnect(
String secret,
) async {
_requireQuickConnect();
final response = await _dio.post(
'/Users/AuthenticateWithQuickConnect',
data: {'Secret': secret},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,25 @@
import 'package:dio/dio.dart';
import 'package:server_core/server_core.dart';

class EmbyDisplayPreferencesApi implements DisplayPreferencesApi {
import '../api/display_preferences_api.dart';
import '../models/system_models.dart';
import '../server_dialect.dart';

class ServerDisplayPreferencesApi implements DisplayPreferencesApi {
final Dio _dio;
final ServerDialect _dialect;
final Map<String, _CacheEntry> _cache = {};
static const _cacheDuration = Duration(minutes: 5);

EmbyDisplayPreferencesApi(this._dio);
ServerDisplayPreferencesApi(this._dio, this._dialect);

String _client(String? client) => client ?? _dialect.displayPrefsClient;

@override
Future<DisplayPreferences> getDisplayPreferences(
String id, {
String? client,
}) async {
final cacheKey = '$id:${client ?? 'emby'}';
final cacheKey = '$id:${_client(client)}';
final entry = _cache[cacheKey];
if (entry != null &&
DateTime.now().difference(entry.cachedAt) < _cacheDuration) {
Expand All @@ -22,9 +28,7 @@ class EmbyDisplayPreferencesApi implements DisplayPreferencesApi {

final response = await _dio.get(
'/DisplayPreferences/$id',
queryParameters: {
'client': client ?? 'emby',
},
queryParameters: {'client': _client(client)},
);
final prefs =
DisplayPreferences.fromJson(response.data as Map<String, dynamic>);
Expand All @@ -41,12 +45,9 @@ class EmbyDisplayPreferencesApi implements DisplayPreferencesApi {
await _dio.post(
'/DisplayPreferences/$id',
data: prefs.toJson(),
queryParameters: {
'client': client ?? 'emby',
},
queryParameters: {'client': _client(client)},
);
final cacheKey = '$id:${client ?? 'emby'}';
_cache[cacheKey] = _CacheEntry(prefs, DateTime.now());
_cache['$id:${_client(client)}'] = _CacheEntry(prefs, DateTime.now());
}

void invalidateCache() => _cache.clear();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import 'package:server_core/server_core.dart';
import '../api/image_api.dart';
import '../server_dialect.dart';

class EmbyImageApi implements ImageApi {
class ServerImageApi implements ImageApi {
final String Function() _getBaseUrl;
final String? Function() _getApiKey;
final ServerDialect _dialect;

EmbyImageApi(this._getBaseUrl, this._getApiKey);
ServerImageApi(this._getBaseUrl, this._getApiKey, this._dialect);

String _buildQuery(Map<String, String> params) {
final apiKey = _getApiKey();
if (apiKey != null) params['api_key'] = apiKey;
if (_dialect.includeApiKeyInImageUrls) {
final apiKey = _getApiKey();
if (apiKey != null) params['api_key'] = apiKey;
}
if (params.isEmpty) return '';
return '?${params.entries.map((e) => '${Uri.encodeComponent(e.key)}=${Uri.encodeComponent(e.value)}').join('&')}';
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import 'package:dio/dio.dart';
import 'package:server_core/server_core.dart';

class JellyfinInstantMixApi implements InstantMixApi {
import '../api/instant_mix_api.dart';

/// Shared instant-mix implementation; the endpoint is identical on Jellyfin
/// and Emby.
class ServerInstantMixApi implements InstantMixApi {
final Dio _dio;

JellyfinInstantMixApi(this._dio);
ServerInstantMixApi(this._dio);

@override
Future<Map<String, dynamic>> getInstantMix(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import 'package:dio/dio.dart';
import 'package:server_core/server_core.dart';
import 'jellyfin_item_fields.dart';

class JellyfinItemsApi implements ItemsApi {
import '../api/items_api.dart';
import '../server_dialect.dart';

class ServerItemsApi implements ItemsApi {
final Dio _dio;
final ServerDialect _dialect;
final String Function() _getUserId;

JellyfinItemsApi(this._dio, this._getUserId);
ServerItemsApi(this._dio, this._dialect, this._getUserId);

bool _shouldRetryCollectionFallback(int statusCode) {
return statusCode == 400 ||
Expand Down Expand Up @@ -126,23 +128,29 @@ class JellyfinItemsApi implements ItemsApi {
String? fields,
}) async {
final userId = _getUserId();
final effectiveFields = fields ?? _dialect.defaultItemFields;
final response = await _dio.get(
'/Users/$userId/Items/$itemId',
queryParameters: {
'Fields': fields ?? kItemFields,
if (mediaSourceId != null) 'mediaSourceId': mediaSourceId,
if (effectiveFields != null && effectiveFields.isNotEmpty)
'Fields': effectiveFields,
'mediaSourceId': ?mediaSourceId,
},
);
return response.data as Map<String, dynamic>;
}

@override
Future<List<Map<String, dynamic>>> getAncestors(String itemId) async {
final response = await _dio.get('/Items/$itemId/Ancestors');
return ((response.data as List?) ?? const [])
.whereType<Map>()
.map((e) => e.cast<String, dynamic>())
.toList(growable: false);
try {
final response = await _dio.get('/Items/$itemId/Ancestors');
return ((response.data as List?) ?? const [])
.whereType<Map>()
.map((e) => e.cast<String, dynamic>())
.toList(growable: false);
} catch (_) {
return const [];
}
}

@override
Expand Down Expand Up @@ -194,7 +202,7 @@ class JellyfinItemsApi implements ItemsApi {
int? imageTypeLimit,
}) async {
final response = await _dio.get(
'/UserItems/Resume',
_dialect.resumePath(_getUserId()),
queryParameters: {
'ParentId': ?parentId,
if (includeItemTypes != null)
Expand All @@ -218,7 +226,7 @@ class JellyfinItemsApi implements ItemsApi {
int? imageTypeLimit,
}) async {
final response = await _dio.get(
'/Items/Latest',
_dialect.latestPath(_getUserId()),
queryParameters: {
'ParentId': ?parentId,
if (includeItemTypes != null)
Expand All @@ -229,6 +237,8 @@ class JellyfinItemsApi implements ItemsApi {
'ImageTypeLimit': ?imageTypeLimit,
},
);
// /Items/Latest returns a bare array, so normalize it into the
// same shape as /Items so all callers stay unchanged.
final data = response.data;
if (data is List) return {'Items': data, 'TotalRecordCount': data.length};
return data as Map<String, dynamic>;
Expand All @@ -243,9 +253,8 @@ class JellyfinItemsApi implements ItemsApi {
String? enableImageTypes,
int? imageTypeLimit,
}) async {
final userId = _getUserId();
final response = await _dio.get(
'/Users/$userId/Items',
_dialect.recentlyReleasedPath(_getUserId()),
queryParameters: {
'ParentId': ?parentId,
if (includeItemTypes != null)
Expand All @@ -256,7 +265,7 @@ class JellyfinItemsApi implements ItemsApi {
'ImageTypeLimit': ?imageTypeLimit,
'SortBy': 'PremiereDate',
'SortOrder': 'Descending',
'MaxPremiereDate': DateTime.now().toUtc().toIso8601String(),
'MaxPremiereDate': DateTime.now().toUtc().toIso8601String(),
},
);
final data = response.data;
Expand Down Expand Up @@ -290,15 +299,18 @@ class JellyfinItemsApi implements ItemsApi {
}) async {
final response = await _dio.get(
'/Items/$itemId/ThemeMedia',
queryParameters: {'InheritFromParent': inheritFromParent},
queryParameters: {
if (_dialect.themeMediaNeedsUserId) 'UserId': _getUserId(),
'InheritFromParent': inheritFromParent,
},
);
return response.data as Map<String, dynamic>;
}

@override
Future<Map<String, dynamic>> getPlaylists() async {
final response = await _dio.get(
'/Items',
_dialect.playlistsPath(_getUserId()),
queryParameters: {
'IncludeItemTypes': 'Playlist',
'Recursive': true,
Expand Down Expand Up @@ -411,8 +423,8 @@ class JellyfinItemsApi implements ItemsApi {
final queryParameters = <String, dynamic>{
'name': name,
'Name': name,
if (ids != null) 'ids': ids,
if (ids != null) 'Ids': ids,
'ids': ?ids,
'Ids': ?ids,
};

Response<dynamic> response;
Expand Down Expand Up @@ -560,6 +572,7 @@ class JellyfinItemsApi implements ItemsApi {

@override
Future<Map<String, dynamic>> getLyrics(String itemId) async {
if (!_dialect.supportsLyrics) return const {'Lyrics': []};
final response = await _dio.get('/Audio/$itemId/Lyrics');
return response.data as Map<String, dynamic>;
}
Expand Down Expand Up @@ -595,16 +608,21 @@ class JellyfinItemsApi implements ItemsApi {

@override
Future<List<Map<String, dynamic>>> getSpecialFeatures(String itemId) async {
final response = await _dio.get('/Items/$itemId/SpecialFeatures');
final data = response.data;
if (data is List) {
return data.cast<Map<String, dynamic>>();
try {
final response = await _dio.get('/Items/$itemId/SpecialFeatures');
final data = response.data;
if (data is List) {
return data.cast<Map<String, dynamic>>();
}
return const [];
} catch (_) {
return const [];
}
return const [];
}

@override
Future<List<Map<String, dynamic>>> getMediaSegments(String itemId) async {
if (!_dialect.supportsMediaSegments) return const [];
final response = await _dio.get('/MediaSegments/$itemId');
final data = response.data as Map<String, dynamic>;
final items = data['Items'] as List? ?? [];
Expand All @@ -617,6 +635,11 @@ class JellyfinItemsApi implements ItemsApi {
required String language,
bool? isPerfectMatch,
}) async {
if (!_dialect.supportsRemoteSubtitleSearch) {
throw UnsupportedError(
'Remote subtitle search is only supported for Jellyfin servers.',
);
}
final response = await _dio.get(
'/Items/$itemId/RemoteSearch/Subtitles/$language',
queryParameters: {'IsPerfectMatch': ?isPerfectMatch},
Expand All @@ -629,6 +652,11 @@ class JellyfinItemsApi implements ItemsApi {

@override
Future<void> downloadRemoteSubtitle(String itemId, String subtitleId) async {
if (!_dialect.supportsRemoteSubtitleSearch) {
throw UnsupportedError(
'Remote subtitle download is only supported for Jellyfin servers.',
);
}
await _dio.post('/Items/$itemId/RemoteSearch/Subtitles/$subtitleId');
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import 'package:dio/dio.dart';
import 'package:server_core/server_core.dart';

class EmbyLiveTvApi implements LiveTvApi {
import '../api/live_tv_api.dart';

/// Shared Live TV implementation; the endpoints are identical on Jellyfin
/// and Emby.
class ServerLiveTvApi implements LiveTvApi {
final Dio _dio;
static const _postChannelIdsThreshold = 1800;

EmbyLiveTvApi(this._dio);
ServerLiveTvApi(this._dio);

@override
Future<Map<String, dynamic>> getChannels({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import 'package:dio/dio.dart';
import 'package:server_core/server_core.dart';

class EmbyPlaybackApi implements PlaybackApi {
import '../api/playback_api.dart';

class ServerPlaybackApi implements PlaybackApi {
final Dio _dio;
final String Function() _getBaseUrl;

EmbyPlaybackApi(this._dio, this._getBaseUrl);
ServerPlaybackApi(this._dio, this._getBaseUrl);

@override
Future<void> reportPlaybackStart(Map<String, dynamic> info) async {
Expand Down
Loading