chore: update Angular cache and TypeScript build info
This commit is contained in:
parent
5d6aa17b81
commit
2420140cad
2
.angular/cache/20.2.2/app/.tsbuildinfo
vendored
2
.angular/cache/20.2.2/app/.tsbuildinfo
vendored
File diff suppressed because one or more lines are too long
BIN
db/newtube.db
BIN
db/newtube.db
Binary file not shown.
@ -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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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) };
|
||||||
|
@ -11,37 +11,39 @@ 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: `
|
||||||
<button
|
@if (isAuthenticated()) {
|
||||||
(click)="onClick($event)"
|
<button
|
||||||
[class.text-red-500]="isLiked()"
|
(click)="onClick($event)"
|
||||||
[class.text-slate-400]="!isLiked()"
|
[class.text-red-500]="isLiked()"
|
||||||
[class.hover\:text-red-500]="!isLiked()"
|
[class.text-slate-400]="!isLiked()"
|
||||||
[disabled]="isLoading() || !isAuthenticated()"
|
[class.hover\:text-red-500]="!isLiked()"
|
||||||
class="transition-colors duration-200 p-1 rounded-full hover:bg-slate-700/50 flex items-center justify-center"
|
[disabled]="isLoading()"
|
||||||
[attr.aria-label]="!isAuthenticated() ? 'Connectez-vous pour aimer' : (isLiked() ? 'Retirer des vidéos aimées' : 'Ajouter aux vidéos aimées')"
|
class="transition-colors duration-200 p-1 rounded-full hover:bg-slate-700/50 flex items-center justify-center"
|
||||||
[attr.aria-pressed]="isLiked()"
|
[attr.aria-label]="isLiked() ? 'Retirer des vidéos aimées' : 'Ajouter aux vidéos aimées'"
|
||||||
>
|
[attr.aria-pressed]="isLiked()"
|
||||||
@if (isLoading()) {
|
>
|
||||||
<svg class="animate-spin h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
@if (isLoading()) {
|
||||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
<svg class="animate-spin h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||||
</svg>
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
|
||||||
<span class="sr-only">Chargement...</span>
|
</svg>
|
||||||
} @else {
|
<span class="sr-only">Chargement...</span>
|
||||||
<svg
|
} @else {
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
<svg
|
||||||
class="h-5 w-5"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 24 24"
|
class="h-5 w-5"
|
||||||
fill="currentColor"
|
viewBox="0 0 24 24"
|
||||||
[attr.aria-hidden]="true"
|
fill="currentColor"
|
||||||
>
|
[attr.aria-hidden]="true"
|
||||||
<path
|
>
|
||||||
[attr.d]="isLiked() ? 'M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z' : 'M16.5 3c-1.74 0-3.41.81-4.5 2.09C10.91 3.81 9.24 3 7.5 3 4.42 3 2 5.42 2 8.5c0 3.78 3.4 6.86 8.55 11.54L12 21.35l1.45-1.32C18.6 15.36 22 12.28 22 8.5 22 5.42 19.58 3 16.5 3zm-4.4 15.55l-.1.1-.1-.1C7.14 14.24 4 11.39 4 8.5 4 6.5 5.5 5 7.5 5c1.54 0 3.04.99 3.57 2.36h1.87C13.46 5.99 14.96 5 16.5 5c2 0 3.5 1.5 3.5 3.5 0 2.89-3.14 5.74-7.9 10.05z'"
|
<path
|
||||||
/>
|
[attr.d]="isLiked() ? 'M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z' : 'M16.5 3c-1.74 0-3.41.81-4.5 2.09C10.91 3.81 9.24 3 7.5 3 4.42 3 2 5.42 2 8.5c0 3.78 3.4 6.86 8.55 11.54L12 21.35l1.45-1.32C18.6 15.36 22 12.28 22 8.5 22 5.42 19.58 3 16.5 3zm-4.4 15.55l-.1.1-.1-.1C7.14 14.24 4 11.39 4 8.5 4 6.5 5.5 5 7.5 5c1.54 0 3.04.99 3.57 2.36h1.87C13.46 5.99 14.96 5 16.5 5c2 0 3.5 1.5 3.5 3.5 0 2.89-3.14 5.74-7.9 10.05z'"
|
||||||
</svg>
|
/>
|
||||||
<span class="sr-only">{{ isLiked() ? 'Retirer des vidéos aimées' : 'Ajouter aux vidéos aimées' }}</span>
|
</svg>
|
||||||
}
|
<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,7 +148,13 @@ 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 {
|
||||||
this.likesService.isLiked(this.provider, this.videoId)
|
this.likesService.isLiked(this.provider, this.videoId)
|
||||||
|
@ -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) => {
|
||||||
|
@ -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 : []),
|
||||||
|
Loading…
x
Reference in New Issue
Block a user