library; import '../../core/contracts/library.dart'; import '../../shared/utils/time_bucket.dart'; enum GroupAxis { watchedTime, addedTime, mediaType } class AggregatedEntrySource { final EmbyRawItem item; final GlobalSearchServerResult source; const AggregatedEntrySource({required this.item, required this.source}); DateTime? dateFor(GroupAxis axis) { switch (axis) { case GroupAxis.watchedTime: final userData = item.extra['UserData']; return parseEmbyDate( userData is Map ? userData['LastPlayedDate'] : null, ); case GroupAxis.addedTime: return parseEmbyDate(item.extra['DateCreated']); case GroupAxis.mediaType: return null; } } } class AggregatedEntry { final EmbyRawItem item; final GlobalSearchServerResult source; final List sources; AggregatedEntry({ required this.item, required this.source, List? sources, }) : assert(sources == null || sources.isNotEmpty), sources = List.unmodifiable( sources ?? [AggregatedEntrySource(item: item, source: source)], ); DateTime? dateFor(GroupAxis axis) { return sources.first.dateFor(axis); } } class AggregatedGroup { final String key; final String label; final int order; final List entries; const AggregatedGroup({ required this.key, required this.label, required this.order, required this.entries, }); } List flatten( List results, { String? serverFilter, bool deduplicateAcrossServers = false, }) { final out = []; for (final sr in results) { if (serverFilter != null && sr.serverId != serverFilter) continue; for (final item in sr.items) { out.add(AggregatedEntry(item: item, source: sr)); } } if (deduplicateAcrossServers && serverFilter == null && out.length > 1) { return _deduplicateEntries(out); } return out; } const _typeOrder = { 'Movie': (label: '电影', order: 0), 'Series': (label: '剧集', order: 1), 'Episode': (label: '单集', order: 2), }; const _typeOther = (label: '其它', order: 3); List groupEntries( List entries, GroupAxis axis, DateTime now, ) { if (axis == GroupAxis.mediaType) { return _groupByType(entries); } return _groupByTime(entries, axis, now); } List _groupByTime( List entries, GroupAxis axis, DateTime now, ) { final scheme = axis == GroupAxis.watchedTime ? TimeBucketScheme.watched : TimeBucketScheme.added; final buckets = items})>{}; for (final e in entries) { final b = bucketFor(e.dateFor(axis), now, scheme); (buckets[b.key] ??= (bucket: b, items: [])).items.add(e); } final groups = buckets.values.map((v) { final items = [...v.items] ..sort((a, b) { final da = a.dateFor(axis); final db = b.dateFor(axis); if (da == null && db == null) return 0; if (da == null) return 1; if (db == null) return -1; return db.compareTo(da); }); return AggregatedGroup( key: v.bucket.key, label: v.bucket.label, order: v.bucket.order, entries: items, ); }).toList()..sort((a, b) => a.order.compareTo(b.order)); return groups; } List _groupByType(List entries) { final groups = items})>{}; for (final e in entries) { final meta = _typeOrder[e.item.Type] ?? _typeOther; final key = e.item.Type ?? 'Other'; (groups[key] ??= ( label: meta.label, order: meta.order, items: [], )).items.add(e); } final out = groups.entries.map((kv) { final items = [...kv.value.items] ..sort((a, b) => (a.item.Name).compareTo(b.item.Name)); return AggregatedGroup( key: kv.key, label: kv.value.label, order: kv.value.order, entries: items, ); }).toList()..sort((a, b) => a.order.compareTo(b.order)); return out; } Set _candidateKeys(EmbyRawItem item) { final type = item.Type; if (type == null || type.isEmpty) return const {}; final ids = normalizedProviderIdsOfItem(item); if (ids.isEmpty) return const {}; return {for (final e in ids.entries) '$type:${e.key}:${e.value}'}; } List _deduplicateEntries(List entries) { final uf = _UnionFind(entries.length); final keyToIndex = {}; for (var i = 0; i < entries.length; i++) { final keys = _candidateKeys(entries[i].item); for (final k in keys) { final prev = keyToIndex[k]; if (prev != null) { uf.union(prev, i); } else { keyToIndex[k] = i; } } } final groups = uf.groups(); final survivors = []; for (final group in groups) { final orderedIndices = [...group] ..sort((leftIndex, rightIndex) { final leftDate = _lastPlayedDate(entries[leftIndex]); final rightDate = _lastPlayedDate(entries[rightIndex]); if (leftDate == null && rightDate == null) { return leftIndex.compareTo(rightIndex); } if (leftDate == null) return 1; if (rightDate == null) return -1; final dateComparison = rightDate.compareTo(leftDate); return dateComparison != 0 ? dateComparison : leftIndex.compareTo(rightIndex); }); final sourceServerIds = {}; final sources = []; for (final entryIndex in orderedIndices) { final entry = entries[entryIndex]; if (!sourceServerIds.add(entry.source.serverId)) continue; sources.add( AggregatedEntrySource(item: entry.item, source: entry.source), ); } final primarySource = sources.first; survivors.add( AggregatedEntry( item: primarySource.item, source: primarySource.source, sources: sources, ), ); } return survivors; } DateTime? _lastPlayedDate(AggregatedEntry e) => e.dateFor(GroupAxis.watchedTime); class _UnionFind { final List _parent; final List _rank; _UnionFind(int n) : _parent = List.generate(n, (i) => i), _rank = List.filled(n, 0); int find(int x) { if (_parent[x] != x) _parent[x] = find(_parent[x]); return _parent[x]; } void union(int a, int b) { final ra = find(a), rb = find(b); if (ra == rb) return; if (_rank[ra] < _rank[rb]) { _parent[ra] = rb; } else if (_rank[ra] > _rank[rb]) { _parent[rb] = ra; } else { _parent[rb] = ra; _rank[ra]++; } } List> groups() { final map = >{}; for (var i = 0; i < _parent.length; i++) { (map[find(i)] ??= []).add(i); } return map.values.toList(); } }