Light theme default + yt-dlp search fallback
This commit is contained in:
@@ -0,0 +1,240 @@
|
||||
import '../models/channel_rank_item.dart';
|
||||
import '../models/subscription_channel.dart';
|
||||
import '../models/video_item.dart';
|
||||
import 'hidden_channels_service.dart';
|
||||
import 'library_service.dart';
|
||||
import 'youtube_service.dart';
|
||||
|
||||
class ChannelCatalogService {
|
||||
ChannelCatalogService._();
|
||||
|
||||
static final ChannelCatalogService instance =
|
||||
ChannelCatalogService._();
|
||||
|
||||
final YoutubeService _youtube = YoutubeService();
|
||||
|
||||
List<ChannelRankItem>? _cache;
|
||||
String? _cacheFingerprint;
|
||||
DateTime? _cacheAt;
|
||||
|
||||
Future<List<ChannelRankItem>> load({
|
||||
bool force = false,
|
||||
}) async {
|
||||
await HiddenChannelsService.instance.ensureLoaded();
|
||||
|
||||
final history = LibraryService.instance.history.value;
|
||||
|
||||
if (history.isEmpty) {
|
||||
return const [];
|
||||
}
|
||||
|
||||
final fingerprint = <String>[
|
||||
'${HiddenChannelsService.instance.revision.value}',
|
||||
for (final video in history.take(100))
|
||||
'${video.id}:${LibraryService.instance.watchCountForVideo(video.id)}',
|
||||
].join('|');
|
||||
|
||||
final cacheFresh = _cacheAt != null &&
|
||||
DateTime.now().difference(_cacheAt!) <
|
||||
const Duration(minutes: 10);
|
||||
|
||||
if (!force &&
|
||||
cacheFresh &&
|
||||
_cache != null &&
|
||||
_cacheFingerprint == fingerprint) {
|
||||
return _cache!;
|
||||
}
|
||||
|
||||
final enriched = <VideoItem>[];
|
||||
|
||||
// History is unique by video ID, but personal repeat counts are stored
|
||||
// separately. Resolve old entries created before channelId existed.
|
||||
for (var start = 0; start < history.length; start += 6) {
|
||||
final end = start + 6 < history.length
|
||||
? start + 6
|
||||
: history.length;
|
||||
|
||||
final chunk = history.sublist(start, end);
|
||||
|
||||
final resolved = await Future.wait(
|
||||
chunk.map(
|
||||
(video) async {
|
||||
if (video.channelId != null &&
|
||||
video.channelId!.isNotEmpty) {
|
||||
return video;
|
||||
}
|
||||
|
||||
try {
|
||||
return await _youtube.getVideoDetails(video.id);
|
||||
} catch (_) {
|
||||
return video;
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
enriched.addAll(resolved);
|
||||
}
|
||||
|
||||
final groups = <String, _ChannelAccumulator>{};
|
||||
|
||||
for (var index = 0; index < enriched.length; index++) {
|
||||
final video = enriched[index];
|
||||
var channelId = video.channelId?.trim() ?? '';
|
||||
SubscriptionChannel? resolvedChannel;
|
||||
|
||||
if (channelId.isEmpty) {
|
||||
try {
|
||||
resolvedChannel =
|
||||
await _youtube.getChannelForVideo(video.id);
|
||||
channelId = resolvedChannel.channelId;
|
||||
} catch (_) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (HiddenChannelsService.instance.isHiddenChannel(
|
||||
channelId: channelId,
|
||||
title: resolvedChannel?.title ?? video.author,
|
||||
)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final personalViews =
|
||||
LibraryService.instance.watchCountForVideo(video.id);
|
||||
|
||||
final safePersonalViews = personalViews > 0 ? personalViews : 1;
|
||||
|
||||
groups
|
||||
.putIfAbsent(
|
||||
channelId,
|
||||
() => _ChannelAccumulator(
|
||||
channelId: channelId,
|
||||
fallbackTitle: resolvedChannel?.title ?? video.author,
|
||||
),
|
||||
)
|
||||
.add(
|
||||
video,
|
||||
personalViews: safePersonalViews,
|
||||
historyIndex: index,
|
||||
channelOverride: resolvedChannel,
|
||||
);
|
||||
}
|
||||
|
||||
final accumulators = groups.values.toList();
|
||||
final result = <ChannelRankItem>[];
|
||||
|
||||
for (var start = 0; start < accumulators.length; start += 6) {
|
||||
final end = start + 6 < accumulators.length
|
||||
? start + 6
|
||||
: accumulators.length;
|
||||
|
||||
final chunk = accumulators.sublist(start, end);
|
||||
|
||||
final resolved = await Future.wait(
|
||||
chunk.map(
|
||||
(group) async {
|
||||
SubscriptionChannel channel;
|
||||
|
||||
if (group.channelOverride != null) {
|
||||
channel = group.channelOverride!;
|
||||
} else {
|
||||
try {
|
||||
channel = await _youtube.getChannel(group.channelId);
|
||||
} catch (_) {
|
||||
channel = SubscriptionChannel(
|
||||
channelId: group.channelId,
|
||||
title: group.fallbackTitle,
|
||||
thumbnailUrl: '',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (HiddenChannelsService.instance.isHiddenChannel(
|
||||
channelId: channel.channelId,
|
||||
title: channel.title,
|
||||
)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return ChannelRankItem(
|
||||
channel: channel,
|
||||
personalViewCount: group.personalViewCount,
|
||||
distinctVideoCount: group.distinctVideoCount,
|
||||
topVideoPersonalViews: group.topVideoPersonalViews,
|
||||
topVideoTitle: group.topVideoTitle,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
result.addAll(resolved.whereType<ChannelRankItem>());
|
||||
}
|
||||
|
||||
result.sort(
|
||||
(a, b) {
|
||||
final views = b.personalViewCount.compareTo(a.personalViewCount);
|
||||
if (views != 0) {
|
||||
return views;
|
||||
}
|
||||
|
||||
final distinct =
|
||||
b.distinctVideoCount.compareTo(a.distinctVideoCount);
|
||||
if (distinct != 0) {
|
||||
return distinct;
|
||||
}
|
||||
|
||||
return a.channel.title
|
||||
.toLowerCase()
|
||||
.compareTo(b.channel.title.toLowerCase());
|
||||
},
|
||||
);
|
||||
|
||||
_cache = List.unmodifiable(result);
|
||||
_cacheFingerprint = fingerprint;
|
||||
_cacheAt = DateTime.now();
|
||||
|
||||
return _cache!;
|
||||
}
|
||||
}
|
||||
|
||||
class _ChannelAccumulator {
|
||||
_ChannelAccumulator({
|
||||
required this.channelId,
|
||||
required this.fallbackTitle,
|
||||
});
|
||||
|
||||
final String channelId;
|
||||
final String fallbackTitle;
|
||||
|
||||
SubscriptionChannel? channelOverride;
|
||||
|
||||
int personalViewCount = 0;
|
||||
int distinctVideoCount = 0;
|
||||
int topVideoPersonalViews = 0;
|
||||
int newestHistoryIndex = 1 << 30;
|
||||
String? topVideoTitle;
|
||||
|
||||
void add(
|
||||
VideoItem video, {
|
||||
required int personalViews,
|
||||
required int historyIndex,
|
||||
SubscriptionChannel? channelOverride,
|
||||
}) {
|
||||
if (channelOverride != null) {
|
||||
this.channelOverride = channelOverride;
|
||||
}
|
||||
|
||||
personalViewCount += personalViews;
|
||||
distinctVideoCount++;
|
||||
|
||||
if (historyIndex < newestHistoryIndex) {
|
||||
newestHistoryIndex = historyIndex;
|
||||
}
|
||||
|
||||
if (personalViews >= topVideoPersonalViews) {
|
||||
topVideoPersonalViews = personalViews;
|
||||
topVideoTitle = video.title;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user