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
@@ -3,6 +3,7 @@ import { CommonModule } from '@angular/common';
import { RouterLink } from '@angular/router';
import { DownloadService, type DownloadJob, type DownloadQuota } from '../../../services/download.service';
import { AuthService } from '../../../services/auth.service';
import { frenchDownloadError } from '../../../utils/http-error.util';
import { TranslatePipe } from '../../../pipes/translate.pipe';
import { ProviderBadgeComponent } from '../../../app/shared/components/provider-badge/provider-badge.component';
@@ -88,14 +89,15 @@ export class DownloadsComponent {
retry(job: DownloadJob): void {
if (this.busyIds().has(job.id)) return;
this.markBusy(job.id, true);
this.error.set(null);
this.downloads.retryJob(job.id).subscribe({
next: () => {
this.markBusy(job.id, false);
this.reload();
},
error: () => {
error: (err) => {
this.markBusy(job.id, false);
this.error.set('Nouvel essai impossible pour le moment.');
this.error.set(frenchDownloadError(err));
},
});
}
@@ -103,14 +105,16 @@ export class DownloadsComponent {
cancel(job: DownloadJob): void {
if (this.busyIds().has(job.id)) return;
this.markBusy(job.id, true);
this.error.set(null);
this.downloads.cancelJob(job.id).subscribe({
next: () => {
this.markBusy(job.id, false);
this.items.update((list) => list.filter((j) => j.id !== job.id));
this.syncPoller();
},
error: () => {
error: (err) => {
this.markBusy(job.id, false);
this.error.set('Suppression impossible pour le moment.');
this.error.set(frenchDownloadError(err));
},
});
}
@@ -188,7 +192,9 @@ export class DownloadsComponent {
private syncPoller(): void {
const hasActive = this.items().some((j) => ACTIVE_STATES.has(j.state));
if (hasActive && !this.poller) {
this.poller = setInterval(() => this.refreshActive(), 3000);
// 5 s : le polling agressif (3 s) saturait à lui seul le seau
// anti-abus et affamait suppressions + formats (429).
this.poller = setInterval(() => this.refreshActive(), 5000);
} else if (!hasActive && this.poller) {
clearInterval(this.poller);
this.poller = null;