fix(auth,download): playlists/likes 401, suppression downloads, 429 formats
CI / build-and-test (push) Failing after 8m20s

- likes + playlists: authMiddlewareCookieAware (cookies + Bearer) au lieu
  du Bearer seul -> corrige 401 Unauthorized meme connecte (creation
  playlist, ajout video, bouton J'aime)
- add-to-playlist/like-button/search-box: retire HttpClientModule local
  qui court-circuitait les intercepteurs fonctionnels
- intercepteur: refresh+retry sur 401 meme sans token en memoire (F5)
- UserService: apiBase() au lieu de /proxy/api en dur (prod :4000)
- J'aime: envoie titre/miniature, erreur visible au lieu de bouton mort
- download: seaux separes (lectures 120/min, ecritures 30/min, formats
  30/min) + reponses limiter en JSON rate_limited + Retry-After
- formats: cache memoire 10 min + ytdlpNetOpts + retry front avec backoff
- polling allege (downloads 5s, suivi job 4s, 429 transitoire non fatal)
- erreurs FR precises (http-error.util): 401/403/404/410/429/quota/500
  avec compte a rebours + bouton Reessayer
This commit is contained in:
2026-09-26 09:26:44 -04:00
parent 2db1f2deee
commit d5ceab7115
11 changed files with 482 additions and 73 deletions
+79
View File
@@ -0,0 +1,79 @@
import { HttpErrorResponse } from '@angular/common/http';
/** Code d'erreur métier renvoyé par le backend : { error: 'rate_limited', ... }. */
export function backendErrorCode(err: unknown): string | null {
try {
const body = (err as HttpErrorResponse)?.error;
if (body && typeof body === 'object' && typeof body.error === 'string') return body.error;
if (typeof body === 'string') {
const m = /"error"\s*:\s*"([a-z_]+)"/i.exec(body);
if (m) return m[1];
}
} catch {}
return null;
}
/**
* Secondes avant de réessayer après un 429 : backend { retryAfterSec } >
* header Retry-After > header RateLimit-Reset (epoch secondes) > défaut.
*/
export function retryAfterSeconds(err: unknown, fallback = 30): number {
try {
const e = err as HttpErrorResponse;
const body = e?.error;
if (body && typeof body === 'object' && Number.isFinite(Number(body.retryAfterSec))) {
return Math.max(1, Math.ceil(Number(body.retryAfterSec)));
}
const headers = e?.headers;
const retryAfter = headers?.get?.('Retry-After');
if (retryAfter && Number.isFinite(Number(retryAfter))) return Math.max(1, Math.ceil(Number(retryAfter)));
const reset = headers?.get?.('RateLimit-Reset');
if (reset && Number.isFinite(Number(reset))) {
const nowSec = Date.now() / 1000;
const wait = Math.ceil(Number(reset) - nowSec);
if (wait >= 1 && wait <= 3600) return wait;
}
} catch {}
return fallback;
}
/** Vrai quand l'échec est transitoire et mérite un retry (429 / réseau / 5xx). */
export function isRetryableStatus(status: number | undefined): boolean {
return status === 0 || status === 429 || (typeof status === 'number' && status >= 500 && status < 600);
}
/**
* Message FR prêt à afficher pour les erreurs du module téléchargement.
* Ne fuit jamais le corps brut (HTML / « Http failure response... »).
*/
export function frenchDownloadError(err: unknown): string {
const e = err as HttpErrorResponse;
const status = typeof e?.status === 'number' ? e.status : undefined;
const code = backendErrorCode(err);
if (status === 0) return 'Serveur de téléchargement injoignable. Vérifiez que le backend tourne puis réessayez.';
if (status === 401) return 'Session expirée. Reconnectez-vous puis réessayez.';
if (status === 403) {
if (code === 'download_disabled_for_provider' || code === 'forbidden') {
return code === 'forbidden'
? 'Action interdite (ressource d’un autre compte).'
: 'Le téléchargement est désactivé pour ce provider.';
}
return 'Action interdite.';
}
if (status === 404) return 'Ce téléchargement n’existe plus (déjà supprimé ou autre compte). Actualisez la liste.';
if (status === 410) return 'Le fichier n’est plus présent sur le serveur (nettoyé). Relancez un téléchargement.';
if (status === 429) {
if (code === 'too_many_downloads') return 'Trop de téléchargements en parallèle. Attendez la fin d’un job puis réessayez.';
if (code === 'storage_quota_exceeded') return 'Quota de stockage atteint. Supprimez d’anciens fichiers puis réessayez.';
const wait = retryAfterSeconds(err);
return `Trop de requêtes pour le moment (limite anti-abus). Réessayez dans ~${wait} s.`;
}
if (status === 400) {
if (code === 'job_not_retryable') return 'Ce job ne peut pas être relancé dans son état actuel.';
return 'Requête invalide.';
}
if (code === 'formats_failed' || status === 500) {
return 'Impossible de récupérer les formats (service vidéo indisponible). Réessayez dans un instant.';
}
return 'Opération impossible pour le moment. Réessayez plus tard.';
}