Skip to content

Commit e2011ea

Browse files
committed
Merge origin/master into optimize-latest-episodes
2 parents ffcee85 + 7d14017 commit e2011ea

7 files changed

Lines changed: 314 additions & 211 deletions

File tree

src/store/index.ts

Lines changed: 165 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { get, writable } from "svelte/store";
1+
import { get, readable, writable } from "svelte/store";
22
import type PodNotes from "src/main";
33
import type { Episode } from "src/types/Episode";
44
import type { PlayedEpisode } from "src/types/PlayedEpisode";
@@ -102,6 +102,170 @@ export const savedFeeds = writable<{ [podcastName: string]: PodcastFeed }>({});
102102

103103
export const episodeCache = writable<{ [podcastName: string]: Episode[] }>({});
104104

105+
const LATEST_EPISODES_PER_FEED = 10;
106+
107+
type LatestEpisodesByFeed = Map<string, Episode[]>;
108+
type FeedEpisodeSources = Map<string, Episode[]>;
109+
110+
function getEpisodeTimestamp(episode?: Episode): number {
111+
if (!episode?.episodeDate) return 0;
112+
113+
return Number(episode.episodeDate);
114+
}
115+
116+
function getLatestEpisodesForFeed(episodes: Episode[]): Episode[] {
117+
if (!episodes?.length) return [];
118+
119+
return episodes
120+
.slice(0, LATEST_EPISODES_PER_FEED)
121+
.sort((a, b) => getEpisodeTimestamp(b) - getEpisodeTimestamp(a));
122+
}
123+
124+
function shallowEqualEpisodes(a?: Episode[], b?: Episode[]): boolean {
125+
if (!a || !b || a.length !== b.length) return false;
126+
127+
for (let i = 0; i < a.length; i += 1) {
128+
if (a[i] !== b[i]) return false;
129+
}
130+
131+
return true;
132+
}
133+
134+
const latestEpisodeIdentifier = (episode: Episode): string =>
135+
`${episode.podcastName}::${episode.title}`;
136+
137+
function insertEpisodeSorted(
138+
episodes: Episode[],
139+
episodeToInsert: Episode,
140+
limit: number,
141+
): Episode[] {
142+
const nextEpisodes = [...episodes];
143+
const value = getEpisodeTimestamp(episodeToInsert);
144+
let low = 0;
145+
let high = nextEpisodes.length;
146+
147+
while (low < high) {
148+
const mid = (low + high) >> 1;
149+
const midValue = getEpisodeTimestamp(nextEpisodes[mid]);
150+
151+
if (value > midValue) {
152+
high = mid;
153+
} else {
154+
low = mid + 1;
155+
}
156+
}
157+
158+
nextEpisodes.splice(low, 0, episodeToInsert);
159+
160+
if (nextEpisodes.length > limit) {
161+
nextEpisodes.length = limit;
162+
}
163+
164+
return nextEpisodes;
165+
}
166+
167+
function removeFeedEntries(
168+
currentLatest: Episode[],
169+
feedEpisodes: Episode[] | undefined = [],
170+
): Episode[] {
171+
if (!feedEpisodes?.length) {
172+
return currentLatest;
173+
}
174+
175+
const feedKeys = new Set(feedEpisodes.map(latestEpisodeIdentifier));
176+
177+
return currentLatest.filter(
178+
(episode) => !feedKeys.has(latestEpisodeIdentifier(episode)),
179+
);
180+
}
181+
182+
function updateLatestEpisodesForFeed(
183+
currentLatest: Episode[],
184+
previousFeedEpisodes: Episode[] | undefined,
185+
nextFeedEpisodes: Episode[] | undefined,
186+
limit: number,
187+
): Episode[] {
188+
let nextLatest = removeFeedEntries(currentLatest, previousFeedEpisodes);
189+
190+
if (!nextFeedEpisodes?.length) {
191+
return nextLatest;
192+
}
193+
194+
for (const episode of nextFeedEpisodes) {
195+
nextLatest = insertEpisodeSorted(nextLatest, episode, limit);
196+
}
197+
198+
return nextLatest;
199+
}
200+
201+
export const latestEpisodes = readable<Episode[]>([], (set) => {
202+
let latestByFeed: LatestEpisodesByFeed = new Map();
203+
let feedSources: FeedEpisodeSources = new Map();
204+
let mergedLatest: Episode[] = [];
205+
206+
const unsubscribe = episodeCache.subscribe((cache) => {
207+
const cacheEntries = Object.entries(cache);
208+
const feedCount = cacheEntries.length;
209+
const latestLimit = Math.max(
210+
1,
211+
LATEST_EPISODES_PER_FEED * Math.max(feedCount, 1),
212+
);
213+
214+
let changed = false;
215+
let nextMerged = mergedLatest;
216+
const nextSources: FeedEpisodeSources = new Map();
217+
const nextLatestByFeed: LatestEpisodesByFeed = new Map();
218+
219+
for (const [feedTitle, episodes] of cacheEntries) {
220+
nextSources.set(feedTitle, episodes);
221+
const previousSource = feedSources.get(feedTitle);
222+
const previousLatest = latestByFeed.get(feedTitle) || [];
223+
224+
const nextLatestForFeed =
225+
previousSource === episodes && previousLatest
226+
? previousLatest
227+
: getLatestEpisodesForFeed(episodes);
228+
229+
nextLatestByFeed.set(feedTitle, nextLatestForFeed);
230+
231+
if (!shallowEqualEpisodes(previousLatest, nextLatestForFeed)) {
232+
changed = true;
233+
nextMerged = updateLatestEpisodesForFeed(
234+
nextMerged,
235+
previousLatest,
236+
nextLatestForFeed,
237+
latestLimit,
238+
);
239+
}
240+
}
241+
242+
for (const feedTitle of latestByFeed.keys()) {
243+
if (!nextSources.has(feedTitle)) {
244+
changed = true;
245+
nextMerged = removeFeedEntries(
246+
nextMerged,
247+
latestByFeed.get(feedTitle),
248+
);
249+
}
250+
}
251+
252+
feedSources = nextSources;
253+
latestByFeed = nextLatestByFeed;
254+
255+
if (changed) {
256+
mergedLatest = nextMerged;
257+
set(mergedLatest);
258+
}
259+
});
260+
261+
return () => {
262+
latestByFeed.clear();
263+
feedSources.clear();
264+
mergedLatest = [];
265+
unsubscribe();
266+
};
267+
});
268+
105269
export const downloadedEpisodes = (() => {
106270
const store = writable<{ [podcastName: string]: DownloadedEpisode[] }>({});
107271
const { subscribe, update, set } = store;

src/ui/PodcastView/EpisodeListItem.svelte

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@
3838
src={episode.artworkUrl}
3939
alt={episode.title}
4040
fadeIn={true}
41+
width="5rem"
42+
height="5rem"
4143
class="podcast-episode-thumbnail"
4244
/>
4345
</div>

0 commit comments

Comments
 (0)