• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

NikkelM / Random-YouTube-Video / 24898777424

24 Apr 2026 03:54PM UTC coverage: 92.478% (-0.1%) from 92.604%
24898777424

push

github

web-flow
Bump postcss from 8.4.38 to 8.5.10 (#352)

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

309 of 363 branches covered (85.12%)

1463 of 1582 relevant lines covered (92.48%)

494.7 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

90.65
/src/shuffleVideo.js
1
// Handles everything concerning the shuffling of videos, including fetching data from the YouTube API
1✔
2
import {
1✔
3
        isEmpty,
1✔
4
        addHours,
1✔
5
        getLength,
1✔
6
        RandomYoutubeVideoError,
1✔
7
        YoutubeAPIError,
1✔
8
        updateSmallButtonStyleForText,
1✔
9
        getPageTypeFromURL
1✔
10
} from "./utils.js";
1✔
11
import { configSync, setSyncStorageValue, getUserQuotaRemainingToday } from "./chromeStorage.js";
1✔
12

1✔
13
// The time when the shuffle started
1✔
14
let shuffleStartTime = null;
1✔
15

1✔
16
// --------------- Public ---------------
1✔
17
// Chooses a random video uploaded on the current YouTube channel
1✔
18
export async function chooseRandomVideo(channelId, firedFromPopup, progressTextElement, shuffleButtonTooltipElement = null) {
1✔
19
        /* c8 ignore start */
1✔
20
        try {
1✔
21
                // The service worker will get stopped after 30 seconds
1✔
22
                // This request will cause a "Receiving end does not exist" error, but starts the worker again as well
1✔
23
                await chrome.runtime.sendMessage({ command: "connectionTest" });
1✔
24
        } catch (error) {
1✔
25
                console.log("The service worker was stopped and had to be restarted.");
1✔
26
        }
1✔
27
        try {
1✔
28
                // While chooseRandomVideo is running, we need to keep the service worker alive
1✔
29
                // Otherwise, it will get stopped after 30 seconds and we will get an error if fetching the videos takes longer
1✔
30
                // So every 20 seconds, we send a message to the service worker to keep it alive
1✔
31
                var keepServiceWorkerAlive = setInterval(() => {
1✔
32
                        chrome.runtime.sendMessage({ command: "connectionTest" });
1✔
33
                }, 20000);
1✔
34
                /* c8 ignore stop */
1✔
35

956✔
36
                shuffleStartTime = new Date();
956✔
37

956✔
38
                // Each user has a set amount of quota they can use per day.
956✔
39
                // If they exceed it, they need to provide a custom API key, or wait until the quota resets the next day
956✔
40
                let userQuotaRemainingToday = await getUserQuotaRemainingToday();
956✔
41

956✔
42
                // If we update the playlist info in any way and want to send it to the database in the end, this variable indicates it
956✔
43
                var shouldUpdateDatabase = false;
956✔
44

956✔
45
                // User preferences
956✔
46
                var databaseSharing = configSync.databaseSharingEnabledOption;
956✔
47

956✔
48
                // Get the id of the uploads playlist for this channel
956✔
49
                var uploadsPlaylistId = channelId ? channelId.replace("UC", "UU") : null;
956✔
50
                if (!uploadsPlaylistId) {
956✔
51
                        throw new RandomYoutubeVideoError(
2✔
52
                                {
2✔
53
                                        code: "RYV-1",
2✔
54
                                        message: "No channel-ID found.",
2✔
55
                                        solveHint: "Please reload the page and try again. Please inform the developer if this keeps happening.",
2✔
56
                                        showTrace: false,
2✔
57
                                        canSavePlaylist: false
2✔
58
                                }
2✔
59
                        );
2✔
60
                }
2✔
61

954✔
62
                console.log(`Shuffling from playlist/channel: ${uploadsPlaylistId}`);
954✔
63

954✔
64
                // Check if the playlist is already saved in local storage, so we don't need to access the database
954✔
65
                var playlistInfo = await tryGetPlaylistFromLocalStorage(uploadsPlaylistId);
954✔
66

954✔
67
                // The playlist does not exist locally. Try to get it from the database first
954✔
68
                if (isEmpty(playlistInfo)) {
956✔
69
                        // No information for this playlist is saved in local storage
141✔
70
                        // Try to get it from the database
141✔
71
                        console.log(`Uploads playlist for this channel does not exist locally.${databaseSharing ? " Trying to get it from the database..." : ""}`);
141✔
72
                        playlistInfo = databaseSharing ? await tryGetPlaylistFromDB(uploadsPlaylistId) : {};
141✔
73

141✔
74
                        // If the playlist does not exist in the database, get it from the API
141✔
75
                        if (isEmpty(playlistInfo)) {
141✔
76
                                if (databaseSharing) {
29✔
77
                                        console.log("Uploads playlist for this channel does not exist in the database. Fetching it from the YouTube API...");
23✔
78
                                } else {
29✔
79
                                        console.log("Fetching the uploads playlist for this channel from the YouTube API...");
6✔
80
                                }
6✔
81
                                ({ playlistInfo, userQuotaRemainingToday } = await getPlaylistFromAPI(uploadsPlaylistId, null, userQuotaRemainingToday, progressTextElement, shuffleButtonTooltipElement));
29✔
82

16✔
83
                                shouldUpdateDatabase = true;
16✔
84
                        } else if (databaseSharing && (playlistInfo["lastUpdatedDBAt"] ?? new Date(0).toISOString()) < addHours(new Date(), -48).toISOString()) {
141!
85
                                // If the playlist exists in the database but is outdated, update it from the API.
69✔
86
                                console.log("Uploads playlist for this channel may be outdated in the database. Updating from the YouTube API...");
69✔
87

69✔
88
                                ({ playlistInfo, userQuotaRemainingToday } = await updatePlaylistFromAPI(playlistInfo, uploadsPlaylistId, null, userQuotaRemainingToday, progressTextElement, shuffleButtonTooltipElement));
69✔
89

51✔
90
                                shouldUpdateDatabase = true;
51✔
91
                        }
51✔
92

110✔
93
                        console.log("Uploads playlist for this channel successfully retrieved.");
110✔
94

110✔
95
                        // The playlist exists locally, but may be outdated. Update it from the database. If needed, update the database values as well.
110✔
96
                } else if ((databaseSharing && ((playlistInfo["lastFetchedFromDB"] ?? new Date(0).toISOString()) < addHours(new Date(), -48).toISOString())) ||
956!
97
                        (!databaseSharing && ((playlistInfo["lastAccessedLocally"] ?? new Date(0).toISOString()) < addHours(new Date(), -48).toISOString()))) {
813!
98
                        console.log(`Local uploads playlist for this channel may be outdated.${databaseSharing ? " Updating from the database..." : ""}`);
659✔
99

659✔
100
                        // Try to get an updated version of the playlist, but keep the information about locally known videos and shorts
659✔
101
                        playlistInfo = databaseSharing ? await tryGetPlaylistFromDB(uploadsPlaylistId, playlistInfo) : {};
659✔
102

659✔
103
                        // The playlist does not exist in the database (==it was deleted since the user last fetched it). Get it from the API.
659✔
104
                        // With the current functionality and db rules, this shouldn't happen, except if the user has opted out of database sharing.
659✔
105
                        if (isEmpty(playlistInfo)) {
659✔
106
                                console.log(`${databaseSharing ? "Uploads playlist for this channel does not exist in the database. " : "Fetching it from the YouTube API..."}`);
129✔
107
                                ({ playlistInfo, userQuotaRemainingToday } = await getPlaylistFromAPI(uploadsPlaylistId, null, userQuotaRemainingToday, progressTextElement, shuffleButtonTooltipElement));
129✔
108

81✔
109
                                shouldUpdateDatabase = true;
81✔
110
                                // If the playlist exists in the database but is outdated there as well, update it from the API.
81✔
111
                        } else if ((playlistInfo["lastUpdatedDBAt"] ?? new Date(0).toISOString()) < addHours(new Date(), -48).toISOString()) {
659!
112
                                console.log("Uploads playlist for this channel may be outdated in the database. Updating from the YouTube API...");
409✔
113
                                ({ playlistInfo, userQuotaRemainingToday } = await updatePlaylistFromAPI(playlistInfo, uploadsPlaylistId, null, userQuotaRemainingToday, progressTextElement, shuffleButtonTooltipElement));
409✔
114

265✔
115
                                shouldUpdateDatabase = true;
265✔
116
                        }
265✔
117
                }
659✔
118

731✔
119
                // Update the remaining user quota in the configSync
731✔
120
                await setSyncStorageValue("userQuotaRemainingToday", Math.max(0, userQuotaRemainingToday));
731✔
121

731✔
122
                // Validate that all required keys exist in the playlistInfo object
731✔
123
                validatePlaylistInfo(playlistInfo);
731✔
124

731✔
125
                // Join the new videos with the old ones to be able to use them when shuffling
731✔
126
                // Do not delete the newVideos key as it may be needed when updating the database
731✔
127
                playlistInfo["videos"]["unknownType"] = Object.assign({}, playlistInfo["videos"]["unknownType"] ?? {}, playlistInfo["newVideos"] ?? {});
956!
128

956✔
129
                let chosenVideos;
956✔
130
                var encounteredDeletedVideos;
956✔
131
                ({ chosenVideos, playlistInfo, shouldUpdateDatabase, encounteredDeletedVideos } = await chooseRandomVideosFromPlaylist(playlistInfo, channelId, shouldUpdateDatabase, progressTextElement, shuffleButtonTooltipElement));
956✔
132

656✔
133
                // Save the playlist to the database and locally
656✔
134
                playlistInfo = await handlePlaylistDatabaseUpload(playlistInfo, uploadsPlaylistId, shouldUpdateDatabase, databaseSharing, encounteredDeletedVideos);
656✔
135
                await savePlaylistToLocalStorage(uploadsPlaylistId, playlistInfo);
656✔
136

656✔
137
                await setSyncStorageValue("numShuffledVideosTotal", configSync.numShuffledVideosTotal + 1);
656✔
138

656✔
139
                await playVideo(chosenVideos, firedFromPopup);
656✔
140
        } catch (error) {
956✔
141
                await setSyncStorageValue("userQuotaRemainingToday", Math.max(0, configSync.userQuotaRemainingToday - 1));
300✔
142

300✔
143
                // There are some errors that still allow us to save the playlist to the database and locally
300✔
144
                if (error instanceof RandomYoutubeVideoError && error.canSavePlaylist == true) {
300✔
145
                        playlistInfo = await handlePlaylistDatabaseUpload(playlistInfo, uploadsPlaylistId, shouldUpdateDatabase, databaseSharing, encounteredDeletedVideos);
75✔
146
                        await savePlaylistToLocalStorage(uploadsPlaylistId, playlistInfo);
75✔
147
                }
75✔
148

300✔
149
                throw error;
300✔
150
        } finally {
956✔
151
                clearInterval(keepServiceWorkerAlive);
956✔
152
        }
956✔
153
}
956✔
154

1✔
155
// --------------- Private ---------------
1✔
156
// ---------- Database ----------
1✔
157
// Try to get the playlist from the database. If it does not exist, return an empty dictionary.
1✔
158
async function tryGetPlaylistFromDB(playlistId, localPlaylistInfo = null) {
785✔
159
        const msg = {
785✔
160
                command: "getPlaylistFromDB",
785✔
161
                data: playlistId
785✔
162
        };
785✔
163

785✔
164
        // Some of the tests break if we do not create a deepCopy here, as the local and database object somehow get linked
785✔
165
        let playlistInfo = await chrome.runtime.sendMessage(msg);
785✔
166

785✔
167
        /* c8 ignore start - These are legacy conversions we don't want to test */
1✔
168
        // In case the playlist is still in the old Array format (before v1.0.0) in the database, convert it to the new format
1✔
169
        if (playlistInfo && playlistInfo["videos"] && Array.isArray(playlistInfo["videos"])) {
1✔
170
                console.log("The playlist was found in the database, but it is in an old format (before v1.0.0). Removing...");
1✔
171

1✔
172
                await chrome.runtime.sendMessage({ command: 'updateDBPlaylistToV1.0.0', data: { key: playlistId } });
1✔
173
                return {};
1✔
174
        }
1✔
175

1✔
176
        // In case the videos have the upload date AND time in the database (before v1.3.0), convert it to only the date
1✔
177
        if (playlistInfo && playlistInfo["videos"] && typeof playlistInfo["videos"] === "string" && playlistInfo["videos"][Object.keys(playlistInfo["videos"])[0]].length > 10) {
1✔
178
                console.log("The playlist was found in the database, but it is in an old format (before v1.3.0). Updating format...");
1✔
179

1✔
180
                // Convert the videos to contain only the date
1✔
181
                for (const videoId in playlistInfo["videos"]) {
1✔
182
                        playlistInfo["videos"][videoId] = playlistInfo["videos"][videoId].substring(0, 10);
1✔
183
                }
1✔
184
        }
1✔
185
        /* c8 ignore stop */
1✔
186

785✔
187
        if (!playlistInfo || !playlistInfo["videos"]) {
785✔
188
                return {};
143✔
189
        }
143✔
190

642✔
191
        playlistInfo["lastFetchedFromDB"] = new Date().toISOString();
642✔
192

642✔
193
        const videosCopy = JSON.parse(JSON.stringify(playlistInfo["videos"]));
642✔
194
        if (!localPlaylistInfo) {
785✔
195
                // Since we just fetched the playlist, we do not have any info locally, so all videos are "unknownType"
112✔
196
                playlistInfo["videos"] = {};
112✔
197
                playlistInfo["videos"]["unknownType"] = videosCopy;
112✔
198
                playlistInfo["videos"]["knownVideos"] = {};
112✔
199
                playlistInfo["videos"]["knownShorts"] = {};
112✔
200
        } else {
785✔
201
                const allVideosInLocalPlaylist = getAllVideosFromLocalPlaylist(localPlaylistInfo);
530✔
202
                const allVideosInLocalPlaylistAsSet = new Set(Object.keys(allVideosInLocalPlaylist));
530✔
203
                const allVideosInDatabaseAsSet = new Set(Object.keys(playlistInfo["videos"]));
530✔
204

530✔
205
                // Add videos that are new from the database to the local playlist
530✔
206
                const videosOnlyInDatabase = Object.keys(playlistInfo["videos"]).filter((videoId) => !allVideosInLocalPlaylistAsSet.has(videoId));
530✔
207

530✔
208
                playlistInfo["videos"] = {};
530✔
209
                playlistInfo["videos"]["unknownType"] = Object.assign({}, localPlaylistInfo["videos"]["unknownType"], Object.fromEntries(videosOnlyInDatabase.map((videoId) => [videoId, videosCopy[videoId]])));
530✔
210
                playlistInfo["videos"]["knownVideos"] = localPlaylistInfo["videos"]["knownVideos"] ?? {};
530!
211
                playlistInfo["videos"]["knownShorts"] = localPlaylistInfo["videos"]["knownShorts"] ?? {};
530!
212

530✔
213
                // Remove videos from the local playlist object that no longer exist in the database
530✔
214
                const videoTypes = ["knownVideos", "knownShorts", "unknownType"];
530✔
215
                for (const type of videoTypes) {
530✔
216
                        for (const videoId in localPlaylistInfo["videos"][type]) {
1,590✔
217
                                if (!allVideosInDatabaseAsSet.has(videoId)) {
6,440✔
218
                                        delete playlistInfo["videos"][type][videoId];
1,140✔
219
                                }
1,140✔
220
                        }
6,440✔
221
                }
1,590✔
222
        }
530✔
223

642✔
224
        return playlistInfo;
642✔
225
}
785✔
226

1✔
227
// Prepare the playlist info object for saving to the database, and then upload it
1✔
228
async function handlePlaylistDatabaseUpload(playlistInfo, uploadsPlaylistId, shouldUpdateDatabase, databaseSharing, encounteredDeletedVideos) {
731✔
229
        if (shouldUpdateDatabase && databaseSharing) {
731✔
230
                console.log("Updating the database with the new playlist information...");
457✔
231

457✔
232
                playlistInfo["lastUpdatedDBAt"] = new Date().toISOString();
457✔
233

457✔
234
                let videosToDatabase = {};
457✔
235
                // If any videos need to be deleted, this should be the union of videos, new videos, minus the videos to delete
457✔
236
                if (encounteredDeletedVideos) {
457✔
237
                        console.log("Some videos need to be deleted from the database. All current videos will be uploaded to the database...");
119✔
238
                        videosToDatabase = getAllVideosFromLocalPlaylist(playlistInfo);
119✔
239
                } else {
457✔
240
                        // Otherwise, we want to only upload new videos. If there are no "newVideos", we upload all videos, as this is the first time we are uploading the playlist
338✔
241
                        console.log("Uploading new video IDs to the database...");
338✔
242
                        if (getLength(playlistInfo["newVideos"] ?? {}) > 0) {
338!
243
                                videosToDatabase = playlistInfo["newVideos"];
177✔
244
                        } else {
338✔
245
                                videosToDatabase = getAllVideosFromLocalPlaylist(playlistInfo);
161✔
246
                        }
161✔
247
                }
338✔
248

457✔
249
                await uploadPlaylistToDatabase(playlistInfo, videosToDatabase, uploadsPlaylistId, encounteredDeletedVideos);
457✔
250

457✔
251
                // If we just updated the database, we automatically have the same version as it
457✔
252
                playlistInfo["lastFetchedFromDB"] = new Date().toISOString();
457✔
253
        }
457✔
254

731✔
255
        return playlistInfo;
731✔
256
}
731✔
257

1✔
258
// Upload a playlist to the database
1✔
259
async function uploadPlaylistToDatabase(playlistInfo, videosToDatabase, uploadsPlaylistId, encounteredDeletedVideos) {
457✔
260
        // Only upload the wanted keys
457✔
261
        const playlistInfoForDatabase = {
457✔
262
                "lastUpdatedDBAt": playlistInfo["lastUpdatedDBAt"] ?? new Date().toISOString(),
457!
263
                "lastVideoPublishedAt": playlistInfo["lastVideoPublishedAt"] ?? new Date(0).toISOString().slice(0, 19) + 'Z',
457!
264
                "videos": videosToDatabase
457✔
265
        };
457✔
266

457✔
267
        // Make sure the data is in the correct format
457✔
268
        if (playlistInfoForDatabase["lastUpdatedDBAt"].length !== 24) {
457!
269
                alert(`Random YouTube Video:\nPlease send this information to the developer:\n\nlastUpdatedDBAt has the wrong format (got ${playlistInfoForDatabase["lastVideoPublishedAt"]}).\nChannelId: ${uploadsPlaylistId}.`);
×
270
                return;
×
271
        }
×
272
        if (playlistInfoForDatabase["lastVideoPublishedAt"].length !== 20) {
457!
273
                alert(`Random YouTube Video:\nPlease send this information to the developer:\n\nlastVideoPublishedAt has the wrong format (got ${playlistInfoForDatabase["lastVideoPublishedAt"]}).\nChannelId: ${uploadsPlaylistId}.`);
×
274
                return;
×
275
        }
×
276
        if (getLength(playlistInfoForDatabase["videos"]) < 1) {
457!
277
                alert(`Random YouTube Video:\nPlease send this information to the developer:\n\nNo videos object was found.\nChannelId: ${uploadsPlaylistId}.`);
×
278
                return;
×
279
        }
×
280

457✔
281
        // Send the playlist info to the database
457✔
282
        const msg = {
457✔
283
                command: encounteredDeletedVideos ? 'overwritePlaylistInfoInDB' : 'updatePlaylistInfoInDB',
457✔
284
                data: {
457✔
285
                        key: uploadsPlaylistId,
457✔
286
                        val: playlistInfoForDatabase
457✔
287
                }
457✔
288
        };
457✔
289

457✔
290
        await chrome.runtime.sendMessage(msg);
457✔
291
}
457✔
292

1✔
293
// ---------- YouTube API ----------
1✔
294
async function getPlaylistFromAPI(playlistId, useAPIKeyAtIndex, userQuotaRemainingToday, progressTextElement, shuffleButtonTooltipElement, disregardUserQuota = false) {
158✔
295
        // Get an API key
158✔
296
        let { APIKey, isCustomKey, keyIndex } = await getAPIKey(useAPIKeyAtIndex);
158✔
297
        // We need to keep track of the original key's index, so we know when we have tried all keys
157✔
298
        const originalKeyIndex = keyIndex;
157✔
299

157✔
300
        // If the user does not use a custom API key and has no quota remaining, we cannot continue
157✔
301
        if (!isCustomKey && userQuotaRemainingToday <= 0 && !disregardUserQuota) {
158✔
302
                throw new RandomYoutubeVideoError(
27✔
303
                        {
27✔
304
                                code: "RYV-4A",
27✔
305
                                message: "You have exceeded your daily quota allocation for the YouTube API.",
27✔
306
                                solveHint: "You can try again tomorrow or provide a custom API key.",
27✔
307
                                showTrace: false,
27✔
308
                                canSavePlaylist: false
27✔
309
                        }
27✔
310
                );
27✔
311
        }
27✔
312

130✔
313
        let playlistInfo = {};
130✔
314
        playlistInfo["videos"] = {};
130✔
315
        playlistInfo["videos"]["unknownType"] = {};
130✔
316
        playlistInfo["videos"]["knownVideos"] = {};
130✔
317
        playlistInfo["videos"]["knownShorts"] = {};
130✔
318

130✔
319
        let pageToken = "";
130✔
320
        let apiResponse;
130✔
321

130✔
322
        ({ apiResponse, APIKey, isCustomKey, keyIndex, userQuotaRemainingToday } = await getPlaylistSnippetFromAPI(playlistId, pageToken, APIKey, isCustomKey, keyIndex, originalKeyIndex, userQuotaRemainingToday, disregardUserQuota));
158✔
323

125✔
324
        // If there are more results we need to fetch than the user has quota remaining (+leeway) and the user is not using a custom API key, we need to throw an error
125✔
325
        // If we would normally disregard the user quota (e.g. if we need to refetch all videos due to the database missing videos), but there are too many uploads, we need to protect the userbase and restrict the operation as well
125✔
326
        const totalResults = apiResponse["pageInfo"]["totalResults"];
125✔
327
        if (totalResults / 50 >= userQuotaRemainingToday + 50 && !isCustomKey && (!disregardUserQuota || totalResults >= 5000)) {
158!
328
                throw new RandomYoutubeVideoError(
27✔
329
                        {
27✔
330
                                code: "RYV-4B",
27✔
331
                                message: `The channel you are shuffling from has too many uploads (${totalResults}) for the amount of API requests you can make. To protect the userbase, each user has a limited amount of requests they can make per day.`,
27✔
332
                                solveHint: "To shuffle from channels with more uploads, please use a custom API key.",
27✔
333
                                showTrace: false,
27✔
334
                                canSavePlaylist: false
27✔
335
                        }
27✔
336
                );
27✔
337
        }
27✔
338

98✔
339
        // The YouTube API limits the number of videos that can be fetched for uploads playlists to 20,000
98✔
340
        // If it seems that such a limitation is in place, we want to alert the user to it
98✔
341
        if (totalResults >= 19999) {
158✔
342
                window.alert("NOTICE: The channel you are shuffling from has a lot of uploads (20,000+). The YouTube API only allows fetching the most recent 20,000 videos, which means that older uploads will not be shuffled from. This limitation is in place no matter if you use a custom API key or not.\n\nThe extension will now fetch all videos it can get from the API.");
1✔
343
        }
1✔
344

98✔
345
        // Set the current progress as text for the shuffle button/info text
98✔
346
        let resultsFetchedCount = apiResponse["items"].length;
98✔
347

98✔
348
        // If there are less than 50 videos, we don't need to show a progress percentage
98✔
349
        if (totalResults > 50) {
158✔
350
                const percentage = Math.round(resultsFetchedCount / totalResults * 100);
34✔
351
                updateProgressTextElement(progressTextElement, `\xa0Fetching: ${percentage}%`, `${percentage}%`, shuffleButtonTooltipElement, "Fetching videos may take longer if the channel has a lot of uploads or your network speed is slow. Please wait...");
34✔
352
        }
34✔
353

98✔
354
        // For each video, add an entry in the form of videoId: uploadTime
98✔
355
        playlistInfo["videos"]["unknownType"] = Object.fromEntries(apiResponse["items"].map((video) => [video["contentDetails"]["videoId"], video["contentDetails"]["videoPublishedAt"].substring(0, 10)]));
98✔
356

98✔
357
        // We also want to get the uploadTime of the most recent video
98✔
358
        playlistInfo["lastVideoPublishedAt"] = apiResponse["items"][0]["contentDetails"]["videoPublishedAt"];
98✔
359
        pageToken = apiResponse["nextPageToken"] ? apiResponse["nextPageToken"] : null;
158✔
360

158✔
361
        while (pageToken !== null) {
158✔
362
                ({ apiResponse, APIKey, isCustomKey, keyIndex, userQuotaRemainingToday } = await getPlaylistSnippetFromAPI(playlistId, pageToken, APIKey, isCustomKey, keyIndex, originalKeyIndex, userQuotaRemainingToday, disregardUserQuota));
67✔
363

66✔
364
                // Set the current progress as text for the shuffle button/info text
66✔
365
                // We never get to this code part if there are less than or exactly 50 videos in the playlist, so we don't need to check for that
66✔
366
                resultsFetchedCount += apiResponse["items"].length;
66✔
367

66✔
368
                const percentage = Math.round(resultsFetchedCount / totalResults * 100);
66✔
369
                updateProgressTextElement(progressTextElement, `\xa0Fetching: ${percentage}%`, `${percentage}%`);
66✔
370

66✔
371
                // For each video, add an entry in the form of videoId: uploadTime
66✔
372
                playlistInfo["videos"]["unknownType"] = Object.assign(playlistInfo["videos"]["unknownType"], Object.fromEntries(apiResponse["items"].map((video) => [video["contentDetails"]["videoId"], video["contentDetails"]["videoPublishedAt"].substring(0, 10)])));
66✔
373

66✔
374
                pageToken = apiResponse["nextPageToken"] ? apiResponse["nextPageToken"] : null;
67✔
375
        }
67✔
376

97✔
377
        playlistInfo["lastFetchedFromDB"] = new Date().toISOString();
97✔
378

97✔
379
        return { playlistInfo, userQuotaRemainingToday };
97✔
380
}
158✔
381

1✔
382
// Get snippets from the API as long as new videos are being found
1✔
383
async function updatePlaylistFromAPI(playlistInfo, playlistId, useAPIKeyAtIndex, userQuotaRemainingToday, progressTextElement, shuffleButtonTooltipElement) {
478✔
384
        // Get an API key
478✔
385
        let { APIKey, isCustomKey, keyIndex } = await getAPIKey(useAPIKeyAtIndex);
478✔
386
        // We need to keep track of the original key's index, so we know when we have tried all keys
478✔
387
        const originalKeyIndex = keyIndex;
478✔
388

478✔
389
        // If the user does not use a custom API key and has no quota remaining, we cannot continue
478✔
390
        if (!isCustomKey && userQuotaRemainingToday <= 0) {
478✔
391
                throw new RandomYoutubeVideoError(
81✔
392
                        {
81✔
393
                                code: "RYV-4A",
81✔
394
                                message: "You have exceeded your daily quota allocation for the YouTube API.",
81✔
395
                                solveHint: "You can try again tomorrow or provide a custom API key.",
81✔
396
                                showTrace: false,
81✔
397
                                canSavePlaylist: false
81✔
398
                        }
81✔
399
                );
81✔
400
        }
81✔
401

397✔
402
        let lastKnownUploadTime = playlistInfo["lastVideoPublishedAt"];
397✔
403

397✔
404
        let apiResponse;
397✔
405
        ({ apiResponse, APIKey, isCustomKey, keyIndex, userQuotaRemainingToday } = await getPlaylistSnippetFromAPI(playlistId, "", APIKey, isCustomKey, keyIndex, originalKeyIndex, userQuotaRemainingToday));
397✔
406

397✔
407
        const totalNumVideosOnChannel = apiResponse["pageInfo"]["totalResults"];
397✔
408
        // If the channel has already reached the API cap, we don't know how many new videos there are, so we put an estimate to show the user something
397✔
409
        // The difference could be negative if there are more videos saved in the database than exist in the playlist, e.g videos were deleted
397✔
410
        const numLocallyKnownVideos = getLength(getAllVideosFromLocalPlaylist(playlistInfo));
397✔
411
        const totalExpectedNewResults = totalNumVideosOnChannel > 19999 ? 1000 : Math.max(totalNumVideosOnChannel - numLocallyKnownVideos, 0);
478!
412

478✔
413
        // If there are more results we need to fetch than the user has quota remaining (+leeway) and the user is not using a custom API key, we need to throw an error
478✔
414
        if (totalExpectedNewResults / 50 >= userQuotaRemainingToday + 50 && !isCustomKey) {
478✔
415
                throw new RandomYoutubeVideoError(
81✔
416
                        {
81✔
417
                                code: "RYV-4B",
81✔
418
                                message: `The channel you are shuffling from has too many new uploads (${totalExpectedNewResults}) for the amount of API requests you can make. To protect the userbase, each user has a limited amount of requests they can make per day.`,
81✔
419
                                solveHint: "To shuffle from channels with more uploads, please use a custom API key.",
81✔
420
                                showTrace: false,
81✔
421
                                canSavePlaylist: false
81✔
422
                        }
81✔
423
                );
81✔
424
        }
81✔
425

316✔
426
        // Set the current progress as text for the shuffle button/info text
316✔
427
        let resultsFetchedCount = apiResponse["items"].length;
316✔
428

316✔
429
        // If there are less than 50 new videos, we don't need to show a progress percentage
316✔
430
        if (totalExpectedNewResults > 50) {
478✔
431
                const percentage = Math.min(Math.round(resultsFetchedCount / totalExpectedNewResults * 100), 100);
129✔
432
                updateProgressTextElement(progressTextElement, `\xa0Fetching: ${percentage}%`, `${percentage}%`, shuffleButtonTooltipElement, "Fetching videos may take longer if the channel has a lot of uploads or your network speed is slow. Please wait...");
129✔
433
        }
129✔
434

316✔
435
        // Update the "last video published at" date (only for the most recent video)
316✔
436
        // If the newest video isn't newer than what we already have, we don't need to update the local storage
316✔
437
        if (lastKnownUploadTime < apiResponse["items"][0]["contentDetails"]["videoPublishedAt"]) {
478✔
438
                console.log("At least one video has been published since the last check, updating video ID's...");
210✔
439
                playlistInfo["lastVideoPublishedAt"] = apiResponse["items"][0]["contentDetails"]["videoPublishedAt"];
210✔
440
        } else {
478✔
441
                console.log("No new videos have been published since the last check.");
106✔
442

106✔
443
                // Make sure that we are not missing any videos in the database
106✔
444
                if (totalNumVideosOnChannel > numLocallyKnownVideos) {
106!
445
                        console.log(`There are less videos saved in the database than are uploaded on the channel (${numLocallyKnownVideos}/${totalNumVideosOnChannel}), so some videos are missing. Refetching all videos...`);
×
446
                        return await getPlaylistFromAPI(playlistId, keyIndex, userQuotaRemainingToday, progressTextElement, shuffleButtonTooltipElement, true);
×
447
                }
×
448

106✔
449
                return { playlistInfo, userQuotaRemainingToday };
106✔
450
        }
106✔
451

210✔
452
        let currVideo = 0;
210✔
453
        let newVideos = {};
210✔
454

210✔
455
        // While the currently saved last video is older then the currently checked video from the API response, we need to add videos to local storage
210✔
456
        while (lastKnownUploadTime < apiResponse["items"][currVideo]["contentDetails"]["videoPublishedAt"]) {
478✔
457
                // Add the video to the newVideos object, with the videoId as key and the upload date (without time) as value
13,626✔
458
                newVideos[apiResponse["items"][currVideo]["contentDetails"]["videoId"]] = apiResponse["items"][currVideo]["contentDetails"]["videoPublishedAt"].substring(0, 10);
13,626✔
459

13,626✔
460
                currVideo++;
13,626✔
461

13,626✔
462
                // If the current page has been completely checked
13,626✔
463
                if (currVideo >= apiResponse["items"].length) {
13,626✔
464
                        // If another page exists, continue checking
258✔
465
                        if (apiResponse["nextPageToken"]) {
258✔
466

258✔
467
                                // Get the next snippet        
258✔
468
                                ({ apiResponse, APIKey, isCustomKey, keyIndex, userQuotaRemainingToday } = await getPlaylistSnippetFromAPI(playlistId, apiResponse["nextPageToken"], APIKey, isCustomKey, keyIndex, originalKeyIndex, userQuotaRemainingToday));
258✔
469

258✔
470
                                // Set the current progress as text for the shuffle button/info text
258✔
471
                                // We never get to this code part if there are less than or exactly 50 new videos, so we don't need to check for that
258✔
472
                                resultsFetchedCount += apiResponse["items"].length;
258✔
473

258✔
474
                                const percentage = Math.min(Math.round(resultsFetchedCount / totalExpectedNewResults * 100), 100);
258✔
475
                                updateProgressTextElement(progressTextElement, `\xa0Fetching: ${percentage}%`, `${percentage}%`);
258✔
476

258✔
477
                                currVideo = 0;
258✔
478
                                // Else, we have checked all videos
258✔
479
                        } else {
258!
480
                                break;
×
481
                        }
×
482
                }
258✔
483
        }
13,626✔
484
        console.log(`Found ${Object.keys(newVideos).length} new video(s).`);
210✔
485

210✔
486
        // Add the new videos to a new key within the playlistInfo
210✔
487
        playlistInfo["newVideos"] = newVideos;
210✔
488

210✔
489
        // Make sure that we are not missing any videos in the database
210✔
490
        const numVideosInDatabase = numLocallyKnownVideos + getLength(playlistInfo["newVideos"]);
210✔
491
        if (totalNumVideosOnChannel > numVideosInDatabase) {
478!
492
                console.log(`There are less videos saved in the database than are uploaded on the channel (${numVideosInDatabase}/${totalNumVideosOnChannel}), so some videos are missing. Refetching all videos...`);
×
493
                return await getPlaylistFromAPI(playlistId, keyIndex, userQuotaRemainingToday, progressTextElement, shuffleButtonTooltipElement, true);
×
494
        }
×
495

210✔
496
        return { playlistInfo, userQuotaRemainingToday };
210✔
497
}
478✔
498

1✔
499
// Send a request to the Youtube API to get a snippet of a playlist
1✔
500
async function getPlaylistSnippetFromAPI(playlistId, pageToken, APIKey, isCustomKey, keyIndex, originalKeyIndex, userQuotaRemainingToday, disregardUserQuota = false) {
852✔
501
        const originalUserQuotaRemainingToday = userQuotaRemainingToday;
852✔
502
        let apiResponse;
852✔
503

852✔
504
        // We wrap this in a while block to simulate a retry mechanism until we get a valid response
852✔
505
        /* eslint no-constant-condition: ["error", { "checkLoops": false }] */
852✔
506
        while (true) {
852✔
507
                try {
853✔
508
                        console.log("Getting snippet from YouTube API...");
853✔
509

853✔
510
                        await fetch(`https://youtube.googleapis.com/youtube/v3/playlistItems?part=contentDetails&maxResults=50&pageToken=${pageToken}&playlistId=${playlistId}&key=${APIKey}`)
853✔
511
                                .then((response) => response.json())
853✔
512
                                .then((data) => apiResponse = data);
853✔
513

852✔
514
                        if (apiResponse["error"]) {
853✔
515
                                throw new YoutubeAPIError(
6✔
516
                                        apiResponse["error"]["code"],
6✔
517
                                        apiResponse["error"]["message"],
6✔
518
                                        apiResponse["error"]["errors"][0]["reason"],
6✔
519
                                        "",
6✔
520
                                        false,
6✔
521
                                        false
6✔
522
                                );
6✔
523
                        }
6✔
524

846✔
525
                        // We allow users to go beyond the daily limit in case there are only a few more videos to be fetched.
846✔
526
                        // But if it goes too far, we need to cancel the operation.
846✔
527
                        userQuotaRemainingToday--;
846✔
528
                        if (userQuotaRemainingToday <= -50 && !isCustomKey && !disregardUserQuota) {
853!
529
                                throw new RandomYoutubeVideoError(
×
530
                                        {
×
531
                                                code: "RYV-4B",
×
532
                                                message: "The channel you are shuffling from has too many uploads for the amount of API requests you can make. To protect the userbase, each user has a limited amount of requests they can make per day.",
×
533
                                                solveHint: "To shuffle from channels with more uploads, please use a custom API key.",
×
534
                                                showTrace: false,
×
535
                                                canSavePlaylist: false
×
536
                                        }
×
537
                                );
×
538
                        }
×
539

846✔
540
                        break;
846✔
541
                } catch (error) {
853✔
542
                        // Immediately set the user quota in sync storage, as we won't be able to do so correctly later due to the error
7✔
543
                        // We will set it again in the error handler and remove 1 from it, so we need to add 1 here to compensate
7✔
544
                        await setSyncStorageValue("userQuotaRemainingToday", Math.max(0, Math.min(200, userQuotaRemainingToday + 1)));
7✔
545

7✔
546
                        // We handle the case where an API key's quota was exceeded
7✔
547
                        if (error instanceof YoutubeAPIError && error.code === 403 && error.reason === "quotaExceeded") {
7✔
548
                                // We need to get another API key
3✔
549
                                if (!isCustomKey) {
3✔
550
                                        console.log("Quota for this key was exceeded, refreshing API keys and trying again...", true);
2✔
551

2✔
552
                                        // In case this is something irregular, we want to check if anything has changed with the API keys now
2✔
553
                                        // We can force this by setting the nextAPIKeysCheckTime to a time in the past
2✔
554
                                        await setSyncStorageValue("nextAPIKeysCheckTime", Date.now() - 100);
2✔
555
                                        ({ APIKey, isCustomKey, keyIndex } = await getAPIKey(keyIndex + 1));
2✔
556

2✔
557
                                        if (keyIndex === originalKeyIndex) {
2✔
558
                                                throw new RandomYoutubeVideoError(
1✔
559
                                                        {
1✔
560
                                                                code: "RYV-2",
1✔
561
                                                                message: "All YouTube API keys have exceeded the allocated daily quota.",
1✔
562
                                                                solveHint: "Please *immediately* inform the developer. You can try again tomorrow or provide a custom API key to immediately resolve this problem.",
1✔
563
                                                                showTrace: false,
1✔
564
                                                                canSavePlaylist: false
1✔
565
                                                        }
1✔
566
                                                );
1✔
567
                                        }
1✔
568
                                } else {
3✔
569
                                        throw new RandomYoutubeVideoError(
1✔
570
                                                {
1✔
571
                                                        code: "RYV-5",
1✔
572
                                                        message: "Your custom API key has reached its daily quota allocation.",
1✔
573
                                                        solveHint: "This can easily happen if the channels you are shuffling from have a lot of uploads, or if you are using the API key for something else as well. You need to wait until the quota is reset or use a different API key.",
1✔
574
                                                        showTrace: false,
1✔
575
                                                        canSavePlaylist: false
1✔
576
                                                }
1✔
577
                                        );
1✔
578
                                }
1✔
579
                        } else if (error instanceof YoutubeAPIError && error.code === 404 && error.reason === "playlistNotFound") {
7✔
580
                                throw new RandomYoutubeVideoError(
1✔
581
                                        {
1✔
582
                                                code: "RYV-6A",
1✔
583
                                                message: "This channel has not uploaded any videos.",
1✔
584
                                                showTrace: false,
1✔
585
                                                canSavePlaylist: false
1✔
586
                                        }
1✔
587
                                );
1✔
588
                        } else {
4✔
589
                                throw error;
3✔
590
                        }
3✔
591
                }
7✔
592
        }
853✔
593

846✔
594
        // If the user is using a custom key, we do not want to update the quota
846✔
595
        userQuotaRemainingToday = (isCustomKey || disregardUserQuota) ? originalUserQuotaRemainingToday : userQuotaRemainingToday;
852✔
596

852✔
597
        return { apiResponse, APIKey, isCustomKey, keyIndex, userQuotaRemainingToday };
852✔
598
}
852✔
599

1✔
600
// ---------- Utility ----------
1✔
601
async function testVideoExistence(videoId, uploadTime) {
6,482✔
602
        let videoAgeInDays = Math.floor((new Date() - new Date(uploadTime)) / (1000 * 60 * 60 * 24));
6,482✔
603

6,482✔
604
        let checkProbability;
6,482✔
605
        // For very new videos, we have just fetched them (we check every 48 hours), so they are very likely not deleted
6,482✔
606
        if (videoAgeInDays <= 2) {
6,482✔
607
                checkProbability = 0;
1,748✔
608
        } else if (videoAgeInDays <= 7) {
6,482✔
609
                checkProbability = 1;
2,939✔
610
        } else if (videoAgeInDays <= 60) {
4,734✔
611
                checkProbability = 0.5;
913✔
612
        } else {
1,795✔
613
                checkProbability = 0.2;
882✔
614
        }
882✔
615

6,482✔
616
        // We don't always want to check if a video exists, as this takes a lot of time
6,482✔
617
        if (Math.random() > checkProbability) {
6,482✔
618
                return true;
2,896✔
619
        }
2,896✔
620

3,586✔
621
        let videoExists;
3,586✔
622
        try {
3,586✔
623
                let response = await fetch(`https://www.youtube.com/oembed?url=http://www.youtube.com/watch?v=${videoId}&format=json`, {
3,586✔
624
                        method: "HEAD"
3,586✔
625
                });
3,586✔
626

3,586✔
627
                // 401 unauthorized means the video may exist, but cannot be embedded
3,586✔
628
                // As an alternative, we check if a thumbnail exists for this video id
3,586✔
629
                if (response.status === 401) {
6,482!
630
                        let thumbResponse = await fetch(`https://img.youtube.com/vi/${videoId}/0.jpg`, {
×
631
                                method: "HEAD"
×
632
                        });
×
633

×
634
                        if (thumbResponse.status !== 200) {
×
635
                                console.log(`Video doesn't exist: ${videoId}`);
×
636
                                videoExists = false;
×
637
                        } else {
×
638
                                videoExists = true;
×
639
                        }
×
640
                } else if (response.status !== 200) {
6,482✔
641
                        console.log(`Video doesn't exist: ${videoId}`);
353✔
642
                        videoExists = false;
353✔
643
                } else {
3,586✔
644
                        videoExists = true;
3,233✔
645
                }
3,233✔
646
        } catch (error) {
6,482!
647
                console.log(`An error was encountered while checking for video existence, so it is assumed the video does not exist: ${videoId}`);
×
648
                videoExists = false;
×
649
        }
×
650

3,586✔
651
        return videoExists;
3,586✔
652
}
6,482✔
653

1✔
654
async function isShort(videoId) {
149✔
655
        let videoIsShort;
149✔
656
        try {
149✔
657
                await fetch(`https://www.youtube.com/oembed?url=http://www.youtube.com/shorts/${videoId}&format=json`, {
149✔
658
                        method: "GET"
149✔
659
                }).then(res => res.json())
149✔
660
                        .then(res => {
149✔
661
                                if (res.thumbnail_url.endsWith("hq2.jpg")) {
133✔
662
                                        videoIsShort = true;
62✔
663
                                } else {
133✔
664
                                        videoIsShort = false;
71✔
665
                                }
71✔
666
                        });
149✔
667
                // We get an 'Unauthorized' response if the video cannot be embedded, which cannot be parsed as JSON using res.json()
133✔
668
                // This fallback tests if we get redirected to a normal video page, which means the video is not a short, but this takes longer
133✔
669
        } catch (error) {
149✔
670
                await fetch(`https://www.youtube.com/shorts/${videoId}`)
16✔
671
                        .then(res => {
16✔
672
                                if (res.redirected) {
16✔
673
                                        videoIsShort = false;
8✔
674
                                } else {
8✔
675
                                        videoIsShort = true;
8✔
676
                                }
8✔
677
                        });
16✔
678
        }
16✔
679
        return videoIsShort;
149✔
680
}
149✔
681

1✔
682
// Requests an API key from the background script
1✔
683
async function getAPIKey(useAPIKeyAtIndex = null) {
638✔
684
        const msg = {
638✔
685
                command: "getAPIKey",
638✔
686
                data: {
638✔
687
                        useAPIKeyAtIndex: useAPIKeyAtIndex
638✔
688
                }
638✔
689
        };
638✔
690

638✔
691
        // The response includes three parts: the API key, whether or not it is a custom key, and at which index of the list of API keys the current key is
638✔
692
        let { APIKey, isCustomKey, keyIndex } = await chrome.runtime.sendMessage(msg);
638✔
693

638✔
694
        if (!APIKey) {
638✔
695
                throw new RandomYoutubeVideoError(
1✔
696
                        {
1✔
697
                                code: "RYV-3",
1✔
698
                                message: "There are no API keys available in the database. It may be that they were removed for security reasons.",
1✔
699
                                solveHint: "Please check back later to see if this has been resolved, otherwise contact the developer. You can always use the extension by providing your custom API key via the popup, which is never uploaded to the extension's database.",
1✔
700
                                showTrace: false,
1✔
701
                                canSavePlaylist: false
1✔
702
                        }
1✔
703
                );
1✔
704
        }
1✔
705

637✔
706
        return { APIKey, isCustomKey, keyIndex };
637✔
707
}
638✔
708

1✔
709
async function chooseRandomVideosFromPlaylist(playlistInfo, channelId, shouldUpdateDatabase, progressTextElement, shuffleButtonTooltipElement) {
731✔
710
        let activeShuffleFilterOption = configSync.channelSettings[channelId]?.activeOption ?? "allVideosOption";
731✔
711
        let activeOptionValue;
731✔
712

731✔
713
        switch (activeShuffleFilterOption) {
731✔
714
                case "allVideosOption":
731✔
715
                        activeOptionValue = null;
616✔
716
                        break;
616✔
717
                case "dateOption":
731✔
718
                case "dateBeforeOption":
731✔
719
                        activeOptionValue = configSync.channelSettings[channelId]?.dateValue;
45✔
720
                        break;
45✔
721
                case "videoIdOption":
731✔
722
                case "videoIdBeforeOption":
731✔
723
                        activeOptionValue = configSync.channelSettings[channelId]?.videoIdValue;
52✔
724
                        break;
52✔
725
                case "percentageOption":
731✔
726
                        // The default is 100%, and we remove the setting from storage if it is 100% to save space
18✔
727
                        activeOptionValue = configSync.channelSettings[channelId]?.percentageValue ?? 100;
18!
728
                        break;
18✔
729
        }
731✔
730

731✔
731
        // If there is no value set for the active option, we alert the user
731✔
732
        if (activeOptionValue === undefined) {
731✔
733
                throw new RandomYoutubeVideoError(
36✔
734
                        {
36✔
735
                                code: "RYV-7",
36✔
736
                                message: `You have set an option to filter the videos that are shuffled (${activeShuffleFilterOption}), but no value for the option is set.`,
36✔
737
                                solveHint: "Please set a value for the active shuffle filter option in the popup, e.g. a valid date or video ID.",
36✔
738
                                showTrace: false,
36✔
739
                                canSavePlaylist: true
36✔
740
                        }
36✔
741
                );
36✔
742
        }
36✔
743

695✔
744
        // Sort all videos by date
695✔
745
        let allUnknownType = Object.assign({}, playlistInfo["videos"]["unknownType"], playlistInfo["newVideos"] ?? {});
731!
746
        let allKnownVideos = playlistInfo["videos"]["knownVideos"];
731✔
747
        let allKnownShorts = playlistInfo["videos"]["knownShorts"];
731✔
748
        let allVideos;
731✔
749

731✔
750
        // 0 = only shorts, 1 = no option set (shorts are included), 2 = ignore shorts
731✔
751
        if (configSync.shuffleIgnoreShortsOption == "1") {
731✔
752
                allVideos = Object.assign({}, allUnknownType, allKnownVideos, allKnownShorts);
677✔
753
        } else if (configSync.shuffleIgnoreShortsOption == "2") {
731✔
754
                allVideos = Object.assign({}, allUnknownType, allKnownVideos);
9✔
755
        } else {
9✔
756
                allVideos = Object.assign({}, allUnknownType, allKnownShorts);
9✔
757
        }
9✔
758

695✔
759
        let videosByDate = Object.keys(allVideos).sort((a, b) => {
695✔
760
                return new Date(allVideos[b]) - new Date(allVideos[a]);
53,036✔
761
        });
695✔
762

695✔
763
        let videosToShuffle = applyShuffleFilter(allVideos, videosByDate, activeShuffleFilterOption, activeOptionValue);
695✔
764

695✔
765
        let chosenVideos = [];
695✔
766
        let randomVideo;
695✔
767
        let encounteredDeletedVideos = false;
695✔
768

695✔
769
        const numVideosToChoose = configSync.shuffleOpenAsPlaylistOption ? configSync.shuffleNumVideosInPlaylist : 1;
731✔
770

731✔
771
        console.log(`Choosing ${numVideosToChoose} random video(s).`);
731✔
772

731✔
773
        // We keep track of the progress of determining the video types if there is a shorts handling filter, to display on the button
731✔
774
        let numVideosProcessed = 0;
731✔
775
        const initialTotalNumVideos = videosToShuffle.length;
731✔
776

731✔
777
        // We use this label to break out of both the for loop and the while loop if there are no more videos after encountering a deleted video
731✔
778
        outerLoop:
731✔
779
        for (let i = 0; i < numVideosToChoose; i++) {
731✔
780
                if (videosToShuffle.length === 0) {
6,210✔
781
                        // All available videos were chosen, so we need to terminate the loop early
81✔
782
                        console.log(`No more videos to choose from (${numVideosToChoose - i} videos too few uploaded on channel).`);
81✔
783
                        break outerLoop;
81✔
784
                }
81✔
785

6,129✔
786
                randomVideo = videosToShuffle[Math.floor(Math.random() * videosToShuffle.length)];
6,129✔
787
                numVideosProcessed++;
6,129✔
788

6,129✔
789
                // If the video does not exist, remove it from the playlist and choose a new one, until we find one that exists
6,129✔
790
                if (!await testVideoExistence(randomVideo, allVideos[randomVideo])) {
6,210✔
791
                        encounteredDeletedVideos = true;
292✔
792
                        // Update the database by removing the deleted videos there as well
292✔
793
                        shouldUpdateDatabase = true;
292✔
794
                        do {
292✔
795
                                // Remove the video from the local playlist object
353✔
796
                                delete playlistInfo["videos"][getVideoType(randomVideo, playlistInfo)][randomVideo];
353✔
797

353✔
798
                                // Remove the deleted video from the videosToShuffle array and choose a new random video
353✔
799
                                videosToShuffle.splice(videosToShuffle.indexOf(randomVideo), 1);
353✔
800
                                randomVideo = videosToShuffle[Math.floor(Math.random() * videosToShuffle.length)];
353✔
801
                                numVideosProcessed++;
353✔
802

353✔
803
                                console.log(`The chosen video does not exist any more, so it will be removed from the database. A new random video has been chosen: ${randomVideo}`);
353✔
804

353✔
805
                                if (randomVideo === undefined) {
353!
806
                                        // If we haven't chosen any videos yet, the channel does not contain any videos
×
807
                                        if (chosenVideos.length === 0) {
×
808
                                                throw new RandomYoutubeVideoError(
×
809
                                                        {
×
810
                                                                code: "RYV-6B",
×
811
                                                                message: "All previously uploaded videos on this channel were deleted (the channel does not have any uploads) or you are ignoring/only shuffling from shorts and the channel only has/has no shorts.",
×
812
                                                                solveHint: "If you are ignoring shorts, disable the option in the popup to shuffle from this channel.",
×
813
                                                                showTrace: false,
×
814
                                                                canSavePlaylist: true
×
815
                                                        }
×
816
                                                )
×
817
                                                // If we have chosen at least one video, we just return those
×
818
                                                /* c8 ignore start - Same behaviour as earlier, but this only triggers if the last chosen videos was a deleted one */
1✔
819
                                        } else {
1✔
820
                                                console.log(`No more videos to choose from (${numVideosToChoose - i} videos too few uploaded on channel).`);
1✔
821
                                                break outerLoop;
1✔
822
                                        }
1✔
823
                                }
1✔
824
                                /* c8 ignore stop */
1✔
825
                        } while (!await testVideoExistence(randomVideo, allVideos[randomVideo]))
292✔
826
                }
353✔
827

6,129✔
828
                // 0 = only shorts, 1 = no option set (shorts are included), 2 = ignore shorts
6,129✔
829
                // If the user does not want to shuffle from shorts, or only wants to shuffle from shorts, and we do not yet know the type of the chosen video, we check if it is a short or not
6,129✔
830
                if (configSync.shuffleIgnoreShortsOption != "1") {
6,210✔
831
                        if (playlistInfo["videos"]["unknownType"][randomVideo] !== undefined) {
171✔
832
                                const videoIsShort = await isShort(randomVideo);
149✔
833

149✔
834
                                // What follows is dependent on if the video is a short or not, and the user's settings
149✔
835
                                // Case 1: !isShort && ignoreShorts => Success
149✔
836
                                if (!videoIsShort && configSync.shuffleIgnoreShortsOption == "2") {
149✔
837
                                        // Move the video to the knownVideos sub-dictionary
39✔
838
                                        playlistInfo["videos"]["knownVideos"][randomVideo] = playlistInfo["videos"]["unknownType"][randomVideo];
39✔
839
                                        delete playlistInfo["videos"]["unknownType"][randomVideo];
39✔
840

39✔
841
                                        // The video is not a short, so add it to the list of chosen videos and remove it from the pool of videos to choose from
39✔
842
                                        chosenVideos.push(randomVideo);
39✔
843
                                        videosToShuffle.splice(videosToShuffle.indexOf(randomVideo), 1);
39✔
844

39✔
845
                                        // Case 2: isShort && ignoreShorts => Failure
39✔
846
                                } else if (videoIsShort && configSync.shuffleIgnoreShortsOption == "2") {
149✔
847
                                        console.log('A chosen video was a short, but shorts are ignored. Choosing a new random video.');
31✔
848

31✔
849
                                        // Move the video to the knownShorts sub-dictionary
31✔
850
                                        playlistInfo["videos"]["knownShorts"][randomVideo] = playlistInfo["videos"]["unknownType"][randomVideo];
31✔
851
                                        delete playlistInfo["videos"]["unknownType"][randomVideo];
31✔
852

31✔
853
                                        // Remove the video from videosToShuffle to not choose it again
31✔
854
                                        // Do not remove it from the playlistInfo object, as we do not want to delete it from the database
31✔
855
                                        videosToShuffle.splice(videosToShuffle.indexOf(randomVideo), 1);
31✔
856

31✔
857
                                        // We need to decrement i, as we did not choose a video in this iteration
31✔
858
                                        i--;
31✔
859

31✔
860
                                        // Case 3: isShort && onlyShorts => Success
31✔
861
                                } else if (videoIsShort && configSync.shuffleIgnoreShortsOption == "0") {
110✔
862
                                        // Move the video to the knownShorts sub-dictionary
39✔
863
                                        playlistInfo["videos"]["knownShorts"][randomVideo] = playlistInfo["videos"]["unknownType"][randomVideo];
39✔
864
                                        delete playlistInfo["videos"]["unknownType"][randomVideo];
39✔
865

39✔
866
                                        // The video is a short, so add it to the list of chosen videos and remove it from the pool of videos to choose from
39✔
867
                                        chosenVideos.push(randomVideo);
39✔
868
                                        videosToShuffle.splice(videosToShuffle.indexOf(randomVideo), 1);
39✔
869

39✔
870
                                        // Case 4: !isShort && onlyShorts => Failure
39✔
871
                                } else if (!videoIsShort && configSync.shuffleIgnoreShortsOption == "0") {
79✔
872
                                        console.log('A chosen video was not a short, but only shorts should be shuffled. Choosing a new random video.');
40✔
873

40✔
874
                                        // Move the video to the knownVideos sub-dictionary
40✔
875
                                        playlistInfo["videos"]["knownVideos"][randomVideo] = playlistInfo["videos"]["unknownType"][randomVideo];
40✔
876
                                        delete playlistInfo["videos"]["unknownType"][randomVideo];
40✔
877

40✔
878
                                        // Remove the video from videosToShuffle to not choose it again
40✔
879
                                        // Do not remove it from the playlistInfo object, as we do not want to delete it from the database
40✔
880
                                        videosToShuffle.splice(videosToShuffle.indexOf(randomVideo), 1);
40✔
881

40✔
882
                                        // We need to decrement i, as we did not choose a video in this iteration
40✔
883
                                        i--;
40✔
884

40✔
885
                                        /* c8 ignore start - This should never happen */
1✔
886
                                } else {
1✔
887
                                        throw new RandomYoutubeVideoError(
1✔
888
                                                {
1✔
889
                                                        code: "RYV-11B",
1✔
890
                                                        message: `An unknown error occurred while testing if the video ${randomVideo} should be included in the shuffle.`,
1✔
891
                                                        solveHint: "Please contact the developer, as this should not happen.",
1✔
892
                                                        showTrace: false,
1✔
893
                                                        canSavePlaylist: false
1✔
894
                                                }
1✔
895
                                        );
1✔
896
                                }
1✔
897
                                /* c8 ignore stop */
1✔
898
                        } else {
171✔
899
                                // Otherwise, the video must be a knownVideo, as we do not include knownShorts in allVideos
22✔
900
                                chosenVideos.push(randomVideo);
22✔
901
                                videosToShuffle.splice(videosToShuffle.indexOf(randomVideo), 1);
22✔
902
                        }
22✔
903

171✔
904
                        // Only if the shuffle started more than 1 second ago, update the button text
171✔
905
                        if (new Date() - shuffleStartTime > 1000) {
171!
906
                                // We display either the percentage of videos processed or the percentage of videos chosen (vs. needed), whichever is higher
×
907
                                const percentage = Math.max(Math.round(chosenVideos.length / numVideosToChoose * 100), Math.round(numVideosProcessed / initialTotalNumVideos * 100));
×
908
                                updateProgressTextElement(progressTextElement, `\xa0Sorting: ${percentage}%`, `${percentage}%`, shuffleButtonTooltipElement, "The extension is currently separating shorts and videos. Please wait...", "Sorting shorts...");
×
909
                        }
×
910
                } else {
6,210✔
911
                        // We are not ignoring shorts and the video exists
5,958✔
912
                        chosenVideos.push(randomVideo);
5,958✔
913
                        videosToShuffle.splice(videosToShuffle.indexOf(randomVideo), 1);
5,958✔
914
                }
5,958✔
915
        }
6,210✔
916

659✔
917
        // If we haven't chosen any videos: The channel has no uploads, or only shorts
659✔
918
        if (chosenVideos.length === 0) {
731✔
919
                throw new RandomYoutubeVideoError(
3✔
920
                        {
3✔
921
                                code: "RYV-6B",
3✔
922
                                message: "All previously uploaded videos on this channel were deleted (the channel does not have any uploads) or you are ignoring/only shuffling from shorts and the channel only has/has no shorts.",
3✔
923
                                solveHint: "If you are ignoring shorts, disable the option in the popup to shuffle from this channel.",
3✔
924
                                showTrace: false,
3✔
925
                                canSavePlaylist: true
3✔
926
                        }
3✔
927
                )
3✔
928
        }
3✔
929
        console.log(`${chosenVideos.length} random video${chosenVideos.length > 1 ? "s have" : " has"} been chosen: [${chosenVideos}]`);
731✔
930

731✔
931
        return { chosenVideos, playlistInfo, shouldUpdateDatabase, encounteredDeletedVideos };
731✔
932
}
731✔
933

1✔
934
function getVideoType(videoId, playlistInfo) {
353✔
935
        if (playlistInfo["videos"]["unknownType"][videoId]) {
353✔
936
                return "unknownType";
259✔
937
        } else if (playlistInfo["videos"]["knownVideos"][videoId]) {
353✔
938
                return "knownVideos";
54✔
939
        } else if (playlistInfo["videos"]["knownShorts"][videoId]) {
94✔
940
                return "knownShorts";
40✔
941
        }
40✔
942
        /* c8 ignore start - This will only happen if we forget to implement something here, so we do not need to test it */
1✔
943
        throw new RandomYoutubeVideoError(
1✔
944
                {
1✔
945
                        code: "RYV-11A",
1✔
946
                        message: `The video that was tested does not exist in the local playlist ${videoId}, so it's type could not be determined.`,
1✔
947
                        solveHint: "Please contact the developer, as this should not happen.",
1✔
948
                        showTrace: false,
1✔
949
                        canSavePlaylist: false
1✔
950
                }
1✔
951
        );
1✔
952
        /* c8 ignore stop */
1✔
953
}
353✔
954

1✔
955
// Applies a filter to the playlist object, based on the setting set in the popup
1✔
956
function applyShuffleFilter(allVideos, videosByDate, activeShuffleFilterOption, activeOptionValue) {
695✔
957
        let videosToShuffle;
695✔
958
        switch (activeShuffleFilterOption) {
695✔
959
                case "allVideosOption":
695✔
960
                        // For this option, no additional filtering is needed
616✔
961
                        videosToShuffle = videosByDate;
616✔
962
                        break;
616✔
963

695✔
964
                case "dateOption":
695✔
965
                        // Take only videos that were released after the specified date
18✔
966
                        videosToShuffle = videosByDate.filter((videoId) => {
18✔
967
                                return new Date(allVideos[videoId]) >= new Date(activeOptionValue);
590✔
968
                        });
18✔
969
                        // If the list is empty, alert the user
18✔
970
                        if (videosToShuffle.length === 0) {
18✔
971
                                throw new RandomYoutubeVideoError(
9✔
972
                                        {
9✔
973
                                                code: "RYV-8A",
9✔
974
                                                message: `There are no videos that were released after the specified date (${activeOptionValue}).`,
9✔
975
                                                solveHint: "Please change the date or use a different shuffle filter option.",
9✔
976
                                                showTrace: false,
9✔
977
                                                canSavePlaylist: true
9✔
978
                                        }
9✔
979
                                );
9✔
980
                        }
9✔
981
                        break;
9✔
982
                case "dateBeforeOption":
695✔
983
                        // Take only videos that were released before the specified date
9✔
984
                        videosToShuffle = videosByDate.filter((videoId) => {
9✔
985
                                return new Date(allVideos[videoId]) <= new Date(activeOptionValue);
295✔
986
                        });
9✔
987
                        if (videosToShuffle.length === 0) {
9!
988
                                throw new RandomYoutubeVideoError(
×
989
                                        {
×
990
                                                code: "RYV-8E",
×
991
                                                message: `There are no videos that were released before the specified date (${activeOptionValue}).`,
×
992
                                                solveHint: "Please change the date or use a different shuffle filter option.",
×
993
                                                showTrace: false,
×
994
                                                canSavePlaylist: true
×
995
                                        }
×
996
                                );
×
997
                        }
×
998
                        break;
9✔
999

695✔
1000
                case "videoIdOption":
695✔
1001
                        // Take only videos that were released after the specified video
17✔
1002
                        // The videos are already sorted by date, so we can just take the videos that are after the specified video in the list
17✔
1003
                        // If the specified video does not exist, we alert the user
17✔
1004
                        var videoIndex = videosByDate.indexOf(activeOptionValue);
17✔
1005
                        if (videoIndex === -1) {
17✔
1006
                                throw new RandomYoutubeVideoError(
9✔
1007
                                        {
9✔
1008
                                                code: "RYV-8B",
9✔
1009
                                                message: `The video ID you specified (${activeOptionValue}) does not map to a video uploaded on this channel.`,
9✔
1010
                                                solveHint: "Please fix the video ID or use a different shuffle filter option.",
9✔
1011
                                                showTrace: false,
9✔
1012
                                                canSavePlaylist: true
9✔
1013
                                        }
9✔
1014
                                );
9✔
1015
                        }
9✔
1016

8✔
1017
                        videosToShuffle = videosByDate.slice(0, videoIndex + 1);
8✔
1018

8✔
1019
                        // If the list is empty, alert the user
8✔
1020
                        if (videosToShuffle.length === 0) {
17!
1021
                                throw new RandomYoutubeVideoError(
×
1022
                                        {
×
1023
                                                code: "RYV-8C",
×
1024
                                                message: `There are no videos that were released after the specified video ID (${activeOptionValue}), or the newest video has not yet been added to the database.`,
×
1025
                                                solveHint: "The extension updates playlists every 48 hours, so please wait for an update, change the video ID used in the filer or use a different filter option.",
×
1026
                                                showTrace: false,
×
1027
                                                canSavePlaylist: true
×
1028
                                        }
×
1029
                                );
×
1030
                        }
×
1031
                        break;
8✔
1032
                case "videoIdBeforeOption":
695✔
1033
                        // Take only videos that were released before the specified video
17✔
1034
                        var videoBeforeIndex = videosByDate.indexOf(activeOptionValue);
17✔
1035
                        if (videoBeforeIndex === -1) {
17✔
1036
                                throw new RandomYoutubeVideoError(
9✔
1037
                                        {
9✔
1038
                                                code: "RYV-8B",
9✔
1039
                                                message: `The video ID you specified (${activeOptionValue}) does not map to a video uploaded on this channel.`,
9✔
1040
                                                solveHint: "Please fix the video ID or use a different shuffle filter option.",
9✔
1041
                                                showTrace: false,
9✔
1042
                                                canSavePlaylist: true
9✔
1043
                                        }
9✔
1044
                                );
9✔
1045
                        }
9✔
1046

8✔
1047
                        videosToShuffle = videosByDate.slice(videoBeforeIndex);
8✔
1048

8✔
1049
                        if (videosToShuffle.length === 0) {
17!
1050
                                throw new RandomYoutubeVideoError(
×
1051
                                        {
×
1052
                                                code: "RYV-8F",
×
1053
                                                message: `There are no videos that were released before the specified video ID (${activeOptionValue}), or the oldest video has not yet been added to the database.`,
×
1054
                                                solveHint: "The extension updates playlists every 48 hours, so please wait for an update, change the video ID used in the filter or use a different filter option.",
×
1055
                                                showTrace: false,
×
1056
                                                canSavePlaylist: true
×
1057
                                        }
×
1058
                                );
×
1059
                        }
×
1060
                        break;
8✔
1061

695✔
1062
                case "percentageOption":
695✔
1063
                        if (activeOptionValue < 1 || activeOptionValue > 100) {
18✔
1064
                                throw new RandomYoutubeVideoError(
9✔
1065
                                        {
9✔
1066
                                                code: "RYV-8D",
9✔
1067
                                                message: `The percentage you specified (${activeOptionValue}%) should be between 1 and 100. Normally, you should not be able to set such a value.`,
9✔
1068
                                                solveHint: "Please fix the percentage in the popup.",
9✔
1069
                                                showTrace: false,
9✔
1070
                                                canSavePlaylist: true
9✔
1071
                                        }
9✔
1072
                                );
9✔
1073
                        }
9✔
1074
                        // Take only a percentage of the videos, and then choose a random video from that subset
9✔
1075
                        videosToShuffle = videosByDate.slice(0, Math.max(1, Math.ceil(videosByDate.length * (activeOptionValue / 100))));
9✔
1076
                        break;
9✔
1077
        }
695✔
1078

659✔
1079
        return videosToShuffle;
659✔
1080
}
695✔
1081

1✔
1082
async function playVideo(chosenVideos, firedFromPopup) {
656✔
1083
        // Get the correct URL format
656✔
1084
        let randomVideoURL;
656✔
1085
        if (configSync.shuffleOpenAsPlaylistOption && chosenVideos.length > 1) {
656✔
1086
                const randomVideos = chosenVideos.join(",");
647✔
1087
                randomVideoURL = `https://www.youtube.com/watch_videos?video_ids=${randomVideos}`;
647✔
1088
        } else {
656✔
1089
                randomVideoURL = `https://www.youtube.com/watch?v=${chosenVideos[0]}`;
9✔
1090
        }
9✔
1091

656✔
1092
        // Find out if the reusable tab is still open (and on a youtube.com page)
656✔
1093
        const currentYouTubeTabs = await chrome.runtime.sendMessage({ command: "getAllYouTubeTabs" }) ?? [];
656!
1094
        const reusableTabExists = currentYouTubeTabs.find((tab) => tab.id === configSync.shuffleTabId);
656✔
1095

656✔
1096
        // Open the video in a new tab, the reusable tab or the current tab
656✔
1097
        // If the shuffle button from the popup was used, we always open the video in the 'same tab' (==the shuffling page)
656✔
1098
        // If the user wants to reuse tabs, we only open in a new tab if the reusable tab is not open any more
656✔
1099
        if (configSync.shuffleOpenInNewTabOption && !firedFromPopup) {
656✔
1100
                const pageType = getPageTypeFromURL(window.location.href);
638✔
1101
                // Video page: Pause the current video if it is playing
638✔
1102
                if (pageType === "video") {
638✔
1103
                        const player = document.querySelector('ytd-player#ytd-player')?.children[0]?.children[0];
638!
1104
                        if (player && player.classList.contains('playing-mode') && !player.classList.contains('unstarted-mode')) {
638!
1105
                                player.children[0].click();
×
1106
                        }
×
1107
                } else if (pageType === "channel") {
638!
1108
                        // Channel page: Pause the featured video if it exists and is playing
×
1109
                        const featuredPlayer = document.querySelector('ytd-player#player')?.children[0]?.children[0];
×
1110
                        if (featuredPlayer && featuredPlayer.classList.contains('playing-mode') && !featuredPlayer.classList.contains('unstarted-mode')) {
×
1111
                                featuredPlayer.children[0].click();
×
1112
                        }
×
1113
                        // Any page: Pause the mini-player if it exists and is playing
×
1114
                        const miniPlayer = document.querySelector('ytd-player#ytd-player')?.children[0]?.children[0];
×
1115
                        if (miniPlayer && miniPlayer.classList.contains('playing-mode') && !miniPlayer.classList.contains('unstarted-mode')) {
×
1116
                                miniPlayer.children[0].click();
×
1117
                        }
×
1118
                } else if (pageType === "short") {
×
1119
                        const player = document.querySelector('ytd-player#player')?.children[0]?.children[0];
×
1120
                        if (player && player.classList.contains('playing-mode') && !player.classList.contains('unstarted-mode')) {
×
1121
                                player.children[0].click();
×
1122
                        }
×
1123
                } else {
×
1124
                        console.log(`The current page type (${pageType}) is not supported when checking if a video player should be paused.`, true);
×
1125
                }
×
1126

638✔
1127
                // If there is a reusable tab and the option is enabled, open the video there
638✔
1128
                if (configSync.shuffleReUseNewTabOption && reusableTabExists) {
638✔
1129
                        aprilFoolsJoke();
9✔
1130

9✔
1131
                        // Focus the reusable tab and open the video there
9✔
1132
                        await chrome.runtime.sendMessage({ command: "openVideoInTabWithId", data: { tabId: configSync.shuffleTabId, videoUrl: randomVideoURL } });
9✔
1133

9✔
1134
                        // If there is no reusable tab or the option is disabled, open the video in a new tab
9✔
1135
                } else {
638✔
1136
                        window.open(randomVideoURL, '_blank').focus();
629✔
1137

629✔
1138
                        // Save the ID of the opened tab as the new reusable tab
629✔
1139
                        await setSyncStorageValue("shuffleTabId", await chrome.runtime.sendMessage({ command: "getCurrentTabId" }));
629✔
1140

629✔
1141
                        // April fools joke: Users get rickrolled once on April 1st every year
629✔
1142
                        // If we open both videos in a new tab, we want the rickroll to be focused
629✔
1143
                        aprilFoolsJoke();
629✔
1144
                }
629✔
1145
        } else {
656✔
1146
                if (firedFromPopup) {
18!
1147
                        // Save the ID of the current tab as the reusable tab, as it is a new page opened from the popup
×
1148
                        await setSyncStorageValue("shuffleTabId", await chrome.runtime.sendMessage({ command: "getCurrentTabId" }));
×
1149
                }
×
1150

18✔
1151
                // We need to open the rickroll first, as otherwise the function call doesn't happen, as we change the URL
18✔
1152
                aprilFoolsJoke();
18✔
1153

18✔
1154
                window.location.assign(randomVideoURL);
18✔
1155
        }
18✔
1156
}
656✔
1157

1✔
1158
// Once per year on April first, rickroll the user
1✔
1159
async function aprilFoolsJoke() {
656✔
1160
        const now = new Date();
656✔
1161
        if (now.getMonth() === 3 && now.getDate() === 1 && configSync.wasLastRickRolledInYear !== String(now.getFullYear())) {
656✔
1162
                await setSyncStorageValue("wasLastRickRolledInYear", String(now.getFullYear()));
18✔
1163

18✔
1164
                window.open("https://www.youtube.com/watch?v=dQw4w9WgXcQ", '_blank').focus();
18✔
1165
        }
18✔
1166
}
656✔
1167

1✔
1168
// ---------- Helper functions ----------
1✔
1169
// Join all videos into one object, useful for database interaction and filtering
1✔
1170
function getAllVideosFromLocalPlaylist(playlistInfo) {
1,207✔
1171
        return Object.assign({}, playlistInfo["videos"]["knownVideos"], playlistInfo["videos"]["knownShorts"], playlistInfo["videos"]["unknownType"]);
1,207✔
1172
}
1,207✔
1173

1✔
1174
/* c8 ignore start - We do not test this function as it will only trigger if we make a mistake programming somewhere, which will get caught by the test suite */
1✔
1175
function validatePlaylistInfo(playlistInfo) {
1✔
1176
        // The playlistInfo object must contain lastVideoPublishedAt, lastFetchedFromDB and videos
1✔
1177
        // The videos subkey must contain knownVideos, knownShorts and unknownType
1✔
1178
        if (!playlistInfo["lastVideoPublishedAt"] || !playlistInfo["lastFetchedFromDB"]
1✔
1179
                || !playlistInfo["videos"] || !playlistInfo["videos"]["knownVideos"] || !playlistInfo["videos"]["knownShorts"] || !playlistInfo["videos"]["unknownType"]) {
1✔
1180
                throw new RandomYoutubeVideoError(
1✔
1181
                        {
1✔
1182
                                code: "RYV-10",
1✔
1183
                                message: `The playlistInfo object is missing one or more required keys (Got: ${Object.keys(playlistInfo)}, videos key: ${Object.keys(playlistInfo["videos"]) ?? "No keys"}).`,
1✔
1184
                                solveHint: "Please try again and inform the developer if the error is not resolved.",
1✔
1185
                                showTrace: false,
1✔
1186
                                canSavePlaylist: false
1✔
1187
                        }
1✔
1188
                );
1✔
1189
        }
1✔
1190
        if (!playlistInfo["newVideos"]) {
1✔
1191
                playlistInfo["newVideos"] = {};
1✔
1192
        }
1✔
1193
}
1✔
1194
/* c8 ignore stop */
1✔
1195

1✔
1196
function updateProgressTextElement(progressTextElement, largeButtonText, smallButtonText, shuffleButtonTooltipElement = null, tooltipText = null, smallButtonTooltipText = null) {
487✔
1197
        if (progressTextElement.id.includes("large-shuffle-button") || progressTextElement.id == "fetchPercentageNoticeShufflingPage") {
487!
1198
                progressTextElement.innerText = largeButtonText;
×
1199
        } else {
487✔
1200
                // Make it the text style if no icon is set, otherwise the icon style
487✔
1201
                if (["shuffle", "close"].includes(smallButtonText)) {
487!
1202
                        updateSmallButtonStyleForText(progressTextElement, false);
×
1203
                } else {
487✔
1204
                        updateSmallButtonStyleForText(progressTextElement, true);
487✔
1205
                }
487✔
1206
                progressTextElement.innerText = smallButtonText;
487✔
1207
        }
487✔
1208

487✔
1209
        // Update the tooltip if requested
487✔
1210
        if (shuffleButtonTooltipElement) {
487!
1211
                if (progressTextElement.id.includes("large-shuffle-button")) {
×
1212
                        shuffleButtonTooltipElement.innerText = tooltipText;
×
1213
                } else if (smallButtonTooltipText) {
×
1214
                        shuffleButtonTooltipElement.innerText = smallButtonTooltipText;
×
1215
                }
×
1216
        }
×
1217
}
487✔
1218

1✔
1219
// ---------- Local storage ----------
1✔
1220
// Tries to fetch the playlist from local storage. If it is not present, returns an empty dictionary
1✔
1221
async function tryGetPlaylistFromLocalStorage(playlistId) {
954✔
1222
        return await chrome.storage.local.get([playlistId]).then(async (result) => {
954✔
1223
                if (result[playlistId] !== undefined) {
954✔
1224
                        /* c8 ignore start */
1✔
1225
                        // To fix a bug introduced in v2.2.1
1✔
1226
                        if (result[playlistId]["videos"] === undefined) {
1✔
1227
                                // Remove from localStorage
1✔
1228
                                await chrome.storage.local.remove([playlistId]);
1✔
1229
                                return {}
1✔
1230
                        }
1✔
1231
                        /* c8 ignore stop */
1✔
1232
                        return result[playlistId];
813✔
1233
                }
813✔
1234
                return {};
141✔
1235
        });
954✔
1236
}
954✔
1237

1✔
1238
async function savePlaylistToLocalStorage(playlistId, playlistInfo) {
731✔
1239
        // Update the playlist locally
731✔
1240
        console.log("Saving playlist to local storage...");
731✔
1241

731✔
1242
        // Only save the wanted keys
731✔
1243
        const playlistInfoForLocalStorage = {
731✔
1244
                // Remember the last time the playlist was accessed locally (==now)
731✔
1245
                "lastAccessedLocally": new Date().toISOString(),
731✔
1246
                "lastFetchedFromDB": playlistInfo["lastFetchedFromDB"] ?? new Date(0).toISOString(),
731!
1247
                "lastVideoPublishedAt": playlistInfo["lastVideoPublishedAt"] ?? new Date(0).toISOString().slice(0, 19) + 'Z',
731!
1248
                "videos": playlistInfo["videos"] ?? {}
731!
1249
        };
731✔
1250

731✔
1251
        await chrome.storage.local.set({ [playlistId]: playlistInfoForLocalStorage });
731✔
1252
}
731✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc