Compare commits

..

No commits in common. "f564567897f60885048cb11e3e06020eafe8afdf" and "ef6b82bb8b06d7e4fb74d80b7ad7e89b2acadc3d" have entirely different histories.

3 changed files with 4 additions and 47 deletions

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{
"name": "twitch-vod-manager",
"version": "4.6.18",
"version": "4.6.17",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "twitch-vod-manager",
"version": "4.6.18",
"version": "4.6.17",
"license": "MIT",
"dependencies": {
"axios": "^1.6.0",

View File

@ -1,6 +1,6 @@
{
"name": "twitch-vod-manager",
"version": "4.6.18",
"version": "4.6.17",
"description": "Twitch VOD Manager - Download Twitch VODs easily",
"main": "dist/main.js",
"author": "xRangerDE",

View File

@ -2319,44 +2319,6 @@ const MAX_STREAMER_PROFILE_CACHE_ENTRIES = 512;
const streamerProfileCache = new Map<string, CacheEntry<StreamerProfile>>();
const inFlightProfileRequests = new Map<string, Promise<StreamerProfile | null>>();
// Avatar bytes get embedded as data URLs in the profile so the renderer
// doesn't have to do its own HTTPS fetch (Electron's renderer img loader
// has a habit of failing silently against the Twitch CDN — undocumented,
// but reproducibly: the same URL works in DevTools but not in the live
// page). Cached by source URL so a single avatar change across multiple
// streamer entries only downloads once.
const avatarDataUrlCache = new Map<string, string>();
const MAX_AVATAR_DATA_URL_CACHE = 256;
async function fetchAvatarAsDataUrl(url: string): Promise<string> {
if (!url) return '';
const cached = avatarDataUrlCache.get(url);
if (cached !== undefined) return cached;
try {
const response = await axios.get<ArrayBuffer>(url, {
responseType: 'arraybuffer',
timeout: 8000,
headers: { 'User-Agent': 'TwitchVODManager/1.0' }
});
const buf = Buffer.from(response.data);
// Twitch CDN almost always serves PNG or JPEG. Detect from the
// response content-type when available, fall back to PNG which is
// the default for profile_image_url.
const contentType = (response.headers['content-type'] as string | undefined)?.split(';')[0]?.trim() || 'image/png';
const dataUrl = `data:${contentType};base64,${buf.toString('base64')}`;
avatarDataUrlCache.set(url, dataUrl);
if (avatarDataUrlCache.size > MAX_AVATAR_DATA_URL_CACHE) {
// FIFO eviction — Map preserves insertion order.
const firstKey = avatarDataUrlCache.keys().next().value as string | undefined;
if (firstKey) avatarDataUrlCache.delete(firstKey);
}
return dataUrl;
} catch (e) {
appendDebugLog('avatar-fetch-failed', { url, error: String(e) });
return '';
}
}
interface HelixUser {
id: string;
login: string;
@ -2515,15 +2477,10 @@ async function getStreamerProfile(login: string, forceRefresh = false): Promise<
}
} catch (_) { /* best-effort */ }
// Embed the avatar bytes as a data URL so the renderer doesn't
// have to make its own HTTPS request. Bypasses any CSP, CORS,
// referrer-policy, or Electron renderer image-loading quirks.
const avatarDataUrl = avatarUrl ? await fetchAvatarAsDataUrl(avatarUrl) : '';
const profile: StreamerProfile = {
login: normalized,
displayName,
avatarUrl: avatarDataUrl || avatarUrl,
avatarUrl,
description,
broadcasterType,
followerCount,