Files
sm-emby-share/lib/features/aggregated/aggregated_grouping.dart
T
2026-07-14 11:11:36 +08:00

273 lines
6.9 KiB
Dart

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<AggregatedEntrySource> sources;
AggregatedEntry({
required this.item,
required this.source,
List<AggregatedEntrySource>? 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<AggregatedEntry> entries;
const AggregatedGroup({
required this.key,
required this.label,
required this.order,
required this.entries,
});
}
List<AggregatedEntry> flatten(
List<GlobalSearchServerResult> results, {
String? serverFilter,
bool deduplicateAcrossServers = false,
}) {
final out = <AggregatedEntry>[];
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 = <String, ({String label, int order})>{
'Movie': (label: '电影', order: 0),
'Series': (label: '剧集', order: 1),
'Episode': (label: '单集', order: 2),
};
const _typeOther = (label: '其它', order: 3);
List<AggregatedGroup> groupEntries(
List<AggregatedEntry> entries,
GroupAxis axis,
DateTime now,
) {
if (axis == GroupAxis.mediaType) {
return _groupByType(entries);
}
return _groupByTime(entries, axis, now);
}
List<AggregatedGroup> _groupByTime(
List<AggregatedEntry> entries,
GroupAxis axis,
DateTime now,
) {
final scheme = axis == GroupAxis.watchedTime
? TimeBucketScheme.watched
: TimeBucketScheme.added;
final buckets =
<String, ({TimeBucket bucket, List<AggregatedEntry> 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<AggregatedGroup> _groupByType(List<AggregatedEntry> entries) {
final groups =
<String, ({String label, int order, List<AggregatedEntry> 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<String> _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<AggregatedEntry> _deduplicateEntries(List<AggregatedEntry> entries) {
final uf = _UnionFind(entries.length);
final keyToIndex = <String, int>{};
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 = <AggregatedEntry>[];
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 = <String>{};
final sources = <AggregatedEntrySource>[];
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<int> _parent;
final List<int> _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<List<int>> groups() {
final map = <int, List<int>>{};
for (var i = 0; i < _parent.length; i++) {
(map[find(i)] ??= []).add(i);
}
return map.values.toList();
}
}