Files

347 lines
9.8 KiB
Dart
Raw Permalink Normal View History

2026-07-14 11:11:36 +08:00
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,
);
},
);
}
}