Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix caching of Youtube API pagination + filter duplicate videoIds. #8472

Merged
merged 3 commits into from
Jan 16, 2025
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 48 additions & 39 deletions app/lib/service/youtube/backend.dart
Original file line number Diff line number Diff line change
Expand Up @@ -121,54 +121,63 @@ class _PkgOfWeekVideoFetcher {
final youtube = YouTubeApi(apiClient);

try {
final videos = <PkgOfWeekVideo>[];
final pageTokensVisited = <String>{};
String? nextPageToken;
for (var check = true; check && videos.length < 50;) {
final rs = await cache.youtubePlaylistItems().get(

final videos = <PkgOfWeekVideo>[];
final videoIds = <String>{};
while (videos.length < 50) {
// get page from cache or from Youtube API
final rs = await cache.youtubePlaylistItems(nextPageToken ?? '').get(
() async => await youtube.playlistItems.list(
['snippet', 'contentDetails'],
playlistId: powPlaylistId,
pageToken: nextPageToken,
),
);
videos.addAll(rs!.items!.map(
(i) {
try {
final videoId = i.contentDetails?.videoId;
if (videoId == null) {
return null;
}
final thumbnails = i.snippet?.thumbnails;
if (thumbnails == null) {
return null;
}
final thumbnail = thumbnails.high ??
thumbnails.default_ ??
thumbnails.maxres ??
thumbnails.standard ??
thumbnails.medium;
final thumbnailUrl = thumbnail?.url;
if (thumbnailUrl == null || thumbnailUrl.isEmpty) {
return null;
}
return PkgOfWeekVideo(
videoId: videoId,
title: i.snippet?.title ?? '',
description:
(i.snippet?.description ?? '').trim().split('\n').first,
thumbnailUrl: thumbnailUrl,
);
} catch (e, st) {
// this item will be skipped, the rest of the list may be displayed
_logger.pubNoticeShout(
'youtube', 'Processing Youtube PlaylistItem failed.', e, st);

// process playlist items
for (final i in rs!.items!) {
try {
final videoId = i.contentDetails?.videoId;
if (videoId == null || videoIds.contains(videoId)) {
continue;
}
final thumbnails = i.snippet?.thumbnails;
if (thumbnails == null) {
continue;
}
final thumbnail = thumbnails.high ??
thumbnails.default_ ??
thumbnails.maxres ??
thumbnails.standard ??
thumbnails.medium;
final thumbnailUrl = thumbnail?.url;
if (thumbnailUrl == null || thumbnailUrl.isEmpty) {
continue;
}
return null;
},
).nonNulls);
// next page
videoIds.add(videoId);
videos.add(PkgOfWeekVideo(
videoId: videoId,
title: i.snippet?.title ?? '',
description:
(i.snippet?.description ?? '').trim().split('\n').first,
thumbnailUrl: thumbnailUrl,
));
} catch (e, st) {
// this item will be skipped, the rest of the list may be displayed
_logger.pubNoticeShout(
'youtube', 'Processing Youtube PlaylistItem failed.', e, st);
}
}

pageTokensVisited.add(nextPageToken ?? '');
// advance to next page token
nextPageToken = rs.nextPageToken;
check = nextPageToken != null && nextPageToken.isNotEmpty;
if (nextPageToken == null ||
pageTokensVisited.contains(nextPageToken)) {
isoos marked this conversation as resolved.
Show resolved Hide resolved
break;
}
}
return videos;
} finally {
Expand Down
21 changes: 11 additions & 10 deletions app/lib/shared/redis_cache.dart
Original file line number Diff line number Diff line change
Expand Up @@ -457,16 +457,17 @@ class CachePatterns {
ResolvedDocUrlVersion.fromJson(v as Map<String, dynamic>),
))['$package-$version'];

Entry<PlaylistItemListResponse> youtubePlaylistItems() => _cache
.withPrefix('youtube/playlist-item-list-response/')
.withTTL(Duration(hours: 6))
.withCodec(utf8)
.withCodec(json)
.withCodec(wrapAsCodec(
encode: (PlaylistItemListResponse v) => v.toJson(),
decode: (v) =>
PlaylistItemListResponse.fromJson(v as Map<String, dynamic>),
))[''];
Entry<PlaylistItemListResponse> youtubePlaylistItems(String pageToken) =>
_cache
.withPrefix('youtube/playlist-item-list-response/')
.withTTL(Duration(hours: 6))
.withCodec(utf8)
.withCodec(json)
.withCodec(wrapAsCodec(
encode: (PlaylistItemListResponse v) => v.toJson(),
decode: (v) =>
PlaylistItemListResponse.fromJson(v as Map<String, dynamic>),
))[pageToken];
}

/// The active cache.
Expand Down
4 changes: 2 additions & 2 deletions app/test/service/youtube/backend_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,12 @@ void main() {
});

test('selectRandomVideos', () {
final random = Random(123);
final items = <int>[0, 1, 2, 3, 4, 5, 6, 7, 9, 10];

for (var i = 0; i < 1000; i++) {
final selected = selectRandomVideos(random, items, 4);
final selected = selectRandomVideos(Random(i), items, 4);

expect(selected, hasLength(4));
expect(selected.first, 0);
expect(selected[1], greaterThan(0));
expect(selected[1], lessThan(4));
Expand Down
Loading