Skip to content

Commit 485fb7c

Browse files
committed
Prefer cached feeds before refresh
1 parent f2d6e80 commit 485fb7c

2 files changed

Lines changed: 162 additions & 55 deletions

File tree

src/services/FeedCacheService.ts

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -90,22 +90,24 @@ export function getCachedEpisodes(
9090
feed: PodcastFeed,
9191
maxAgeMs: number = DEFAULT_TTL_MS,
9292
): Episode[] | null {
93-
const store = loadCache();
94-
const cacheKey = getFeedKey(feed);
95-
const cachedValue = store[cacheKey];
93+
const cachedEpisodesWithStatus = getCachedEpisodesWithStatus(
94+
feed,
95+
maxAgeMs,
96+
);
9697

97-
if (!cachedValue) {
98+
if (!cachedEpisodesWithStatus) {
9899
return null;
99100
}
100101

101-
const isExpired = Date.now() - cachedValue.updatedAt > maxAgeMs;
102-
if (isExpired) {
102+
if (cachedEpisodesWithStatus.isExpired) {
103+
const store = loadCache();
104+
const cacheKey = getFeedKey(feed);
103105
delete store[cacheKey];
104106
persistCache();
105107
return null;
106108
}
107109

108-
return cachedValue.episodes.map(deserializeEpisode);
110+
return cachedEpisodesWithStatus.episodes;
109111
}
110112

111113
export function setCachedEpisodes(feed: PodcastFeed, episodes: Episode[]): void {
@@ -135,3 +137,26 @@ export function clearFeedCache(): void {
135137
console.error("Failed to clear feed cache:", error);
136138
}
137139
}
140+
141+
export function getCachedEpisodesWithStatus(
142+
feed: PodcastFeed,
143+
maxAgeMs: number = DEFAULT_TTL_MS,
144+
):
145+
| {
146+
episodes: Episode[];
147+
isExpired: boolean;
148+
}
149+
| null {
150+
const store = loadCache();
151+
const cacheKey = getFeedKey(feed);
152+
const cachedValue = store[cacheKey];
153+
154+
if (!cachedValue) {
155+
return null;
156+
}
157+
158+
return {
159+
episodes: cachedValue.episodes.map(deserializeEpisode),
160+
isExpired: Date.now() - cachedValue.updatedAt > maxAgeMs,
161+
};
162+
}

src/ui/PodcastView/PodcastView.svelte

Lines changed: 130 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -27,65 +27,66 @@ import { onMount } from "svelte";
2727
import searchEpisodes from "src/utility/searchEpisodes";
2828
import type { Playlist } from "src/types/Playlist";
2929
import spawnEpisodeContextMenu from "./spawnEpisodeContextMenu";
30-
import {
31-
getCachedEpisodes,
32-
setCachedEpisodes,
33-
} from "src/services/FeedCacheService";
34-
import { get } from "svelte/store";
30+
import {
31+
getCachedEpisodes,
32+
getCachedEpisodesWithStatus,
33+
setCachedEpisodes,
34+
} from "src/services/FeedCacheService";
35+
import { get } from "svelte/store";
3536
3637
let feeds: PodcastFeed[] = [];
3738
let selectedFeed: PodcastFeed | null = null;
3839
let selectedPlaylist: Playlist | null = null;
3940
let displayedEpisodes: Episode[] = [];
4041
let displayedPlaylists: Playlist[] = [];
4142
let latestEpisodes: Episode[] = [];
43+
const FEED_REFRESH_CONCURRENCY = 3;
4244
43-
onMount(() => {
44-
const unsubscribePlaylists = playlists.subscribe((pl) => {
45-
displayedPlaylists = [$queue, $favorites, $localFiles, ...Object.values(pl)];
46-
});
45+
onMount(() => {
46+
const unsubscribePlaylists = playlists.subscribe((pl) => {
47+
displayedPlaylists = [$queue, $favorites, $localFiles, ...Object.values(pl)];
48+
});
4749
48-
const unsubscribeSavedFeeds = savedFeeds.subscribe((storeValue) => {
49-
feeds = Object.values(storeValue);
50-
});
50+
const unsubscribeSavedFeeds = savedFeeds.subscribe((storeValue) => {
51+
feeds = Object.values(storeValue);
52+
void hydrateAndRefreshFeeds();
53+
});
5154
52-
const unsubscribeEpisodeCache = episodeCache.subscribe((cache) => {
53-
latestEpisodes = Object.entries(cache)
54-
.map(([_, episodes]) => episodes.slice(0, 10))
55-
.flat()
56-
.sort((a, b) => {
57-
if (a.episodeDate && b.episodeDate)
58-
return Number(b.episodeDate) - Number(a.episodeDate);
55+
const unsubscribeEpisodeCache = episodeCache.subscribe((cache) => {
56+
latestEpisodes = Object.entries(cache)
57+
.map(([_, episodes]) => episodes.slice(0, 10))
58+
.flat()
59+
.sort((a, b) => {
60+
if (a.episodeDate && b.episodeDate)
61+
return Number(b.episodeDate) - Number(a.episodeDate);
5962
60-
return 0;
61-
});
62-
});
63+
return 0;
64+
});
65+
});
6366
64-
(async () => {
65-
await fetchEpisodesInAllFeeds(feeds);
67+
return () => {
68+
unsubscribeEpisodeCache();
69+
unsubscribeSavedFeeds();
70+
unsubscribePlaylists();
71+
};
72+
});
6673
67-
if (!selectedFeed) {
68-
displayedEpisodes = latestEpisodes;
69-
}
70-
})();
74+
function getFeedCacheSettings() {
75+
const pluginInstance = get(plugin);
76+
const feedCacheSettings = pluginInstance?.settings?.feedCache;
7177
72-
return () => {
73-
unsubscribeEpisodeCache();
74-
unsubscribeSavedFeeds();
75-
unsubscribePlaylists();
76-
};
77-
});
78+
return {
79+
cacheEnabled: feedCacheSettings?.enabled !== false,
80+
cacheTtlMs:
81+
Math.max(1, feedCacheSettings?.ttlHours ?? 6) * 60 * 60 * 1000,
82+
};
83+
}
7884
7985
async function fetchEpisodes(
8086
feed: PodcastFeed,
8187
useCache: boolean = true,
8288
): Promise<Episode[]> {
83-
84-
const pluginInstance = get(plugin);
85-
const feedCacheSettings = pluginInstance?.settings?.feedCache;
86-
const cacheEnabled = feedCacheSettings?.enabled !== false;
87-
const cacheTtlMs =
88-
Math.max(1, feedCacheSettings?.ttlHours ?? 6) * 60 * 60 * 1000;
89+
const { cacheEnabled, cacheTtlMs } = getFeedCacheSettings();
8990
9091
const cachedEpisodesInFeed = $episodeCache[feed.title];
9192
@@ -129,14 +130,95 @@ onMount(() => {
129130
}
130131
}
131132
132-
function fetchEpisodesInAllFeeds(
133-
feedsToSearch: PodcastFeed[]
134-
): Promise<Episode[]> {
135-
return Promise.all(
136-
feedsToSearch.map((feed) => fetchEpisodes(feed))
137-
).then((episodes) => {
138-
return episodes.flat();
139-
});
133+
async function hydrateAndRefreshFeeds() {
134+
if (!feeds.length) {
135+
return;
136+
}
137+
138+
const { cacheEnabled, cacheTtlMs } = getFeedCacheSettings();
139+
140+
const cachedFeeds = cacheEnabled
141+
? feeds
142+
.map((feed) => {
143+
const cached = getCachedEpisodesWithStatus(feed, cacheTtlMs);
144+
if (!cached?.episodes.length) {
145+
return null;
146+
}
147+
148+
return { feed, ...cached };
149+
})
150+
.filter(Boolean) as Array<{
151+
feed: PodcastFeed;
152+
episodes: Episode[];
153+
isExpired: boolean;
154+
}>
155+
: [];
156+
157+
if (cachedFeeds.length) {
158+
episodeCache.update((cache) => {
159+
const updatedCache = { ...cache };
160+
161+
for (const { feed, episodes } of cachedFeeds) {
162+
if (updatedCache[feed.title]?.length) {
163+
continue;
164+
}
165+
166+
updatedCache[feed.title] = episodes;
167+
}
168+
169+
return updatedCache;
170+
});
171+
172+
if (!selectedFeed && !selectedPlaylist) {
173+
displayedEpisodes = latestEpisodes;
174+
}
175+
}
176+
177+
const feedsToRefresh = cacheEnabled
178+
? feeds.filter((feed) => {
179+
const feedKey = feed.url ?? feed.title;
180+
const cached = cachedFeeds.find(
181+
({ feed: cachedFeed }) =>
182+
(cachedFeed.url ?? cachedFeed.title) === feedKey,
183+
);
184+
185+
return !cached || cached.isExpired;
186+
})
187+
: feeds;
188+
189+
if (!feedsToRefresh.length) {
190+
if (!selectedFeed && !selectedPlaylist) {
191+
displayedEpisodes = latestEpisodes;
192+
}
193+
return;
194+
}
195+
196+
void refreshFeedsWithLimit(feedsToRefresh);
197+
}
198+
199+
async function refreshFeedsWithLimit(feedsToRefresh: PodcastFeed[]) {
200+
const queueToRefresh = [...feedsToRefresh];
201+
const workers = Array.from(
202+
{ length: FEED_REFRESH_CONCURRENCY },
203+
async () => {
204+
while (queueToRefresh.length) {
205+
const feed = queueToRefresh.shift();
206+
if (!feed) break;
207+
208+
await fetchEpisodes(feed, false);
209+
}
210+
},
211+
);
212+
213+
try {
214+
await Promise.all(workers);
215+
} catch (error) {
216+
console.error("Failed to refresh saved feeds:", error);
217+
} finally {
218+
if (!selectedFeed && !selectedPlaylist) {
219+
displayedEpisodes = latestEpisodes;
220+
}
221+
}
140222
}
141223
142224
async function handleClickPodcast(

0 commit comments

Comments
 (0)