Compare commits
2 Commits
ef6b82bb8b
...
f564567897
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f564567897 | ||
|
|
b1fd73cbe6 |
4
package-lock.json
generated
4
package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "twitch-vod-manager",
|
||||
"version": "4.6.17",
|
||||
"version": "4.6.18",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "twitch-vod-manager",
|
||||
"version": "4.6.17",
|
||||
"version": "4.6.18",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"axios": "^1.6.0",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "twitch-vod-manager",
|
||||
"version": "4.6.17",
|
||||
"version": "4.6.18",
|
||||
"description": "Twitch VOD Manager - Download Twitch VODs easily",
|
||||
"main": "dist/main.js",
|
||||
"author": "xRangerDE",
|
||||
|
||||
45
src/main.ts
45
src/main.ts
@ -2319,6 +2319,44 @@ 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;
|
||||
@ -2477,10 +2515,15 @@ 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,
|
||||
avatarUrl: avatarDataUrl || avatarUrl,
|
||||
description,
|
||||
broadcasterType,
|
||||
followerCount,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user