first commit

This commit is contained in:
2025-09-14 23:05:30 -04:00
commit 2866d74b32
122 changed files with 95474 additions and 0 deletions
@@ -0,0 +1,171 @@
import { ChangeDetectionStrategy, Component, computed, inject, signal } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ActivatedRoute, RouterLink } from '@angular/router';
import { ThemesService } from '../../services/themes.service';
import { YoutubeApiService } from '../../services/youtube-api.service';
import { Provider, InstanceService } from '../../services/instance.service';
import { Video } from '../../models/video.model';
import { TranslatePipe } from '../../pipes/translate.pipe';
interface ProviderBlock {
provider: Provider;
label: string;
loading: boolean;
error: string | null;
items: Video[];
}
@Component({
selector: 'app-theme-page',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: './theme-page.component.html',
imports: [CommonModule, RouterLink, TranslatePipe]
})
export class ThemePageComponent {
private route = inject(ActivatedRoute);
themes = inject(ThemesService);
private api = inject(YoutubeApiService);
private instances = inject(InstanceService);
// Show all supported providers on Tendances
private providersToShow: Provider[] = ['youtube', 'twitch', 'dailymotion', 'peertube', 'rumble', 'odysee'];
readonly themeSlug = signal<string>('trending');
readonly themeLabel = computed(() => this.themes.bySlug(this.themeSlug())?.label || this.themeSlug());
blocks = signal<ProviderBlock[]>([]);
constructor() {
this.route.paramMap.subscribe(pm => {
const slug = pm.get('theme') || pm.get('slug') || 'trending';
this.themeSlug.set(slug);
this.loadAllBlocks();
});
}
private loadAllBlocks() {
const slug = this.themeSlug();
const blocks: ProviderBlock[] = this.providersToShow.map(p => ({ provider: p, label: this.labelFor(p), loading: true, error: null, items: [] }));
this.blocks.set(blocks);
for (const b of blocks) this.fetchForProvider(b, slug);
}
private tokensForTheme(slug: string): string[] {
const tokens = this.themes.tokensFor(slug);
// Special cases
if (slug === 'trending') return [];
if (slug === 'live') return ['live'];
return tokens;
}
private labelFor(p: Provider): string {
switch (p) {
case 'youtube': return 'YouTube';
case 'twitch': return 'Twitch';
case 'peertube': return 'PeerTube';
case 'rumble': return 'Rumble';
case 'odysee': return 'Odysee';
case 'dailymotion': return 'Dailymotion';
}
}
private fetchForProvider(block: ProviderBlock, slug: string) {
// Provider readiness (e.g., YouTube key, Twitch creds). If not ready, surface reason and skip network calls.
const ready = this.instances.getProviderReadiness(block.provider);
if (!ready.ready) {
block.loading = false;
block.error = ready.reason || 'Provider not ready';
console.warn('[ThemePage] Provider not ready', { provider: block.provider, theme: slug, reason: block.error });
this.blocks.update(a => [...a]);
return;
}
// If YouTube quota already exceeded, surface and skip
if (block.provider === 'youtube' && this.api.quotaExceeded$.getValue()) {
block.loading = false;
block.error = 'YouTube quota exceeded. Try again later or switch provider.';
console.warn('[ThemePage] Skip fetch: YouTube quota exceeded', { theme: slug });
this.blocks.update(a => [...a]);
return;
}
const tokens = this.tokensForTheme(slug);
const max = 10;
block.loading = true;
block.error = null;
if (slug === 'trending') {
this.api.getTrendingPage(null, block.provider).subscribe({
next: res => {
block.items = (res.items || []).slice(0, max);
block.loading = false;
if (!block.items.length) console.info('[ThemePage] Empty block page', { provider: block.provider, theme: slug });
this.blocks.update(a => [...a]);
},
error: (e) => {
if (block.provider === 'youtube' && e?.status === 403 && (e?.error?.error?.errors?.[0]?.reason === 'quotaExceeded' || /quota/i.test(String(e?.error?.error?.message || e?.message || '')))) {
this.api.quotaExceeded$.next(true);
block.error = 'YouTube quota exceeded. Try again later or switch provider.';
} else {
block.error = 'Erreur de chargement';
}
block.loading = false; this.blocks.update(a => [...a]);
}
});
return;
}
const q = tokens.join(' ');
if (!q) {
// fallback to trending
this.api.getTrendingPage(null, block.provider).subscribe({
next: res => {
block.items = (res.items || []).slice(0, max);
block.loading = false;
if (!block.items.length) console.info('[ThemePage] Empty block page', { provider: block.provider, theme: slug, mode: 'fallback_trending' });
this.blocks.update(a => [...a]);
},
error: (e) => {
if (block.provider === 'youtube' && e?.status === 403 && (e?.error?.error?.errors?.[0]?.reason === 'quotaExceeded' || /quota/i.test(String(e?.error?.error?.message || e?.message || '')))) {
this.api.quotaExceeded$.next(true);
block.error = 'YouTube quota exceeded. Try again later or switch provider.';
} else {
block.error = 'Erreur de chargement';
}
block.loading = false; this.blocks.update(a => [...a]);
}
});
} else {
this.api.searchVideosPage(q, null, block.provider).subscribe({
next: res => {
block.items = (res.items || []).slice(0, max);
block.loading = false;
if (!block.items.length) console.info('[ThemePage] Empty block page', { provider: block.provider, theme: slug, query: q });
this.blocks.update(a => [...a]);
},
error: (e) => {
if (block.provider === 'youtube' && e?.status === 403 && (e?.error?.error?.errors?.[0]?.reason === 'quotaExceeded' || /quota/i.test(String(e?.error?.error?.message || e?.message || '')))) {
this.api.quotaExceeded$.next(true);
block.error = 'YouTube quota exceeded. Try again later or switch provider.';
} else {
block.error = 'Erreur de chargement';
}
block.loading = false; this.blocks.update(a => [...a]);
}
});
}
}
watchQueryParams(provider: Provider, v: Video): Record<string, any> {
const qp: any = { p: provider };
if (provider === 'odysee' && v.url?.startsWith('https://odysee.com/')) {
try {
let slug = v.url.substring('https://odysee.com/'.length);
if (slug.startsWith('/')) slug = slug.slice(1);
qp.slug = slug;
} catch {}
}
if (provider === 'twitch' && v.type === 'channel') {
// Try to use channel name
if (v.videoId) qp.channel = v.videoId;
}
return qp;
}
}