347 lines
9.8 KiB
Dart
347 lines
9.8 KiB
Dart
import 'dart:async';
|
|
|
|
import '../../../shared/utils/app_logger.dart';
|
|
import '../../contracts/contracts.dart';
|
|
import '../ports/emby_gateway.dart';
|
|
import '../ports/server_repository.dart';
|
|
import '../ports/session_repository.dart';
|
|
import 'usecase_helpers.dart';
|
|
|
|
const _ucTag = 'UseCase';
|
|
|
|
|
|
Stream<GlobalSearchServerResult> _fanOutPerServer({
|
|
required SessionRepository sessionRepository,
|
|
required ServerRepository serverRepository,
|
|
required Future<List<EmbyRawItem>?> Function(AuthedRequestContext context)
|
|
fetchItems,
|
|
required String logTag,
|
|
}) {
|
|
final controller = StreamController<GlobalSearchServerResult>();
|
|
() async {
|
|
try {
|
|
final allSessions = await sessionRepository.listAll();
|
|
final allServers = await serverRepository.list();
|
|
final serverMap = {for (final s in allServers) s.id: s};
|
|
|
|
final futures = allSessions.map((session) async {
|
|
final server = serverMap[session.serverId];
|
|
if (server == null) return;
|
|
final context = AuthedRequestContext(
|
|
baseUrl: server.baseUrl,
|
|
token: session.token,
|
|
userId: session.userId,
|
|
);
|
|
try {
|
|
final items = await fetchItems(context);
|
|
if (items == null || items.isEmpty) return;
|
|
controller.add(
|
|
GlobalSearchServerResult(
|
|
serverId: server.id,
|
|
serverName: server.name,
|
|
serverUrl: server.baseUrl,
|
|
token: session.token,
|
|
userId: session.userId,
|
|
items: items,
|
|
),
|
|
);
|
|
} catch (e) {
|
|
AppLogger.warn(_ucTag, '$logTag on ${server.name} failed', e);
|
|
}
|
|
});
|
|
|
|
await Future.wait(futures);
|
|
} catch (e) {
|
|
controller.addError(e);
|
|
} finally {
|
|
controller.close();
|
|
}
|
|
}();
|
|
return controller.stream;
|
|
}
|
|
|
|
class GetLibraryViewsUseCase extends AuthedUseCase {
|
|
GetLibraryViewsUseCase({
|
|
required super.sessionRepository,
|
|
required super.serverRepository,
|
|
required super.embyGateway,
|
|
});
|
|
|
|
Future<LibraryViewsRes> execute() async {
|
|
return embyGateway.getUserViews(await ctx());
|
|
}
|
|
}
|
|
|
|
class GetLibraryResumeUseCase extends AuthedUseCase {
|
|
GetLibraryResumeUseCase({
|
|
required super.sessionRepository,
|
|
required super.serverRepository,
|
|
required super.embyGateway,
|
|
});
|
|
|
|
Future<LibraryResumeRes> execute() async {
|
|
return embyGateway.getResumeItems(await ctx(), limit: 20);
|
|
}
|
|
}
|
|
|
|
class GetLibraryLatestUseCase extends AuthedUseCase {
|
|
GetLibraryLatestUseCase({
|
|
required super.sessionRepository,
|
|
required super.serverRepository,
|
|
required super.embyGateway,
|
|
});
|
|
|
|
Future<LibraryLatestRes> execute(
|
|
LibraryLatestReq input, {
|
|
CancellationToken? cancelToken,
|
|
}) async {
|
|
return embyGateway.getLatestItems(
|
|
await ctx(),
|
|
input.parentId,
|
|
limit: input.limit ?? 16,
|
|
cancelToken: cancelToken,
|
|
);
|
|
}
|
|
}
|
|
|
|
class GetLibraryItemsUseCase extends AuthedUseCase {
|
|
GetLibraryItemsUseCase({
|
|
required super.sessionRepository,
|
|
required super.serverRepository,
|
|
required super.embyGateway,
|
|
});
|
|
|
|
Future<LibraryItemsRes> execute(LibraryItemsReq input) async {
|
|
final items = await embyGateway.getLibraryItems(
|
|
ctx: await ctx(),
|
|
parentId: input.parentId,
|
|
includeItemTypes: input.includeItemTypes,
|
|
genreIds: input.genreIds,
|
|
searchTerm: input.searchTerm,
|
|
anyProviderIdEquals: input.anyProviderIdEquals,
|
|
fields: input.fields,
|
|
enableImageTypes: input.enableImageTypes,
|
|
groupProgramsBySeries: input.groupProgramsBySeries,
|
|
imageTypeLimit: input.imageTypeLimit,
|
|
startIndex: input.startIndex,
|
|
limit: input.limit,
|
|
recursive: input.recursive,
|
|
sortBy: input.sortBy,
|
|
sortOrder: input.sortOrder,
|
|
);
|
|
return LibraryItemsRes(
|
|
parentId: input.parentId ?? '__global__',
|
|
items: items,
|
|
);
|
|
}
|
|
}
|
|
|
|
|
|
class GetHomeBannerUseCase extends AuthedUseCase {
|
|
GetHomeBannerUseCase({
|
|
required super.sessionRepository,
|
|
required super.serverRepository,
|
|
required super.embyGateway,
|
|
});
|
|
|
|
Future<List<EmbyRawItem>> execute({int fetchLimit = 24, int take = 8}) async {
|
|
final items = await embyGateway.getLibraryItems(
|
|
ctx: await ctx(),
|
|
includeItemTypes: 'Movie,Series',
|
|
recursive: true,
|
|
sortBy: 'DateCreated',
|
|
sortOrder: SortOrder.descending,
|
|
fields:
|
|
'BasicSyncInfo,CommunityRating,ProductionYear,Overview,Genres,ProviderIds,UserData',
|
|
enableImageTypes: 'Backdrop,Logo,Primary,Thumb',
|
|
imageTypeLimit: 2,
|
|
limit: fetchLimit,
|
|
);
|
|
return items
|
|
.where((it) => (it.BackdropImageTags ?? const []).isNotEmpty)
|
|
.take(take)
|
|
.toList(growable: false);
|
|
}
|
|
}
|
|
|
|
class GlobalKeywordSearchUseCase extends AuthedUseCase {
|
|
GlobalKeywordSearchUseCase({
|
|
required super.sessionRepository,
|
|
required super.serverRepository,
|
|
required super.embyGateway,
|
|
});
|
|
|
|
static const _defaultTypes = 'Movie,Series';
|
|
|
|
Stream<GlobalSearchServerResult> executeStream(GlobalSearchReq input) {
|
|
final controller = StreamController<GlobalSearchServerResult>();
|
|
() async {
|
|
final keyword = input.keyword.trim();
|
|
if (keyword.isEmpty) {
|
|
controller.close();
|
|
return;
|
|
}
|
|
try {
|
|
var sessions = await sessionRepository.listAll();
|
|
if (input.serverId != null) {
|
|
sessions = sessions
|
|
.where((s) => s.serverId == input.serverId)
|
|
.toList();
|
|
}
|
|
final allServers = await serverRepository.list();
|
|
final serverMap = {for (final s in allServers) s.id: s};
|
|
final types = input.includeItemTypes ?? _defaultTypes;
|
|
|
|
final futures = sessions.map((session) async {
|
|
final result = await _searchOne(
|
|
session: session,
|
|
server: serverMap[session.serverId],
|
|
keyword: keyword,
|
|
types: types,
|
|
limit: input.limitPerServer ?? 30,
|
|
);
|
|
if (result != null) controller.add(result);
|
|
});
|
|
|
|
await Future.wait(futures);
|
|
} catch (error, stackTrace) {
|
|
controller.addError(error, stackTrace);
|
|
} finally {
|
|
controller.close();
|
|
}
|
|
}();
|
|
return controller.stream;
|
|
}
|
|
|
|
Future<GlobalSearchServerResult?> _searchOne({
|
|
required SessionData session,
|
|
required EmbyServer? server,
|
|
required String keyword,
|
|
required String types,
|
|
required int limit,
|
|
}) async {
|
|
if (server == null) return null;
|
|
final c = AuthedRequestContext(
|
|
baseUrl: server.baseUrl,
|
|
token: session.token,
|
|
userId: session.userId,
|
|
);
|
|
try {
|
|
final items = await embyGateway.getLibraryItems(
|
|
ctx: c,
|
|
searchTerm: keyword,
|
|
recursive: true,
|
|
includeItemTypes: types,
|
|
limit: limit,
|
|
sortBy: 'SortName',
|
|
sortOrder: SortOrder.ascending,
|
|
);
|
|
if (items.isEmpty) return null;
|
|
return GlobalSearchServerResult(
|
|
serverId: server.id,
|
|
serverName: server.name,
|
|
serverUrl: server.baseUrl,
|
|
token: session.token,
|
|
userId: session.userId,
|
|
items: items,
|
|
);
|
|
} catch (e) {
|
|
AppLogger.warn(_ucTag, 'GlobalKeywordSearch on ${server.name} failed', e);
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
|
|
class GetAggregatedResumeUseCase extends AuthedUseCase {
|
|
GetAggregatedResumeUseCase({
|
|
required super.sessionRepository,
|
|
required super.serverRepository,
|
|
required super.embyGateway,
|
|
});
|
|
|
|
Stream<GlobalSearchServerResult> executeStream({int? limitPerServer}) {
|
|
return _fanOutPerServer(
|
|
sessionRepository: sessionRepository,
|
|
serverRepository: serverRepository,
|
|
logTag: 'AggregatedResume',
|
|
fetchItems: (context) async {
|
|
final res = await embyGateway.getResumeItems(
|
|
context,
|
|
limit: limitPerServer ?? 20,
|
|
);
|
|
return res.Items;
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
class GetAggregatedFavoritesUseCase extends AuthedUseCase {
|
|
static const _defaultTypes = 'Movie,Series,Episode';
|
|
|
|
GetAggregatedFavoritesUseCase({
|
|
required super.sessionRepository,
|
|
required super.serverRepository,
|
|
required super.embyGateway,
|
|
});
|
|
|
|
Stream<GlobalSearchServerResult> executeStream({
|
|
String includeItemTypes = _defaultTypes,
|
|
int? limitPerServer,
|
|
}) {
|
|
final types = includeItemTypes.trim().isEmpty
|
|
? _defaultTypes
|
|
: includeItemTypes;
|
|
return _fanOutPerServer(
|
|
sessionRepository: sessionRepository,
|
|
serverRepository: serverRepository,
|
|
logTag: 'AggregatedFavorites',
|
|
fetchItems: (context) async {
|
|
final items = await embyGateway.getFavoriteItems(
|
|
ctx: context,
|
|
includeItemTypes: types,
|
|
sortBy: 'SortName',
|
|
sortOrder: SortOrder.ascending,
|
|
);
|
|
return limitPerServer == null
|
|
? items
|
|
: items.take(limitPerServer).toList(growable: false);
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
class GetAggregatedRecentUseCase extends AuthedUseCase {
|
|
static const _defaultTypes = 'Movie,Series';
|
|
|
|
GetAggregatedRecentUseCase({
|
|
required super.sessionRepository,
|
|
required super.serverRepository,
|
|
required super.embyGateway,
|
|
});
|
|
|
|
Stream<GlobalSearchServerResult> executeStream({
|
|
String includeItemTypes = _defaultTypes,
|
|
int? limitPerServer,
|
|
}) {
|
|
return _fanOutPerServer(
|
|
sessionRepository: sessionRepository,
|
|
serverRepository: serverRepository,
|
|
logTag: 'AggregatedRecent',
|
|
fetchItems: (context) async {
|
|
return embyGateway.getLibraryItems(
|
|
ctx: context,
|
|
includeItemTypes: includeItemTypes,
|
|
fields:
|
|
'BasicSyncInfo,CollectionType,PrimaryImageAspectRatio,UserData,'
|
|
'CommunityRating,ProviderIds,ProductionYear,ChildCount,Container,'
|
|
'CanDelete,DateCreated',
|
|
sortBy: 'DateCreated',
|
|
sortOrder: SortOrder.descending,
|
|
recursive: true,
|
|
limit: limitPerServer ?? 20,
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|