119 lines
3.6 KiB
TypeScript
119 lines
3.6 KiB
TypeScript
import browser from "webextension-polyfill";
|
|
import type { Message } from "../shared/messages";
|
|
import type { DetailUpdate } from "../types/playlist";
|
|
import { CATEGORY_MAP } from "../shared/category-map";
|
|
|
|
const LOG = "[yt-playlist-features:bg]";
|
|
const DEFAULT_API_KEY = "AIzaSyDPyWG3ABnVV3en_KBhIxUH6O2_A0oP4Wk";
|
|
const BATCH_SIZE = 50;
|
|
|
|
async function getApiKey(): Promise<string> {
|
|
const result = await browser.storage.sync.get("apiKey");
|
|
return (result.apiKey as string) || DEFAULT_API_KEY;
|
|
}
|
|
|
|
const BATCH_DELAY = 500; // ms between batches
|
|
const RETRY_DELAY = 5000; // ms before retrying after rate limit
|
|
const MAX_RETRIES = 3;
|
|
|
|
async function fetchVideoDetails(
|
|
videoIds: string[],
|
|
tabId: number,
|
|
): Promise<void> {
|
|
const apiKey = await getApiKey();
|
|
if (!apiKey) {
|
|
browser.tabs.sendMessage(tabId, {
|
|
type: "VIDEO_DETAILS_ERROR",
|
|
error: "no-api-key",
|
|
} satisfies Message);
|
|
return;
|
|
}
|
|
|
|
for (let i = 0; i < videoIds.length; i += BATCH_SIZE) {
|
|
if (i > 0) await new Promise((r) => setTimeout(r, BATCH_DELAY));
|
|
|
|
const batch = videoIds.slice(i, i + BATCH_SIZE);
|
|
const ids = batch.join(",");
|
|
const url = `https://www.googleapis.com/youtube/v3/videos?part=snippet,statistics&id=${ids}&key=${apiKey}`;
|
|
|
|
let success = false;
|
|
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
|
try {
|
|
if (attempt > 0) {
|
|
console.log(LOG, `Retry ${attempt} after rate limit, waiting ${RETRY_DELAY}ms...`);
|
|
await new Promise((r) => setTimeout(r, RETRY_DELAY));
|
|
}
|
|
|
|
const res = await fetch(url);
|
|
if (res.status === 403) {
|
|
console.warn(LOG, `Rate limited (attempt ${attempt + 1}/${MAX_RETRIES})`);
|
|
continue;
|
|
}
|
|
if (!res.ok) {
|
|
const body = await res.text();
|
|
console.error(LOG, `API error ${res.status}:`, body);
|
|
browser.tabs.sendMessage(tabId, {
|
|
type: "VIDEO_DETAILS_ERROR",
|
|
error: `api-error-${res.status}`,
|
|
} satisfies Message);
|
|
return;
|
|
}
|
|
|
|
const data = await res.json();
|
|
const updates: DetailUpdate[] = (data.items ?? []).map((item: any) => ({
|
|
videoId: item.id,
|
|
viewCountText: item.statistics?.viewCount
|
|
? parseInt(item.statistics.viewCount, 10).toLocaleString()
|
|
: null,
|
|
publishedAt: item.snippet?.publishedAt?.slice(0, 10) ?? null,
|
|
category: CATEGORY_MAP[item.snippet?.categoryId] ?? null,
|
|
}));
|
|
|
|
browser.tabs.sendMessage(tabId, {
|
|
type: "VIDEO_DETAILS_UPDATE",
|
|
updates,
|
|
} satisfies Message);
|
|
success = true;
|
|
break;
|
|
} catch (e) {
|
|
console.error(LOG, "Fetch failed:", e);
|
|
browser.tabs.sendMessage(tabId, {
|
|
type: "VIDEO_DETAILS_ERROR",
|
|
error: "network-error",
|
|
} satisfies Message);
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (!success) {
|
|
console.error(LOG, "Max retries exceeded for batch");
|
|
browser.tabs.sendMessage(tabId, {
|
|
type: "VIDEO_DETAILS_ERROR",
|
|
error: "rate-limit-exceeded",
|
|
} satisfies Message);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
browser.runtime.onMessage.addListener(
|
|
async (message: unknown, sender: browser.Runtime.MessageSender) => {
|
|
const msg = message as Message;
|
|
|
|
if (msg.type === "PLAYLIST_EXTRACTED") {
|
|
console.log(
|
|
LOG,
|
|
`Playlist: "${msg.data.metadata.title}" (${msg.data.extractedCount} videos)`,
|
|
);
|
|
}
|
|
|
|
if (msg.type === "FETCH_VIDEO_DETAILS") {
|
|
const tabId = sender.tab?.id;
|
|
if (!tabId) return;
|
|
fetchVideoDetails(msg.videoIds, tabId);
|
|
}
|
|
},
|
|
);
|
|
|
|
console.log(LOG, "Service worker started.");
|