chore: update Angular cache and TypeScript build info

This commit is contained in:
Bruno Charest 2025-09-21 10:04:52 -04:00
parent 5d6aa17b81
commit 2420140cad
7 changed files with 174 additions and 43 deletions

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@ -70,5 +70,32 @@
"pathRewrite": { "pathRewrite": {
"^/proxy/api": "/api" "^/proxy/api": "/api"
} }
},
"/api": {
"target": "http://localhost:4000",
"secure": false,
"changeOrigin": true,
"logLevel": "debug"
},
"/assets/config.js": {
"target": "http://localhost:4000",
"secure": false,
"changeOrigin": true,
"logLevel": "debug"
},
"/assets/config.local.js": {
"target": "http://localhost:4000",
"secure": false,
"changeOrigin": true,
"logLevel": "debug"
},
"/api/odysee": {
"target": "http://localhost:4000",
"secure": false,
"changeOrigin": true,
"logLevel": "debug",
"pathRewrite": {
"^/api": "/api"
}
} }
} }

View File

@ -572,25 +572,93 @@ function formatListFromMeta(meta) {
// Routes under /api // Routes under /api
// -------------------- YouTube simple cache (GET) -------------------- // -------------------- YouTube simple cache (GET) --------------------
// Cache TTL defaults to 15 minutes, configurable via env YT_CACHE_TTL_MS // YouTube API key rotation and error handling (similar to Angular service)
const YT_CACHE_TTL_MS = Number(process.env.YT_CACHE_TTL_MS || 15 * 60 * 1000); const ytKeys = (() => {
/** @type {Map<string, { ts: number, data: any }>} */ try {
const ytCache = new Map(); const keys = process.env.YOUTUBE_API_KEYS;
if (keys && keys !== 'undefined' && keys !== 'null') {
return JSON.parse(keys);
}
} catch {}
const single = process.env.YOUTUBE_API_KEY;
return single ? [single] : [];
})();
let ytKeyIndex = 0;
const ytKeyBans = new Map(); // key -> bannedUntil epoch ms
// Ban duration (default 6h) can be overridden via env YT_KEY_BAN_MS
const YT_KEY_BAN_MS = Number(process.env.YT_KEY_BAN_MS || 6 * 60 * 60 * 1000);
function getActiveYouTubeKey() {
if (!ytKeys || ytKeys.length === 0) return null;
const now = Date.now();
// Find a non-banned key
for (let i = 0; i < ytKeys.length; i++) {
const key = ytKeys[ytKeyIndex % ytKeys.length];
ytKeyIndex = (ytKeyIndex + 1) % ytKeys.length;
const bannedUntil = ytKeyBans.get(key);
if (!bannedUntil || now > bannedUntil) {
return key;
}
}
return ytKeys[0]; // fallback to first key
}
function banYouTubeKey(key) {
if (!key) return;
const bannedUntil = Date.now() + YT_KEY_BAN_MS;
ytKeyBans.set(key, bannedUntil);
console.warn(`[YouTube API] Banned key ending with ...${key.slice(-4)} until ${new Date(bannedUntil).toISOString()}`);
}
function logYouTubeApiUsage(key, status, path) {
const shortKey = key ? `...${key.slice(-4)}` : 'none';
const logLevel = status >= 400 ? 'warn' : 'info';
console[logLevel](`[YouTube API] Key ${shortKey} - ${status} - ${path}`);
}
// Example: /api/yt/youtube/v3/videos?... -> https://www.googleapis.com/youtube/v3/videos?...
r.get('/yt/*', async (req, res) => { r.get('/yt/*', async (req, res) => {
try { try {
const googlePath = req.originalUrl.replace(/^\/api\/yt/, ''); const googlePath = req.originalUrl.replace(/^\/api\/yt/, '');
const targetUrl = `https://www.googleapis.com${googlePath}`; const targetUrl = `https://www.googleapis.com${googlePath}`;
const now = Date.now(); const now = Date.now();
const cached = ytCache.get(targetUrl); const cached = ytCache.get(targetUrl);
// Check if we have cached data and it's still valid
if (cached && (now - cached.ts) < YT_CACHE_TTL_MS) { if (cached && (now - cached.ts) < YT_CACHE_TTL_MS) {
return res.json(cached.data); return res.json(cached.data);
} }
const response = await axios.get(targetUrl, { timeout: 15000, validateStatus: s => s >= 200 && s < 400 });
const key = getActiveYouTubeKey();
if (!key) {
console.warn('[YouTube API] No API key available');
return res.status(503).json({ error: 'youtube_api_key_unavailable' });
}
// Add API key to the URL
const url = new URL(targetUrl);
url.searchParams.set('key', key);
const finalUrl = url.toString();
const response = await axios.get(finalUrl, { timeout: 15000, validateStatus: s => s >= 200 && s < 500 });
const status = response.status;
const data = response.data; const data = response.data;
ytCache.set(targetUrl, { ts: now, data });
return res.status(response.status || 200).json(data); // Log the usage
logYouTubeApiUsage(key, status, googlePath);
// Cache the result
ytCache.set(targetUrl, {
ts: now,
data: data,
status: status,
isError: status >= 400
});
return res.status(status).json(data);
} catch (e) { } catch (e) {
const status = e?.response?.status || 500; const status = e?.response?.status || 500;
const data = e?.response?.data || { error: 'yt_cache_upstream_error', details: String(e?.message || e) }; const data = e?.response?.data || { error: 'yt_cache_upstream_error', details: String(e?.message || e) };

View File

@ -11,14 +11,15 @@ import { AuthService } from '../../../../services/auth.service';
imports: [CommonModule, HttpClientModule], imports: [CommonModule, HttpClientModule],
// Le service est déjà fourni via providedIn: 'root' dans le service // Le service est déjà fourni via providedIn: 'root' dans le service
template: ` template: `
@if (isAuthenticated()) {
<button <button
(click)="onClick($event)" (click)="onClick($event)"
[class.text-red-500]="isLiked()" [class.text-red-500]="isLiked()"
[class.text-slate-400]="!isLiked()" [class.text-slate-400]="!isLiked()"
[class.hover\:text-red-500]="!isLiked()" [class.hover\:text-red-500]="!isLiked()"
[disabled]="isLoading() || !isAuthenticated()" [disabled]="isLoading()"
class="transition-colors duration-200 p-1 rounded-full hover:bg-slate-700/50 flex items-center justify-center" class="transition-colors duration-200 p-1 rounded-full hover:bg-slate-700/50 flex items-center justify-center"
[attr.aria-label]="!isAuthenticated() ? 'Connectez-vous pour aimer' : (isLiked() ? 'Retirer des vidéos aimées' : 'Ajouter aux vidéos aimées')" [attr.aria-label]="isLiked() ? 'Retirer des vidéos aimées' : 'Ajouter aux vidéos aimées'"
[attr.aria-pressed]="isLiked()" [attr.aria-pressed]="isLiked()"
> >
@if (isLoading()) { @if (isLoading()) {
@ -42,6 +43,7 @@ import { AuthService } from '../../../../services/auth.service';
<span class="sr-only">{{ isLiked() ? 'Retirer des vidéos aimées' : 'Ajouter aux vidéos aimées' }}</span> <span class="sr-only">{{ isLiked() ? 'Retirer des vidéos aimées' : 'Ajouter aux vidéos aimées' }}</span>
} }
</button> </button>
}
`, `,
styles: [ styles: [
` `
@ -107,11 +109,23 @@ export class LikeButtonComponent implements OnInit, OnDestroy, OnChanges {
ngOnInit() { ngOnInit() {
// Vérifie le statut du like à l'initialisation // Vérifie le statut du like à l'initialisation
this.isAuthenticated.set(!!this.auth.currentUser()); this.isAuthenticated.set(!!this.auth.currentUser());
if (!this.isAuthenticated()) {
// Utilisateur non connecté: ne pas appeler l'API et ne rien afficher
this.isLiked.set(false);
this.isLoading.set(false);
return;
}
this.checkLikeStatus(); this.checkLikeStatus();
} }
ngOnChanges(changes: SimpleChanges) { ngOnChanges(changes: SimpleChanges) {
// Vérifie le statut du like lorsque les propriétés d'entrée changent // Vérifie le statut du like lorsque les propriétés d'entrée changent
this.isAuthenticated.set(!!this.auth.currentUser());
if (!this.isAuthenticated()) {
this.isLiked.set(false);
this.isLoading.set(false);
return;
}
if (changes['videoId'] && !changes['videoId'].firstChange) { if (changes['videoId'] && !changes['videoId'].firstChange) {
this.checkLikeStatus(); this.checkLikeStatus();
} }
@ -134,6 +148,12 @@ export class LikeButtonComponent implements OnInit, OnDestroy, OnChanges {
this.isLoading.set(false); this.isLoading.set(false);
return; return;
} }
// Ne rien faire si l'utilisateur n'est pas connecté
if (!this.isAuthenticated()) {
this.isLiked.set(false);
this.isLoading.set(false);
return;
}
this.isLoading.set(true); this.isLoading.set(true);
try { try {

View File

@ -33,6 +33,11 @@ export const authInterceptor: HttpInterceptorFn = (req, next) => {
if (isRefreshCall) { if (isRefreshCall) {
return throwError(() => err); return throwError(() => err);
} }
// If there is no access token at all, we are not logged in: do not try refresh
const hasToken = !!auth.getAccessToken();
if (!hasToken) {
return throwError(() => err);
}
// Try a single refresh then retry the request once // Try a single refresh then retry the request once
return auth.refresh().pipe( return auth.refresh().pipe(
switchMap((ok) => { switchMap((ok) => {

View File

@ -217,7 +217,12 @@ export class YoutubeApiService {
chart: 'mostPopular', chart: 'mostPopular',
regionCode: String(region), regionCode: String(region),
maxResults: '24', maxResults: '24',
// Optimize quota usage: only get essential parts
part: 'snippet,contentDetails,statistics', part: 'snippet,contentDetails,statistics',
// Add safeSearch for better content filtering
safeSearch: 'moderate',
// Video category for trending (Music = 10, Gaming = 20, etc.)
videoCategoryId: '0', // All categories
key: String(key), key: String(key),
}); });
@ -371,11 +376,15 @@ export class YoutubeApiService {
if (!key) return of({ items: [], nextCursor: null }); if (!key) return of({ items: [], nextCursor: null });
const params = new URLSearchParams({ const params = new URLSearchParams({
type: 'video', type: 'video',
part: 'snippet', part: 'snippet,contentDetails,statistics',
// Reduce initial page size to 12 to limit payload; unit cost per call remains but fewer follow-up calls // Optimize for quota usage: reduce initial results
maxResults: '12', maxResults: '12',
q: String(query), q: String(query),
regionCode: String(region), regionCode: String(region),
// Add safeSearch for better content filtering
safeSearch: 'moderate',
// Video category filter for better relevance
videoCategoryId: '0', // All categories
key: String(key), key: String(key),
}); });
// Prefer results matching user's language when available // Prefer results matching user's language when available
@ -952,6 +961,8 @@ export class YoutubeApiService {
part: 'contentDetails,statistics', part: 'contentDetails,statistics',
id: ids.join(','), id: ids.join(','),
maxResults: String(Math.min(50, ids.length)), maxResults: String(Math.min(50, ids.length)),
// Add safeSearch for consistency
safeSearch: 'moderate',
}); });
return this.fetchYouTube('/youtube/v3/videos', params, apiKey).pipe( return this.fetchYouTube('/youtube/v3/videos', params, apiKey).pipe(
map((res: any) => Array.isArray(res?.items) ? res.items : []), map((res: any) => Array.isArray(res?.items) ? res.items : []),