chore: update Angular cache files and TypeScript build info

This commit is contained in:
Bruno Charest 2025-09-17 21:27:20 -04:00
parent 3dbfb04b15
commit f3a78b7d7e
12 changed files with 60 additions and 19 deletions

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@ -73,6 +73,7 @@ CREATE TABLE IF NOT EXISTS watch_history (
provider TEXT NOT NULL, provider TEXT NOT NULL,
video_id TEXT NOT NULL, video_id TEXT NOT NULL,
title TEXT, title TEXT,
thumbnail TEXT,
watched_at TEXT NOT NULL, watched_at TEXT NOT NULL,
progress_seconds INTEGER DEFAULT 0, progress_seconds INTEGER DEFAULT 0,
duration_seconds INTEGER DEFAULT 0, duration_seconds INTEGER DEFAULT 0,
@ -145,5 +146,6 @@ CREATE TABLE IF NOT EXISTS video_tags (
provider TEXT NOT NULL, provider TEXT NOT NULL,
video_id TEXT NOT NULL, video_id TEXT NOT NULL,
tag_id TEXT NOT NULL REFERENCES tags(id) ON DELETE CASCADE, tag_id TEXT NOT NULL REFERENCES tags(id) ON DELETE CASCADE,
created_at TEXT NOT NULL,
PRIMARY KEY (user_id, provider, video_id, tag_id) PRIMARY KEY (user_id, provider, video_id, tag_id)
); );

View File

@ -279,11 +279,17 @@ function ensureTag(userId, name) {
return id; return id;
} }
export function likeVideo({ userId, provider, videoId }) { export function likeVideo({ userId, provider, videoId, title, thumbnail }) {
const tagId = ensureTag(userId, 'like'); const tagId = ensureTag(userId, 'like');
// Upsert-like behavior; ignore if exists // Upsert-like behavior; ignore if exists
db.prepare(`INSERT OR IGNORE INTO video_tags (user_id, provider, video_id, tag_id) VALUES (?, ?, ?, ?)`) db.prepare(`INSERT OR IGNORE INTO video_tags (user_id, provider, video_id, tag_id, created_at) VALUES (?, ?, ?, ?, ?)`)
.run(userId, provider, videoId, tagId); .run(userId, provider, videoId, tagId, nowIso());
// Also update the watch_history table with the title and thumbnail
if (title || thumbnail) {
upsertWatchHistory({ userId, provider, videoId, title, thumbnail });
}
return { provider, video_id: videoId }; return { provider, video_id: videoId };
} }
@ -350,6 +356,7 @@ export function listLikedVideos({ userId, limit = 100, q }) {
SELECT SELECT
vt.provider, vt.provider,
vt.video_id, vt.video_id,
vt.created_at,
COALESCE(wh.title, '') AS title, COALESCE(wh.title, '') AS title,
COALESCE(wh.thumbnail, '') AS thumbnail, COALESCE(wh.thumbnail, '') AS thumbnail,
wh.last_watched_at AS last_watched_at wh.last_watched_at AS last_watched_at
@ -361,7 +368,7 @@ export function listLikedVideos({ userId, limit = 100, q }) {
WHERE vt.user_id = ? AND vt.tag_id = ? WHERE vt.user_id = ? AND vt.tag_id = ?
`; `;
const orderLimit = ` const orderLimit = `
ORDER BY COALESCE(wh.last_watched_at, wh.watched_at) DESC ORDER BY vt.created_at DESC
LIMIT ? LIMIT ?
`; `;
const query = hasQ const query = hasQ

View File

@ -1058,10 +1058,36 @@ r.get('/user/likes', authMiddleware, (req, res) => {
return res.json(rows); return res.json(rows);
}); });
r.post('/user/likes', authMiddleware, (req, res) => { r.post('/user/likes', authMiddleware, async (req, res) => {
const { provider, videoId } = req.body || {}; let { provider, videoId, title, thumbnail } = req.body || {};
try {
console.log('[POST /user/likes] payload:', {
provider,
videoId,
titlePreview: typeof title === 'string' ? title.slice(0, 80) : title,
hasThumbnail: Boolean(thumbnail)
});
} catch {}
if (!provider || !videoId) return res.status(400).json({ error: 'provider and videoId are required' }); if (!provider || !videoId) return res.status(400).json({ error: 'provider and videoId are required' });
const row = likeVideo({ userId: req.user.id, provider, videoId });
// Server-side enrichment: if title or thumbnail is missing, fetch minimal details via yt-dlp
try {
const needTitle = !(typeof title === 'string' && title.trim().length > 0);
const needThumb = !(typeof thumbnail === 'string' && thumbnail.trim().length > 0);
if (needTitle || needThumb) {
const url = providerUrlFrom(provider, videoId, { instance: req.query.instance, slug: req.query.slug, sourceUrl: req.query.sourceUrl });
try {
const raw = await youtubedl(url, { dumpSingleJson: true, noWarnings: true, noCheckCertificates: true, skipDownload: true });
const meta = (typeof raw === 'string') ? JSON.parse(raw || '{}') : (raw || {});
if (needTitle) title = meta?.title || title || '';
if (needThumb) thumbnail = meta?.thumbnail || (Array.isArray(meta?.thumbnails) && meta.thumbnails.length ? meta.thumbnails[0].url : thumbnail || '');
} catch (e) {
console.warn('[POST /user/likes] details fetch failed, continuing without enrichment:', e?.message || e);
}
}
} catch {}
const row = likeVideo({ userId: req.user.id, provider, videoId, title, thumbnail });
return res.status(201).json(row); return res.status(201).json(row);
}); });

View File

@ -28,7 +28,7 @@
{{ video.duration / 60 | number:'1.0-0' }}:{{ (video.duration % 60) | number:'2.0-0' }} {{ video.duration / 60 | number:'1.0-0' }}:{{ (video.duration % 60) | number:'2.0-0' }}
</div> </div>
<div class="absolute bottom-2 left-2"> <div class="absolute bottom-2 left-2">
<app-like-button [videoId]="video.videoId" [provider]="'youtube'"></app-like-button> <app-like-button [videoId]="video.videoId" [provider]="instances.selectedProvider()" [title]="video.title" [thumbnail]="video.thumbnail"></app-like-button>
</div> </div>
</div> </div>
<div class="p-4 flex-grow flex flex-col"> <div class="p-4 flex-grow flex flex-col">

View File

@ -25,7 +25,7 @@ import { LikeButtonComponent } from '../shared/components/like-button/like-butto
}) })
export class HomeComponent { export class HomeComponent {
private apiService = inject(YoutubeApiService); private apiService = inject(YoutubeApiService);
private instances = inject(InstanceService); instances = inject(InstanceService);
trendingVideos = signal<Video[]>([]); trendingVideos = signal<Video[]>([]);
loading = signal(true); loading = signal(true);

View File

@ -55,7 +55,7 @@
{{ formatViews(video.views) }} en direct {{ formatViews(video.views) }} en direct
</div> </div>
<div class="absolute bottom-2 left-2"> <div class="absolute bottom-2 left-2">
<app-like-button [videoId]="video.videoId" [provider]="'twitch'"></app-like-button> <app-like-button [videoId]="video.videoId" [provider]="'twitch'" [title]="video.title" [thumbnail]="video.thumbnail"></app-like-button>
</div> </div>
</div> </div>
<div class="p-4 flex-grow flex flex-col"> <div class="p-4 flex-grow flex flex-col">
@ -91,7 +91,7 @@
{{ video.duration / 60 | number:'1.0-0' }}:{{ (video.duration % 60) | number:'2.0-0' }} {{ video.duration / 60 | number:'1.0-0' }}:{{ (video.duration % 60) | number:'2.0-0' }}
</div> </div>
<div class="absolute bottom-2 left-2"> <div class="absolute bottom-2 left-2">
<app-like-button [videoId]="video.videoId" [provider]="'twitch'"></app-like-button> <app-like-button [videoId]="video.videoId" [provider]="'twitch'" [title]="video.title" [thumbnail]="video.thumbnail"></app-like-button>
</div> </div>
</div> </div>
<div class="p-4 flex-grow flex flex-col"> <div class="p-4 flex-grow flex flex-col">
@ -127,7 +127,7 @@
{{ video.duration / 60 | number:'1.0-0' }}:{{ (video.duration % 60) | number:'2.0-0' }} {{ video.duration / 60 | number:'1.0-0' }}:{{ (video.duration % 60) | number:'2.0-0' }}
</div> </div>
<div class="absolute bottom-2 left-2"> <div class="absolute bottom-2 left-2">
<app-like-button [videoId]="video.videoId" [provider]="selectedProviderForView()"></app-like-button> <app-like-button [videoId]="video.videoId" [provider]="selectedProviderForView()" [title]="video.title" [thumbnail]="video.thumbnail"></app-like-button>
</div> </div>
</div> </div>
<div class="p-4 flex-grow flex flex-col"> <div class="p-4 flex-grow flex flex-col">

View File

@ -82,6 +82,8 @@ export class LikeButtonComponent implements OnInit, OnDestroy, OnChanges {
private auth = inject(AuthService); private auth = inject(AuthService);
@Input() provider = 'youtube'; @Input() provider = 'youtube';
@Input() title?: string;
@Input() thumbnail?: string;
private _videoId = ''; private _videoId = '';
@ -193,7 +195,10 @@ export class LikeButtonComponent implements OnInit, OnDestroy, OnChanges {
} }
}); });
} else { } else {
this.likesService.like(this.provider, this.videoId) const cleanTitle = (this.title || '').trim();
const cleanThumb = (this.thumbnail || '').trim();
try { console.debug('[LikeButton] like payload', { provider: this.provider, videoId: this.videoId, title: cleanTitle, hasThumb: !!cleanThumb }); } catch {}
this.likesService.like(this.provider, this.videoId, cleanTitle || undefined, cleanThumb || undefined)
.pipe( .pipe(
finalize(() => { finalize(() => {
if (!this.destroyRef) { if (!this.destroyRef) {

View File

@ -80,7 +80,7 @@
<div class="relative"> <div class="relative">
<img [src]="v.thumbnail" [alt]="v.title" loading="lazy" decoding="async" class="w-full h-32 object-cover"> <img [src]="v.thumbnail" [alt]="v.title" loading="lazy" decoding="async" class="w-full h-32 object-cover">
<div class="absolute bottom-2 left-2"> <div class="absolute bottom-2 left-2">
<app-like-button [videoId]="v.videoId" [provider]="provider()"></app-like-button> <app-like-button [videoId]="v.videoId" [provider]="provider()" [title]="v.title" [thumbnail]="v.thumbnail"></app-like-button>
</div> </div>
</div> </div>
<div class="p-2 text-sm line-clamp-2">{{ v.title }}</div> <div class="p-2 text-sm line-clamp-2">{{ v.title }}</div>
@ -131,7 +131,7 @@
<div class="relative"> <div class="relative">
<img [src]="v.thumbnail" [alt]="v.title" loading="lazy" decoding="async" class="w-full h-32 object-cover"> <img [src]="v.thumbnail" [alt]="v.title" loading="lazy" decoding="async" class="w-full h-32 object-cover">
<div class="absolute bottom-2 left-2"> <div class="absolute bottom-2 left-2">
<app-like-button [videoId]="v.videoId" [provider]="provider()"></app-like-button> <app-like-button [videoId]="v.videoId" [provider]="provider()" [title]="v.title" [thumbnail]="v.thumbnail"></app-like-button>
</div> </div>
</div> </div>
<div class="p-2 text-sm line-clamp-2">{{ v.title }}</div> <div class="p-2 text-sm line-clamp-2">{{ v.title }}</div>

View File

@ -33,7 +33,7 @@
{{ v.duration / 60 | number:'1.0-0' }}:{{ (v.duration % 60) | number:'2.0-0' }} {{ v.duration / 60 | number:'1.0-0' }}:{{ (v.duration % 60) | number:'2.0-0' }}
</div> </div>
<div class="absolute bottom-2 left-2"> <div class="absolute bottom-2 left-2">
<app-like-button [videoId]="v.videoId" [provider]="b.provider"></app-like-button> <app-like-button [videoId]="v.videoId" [provider]="b.provider" [title]="v.title" [thumbnail]="v.thumbnail"></app-like-button>
</div> </div>
</div> </div>
<div class="p-3"> <div class="p-3">

View File

@ -8,6 +8,7 @@ export interface LikedVideoItem {
title?: string; title?: string;
thumbnail?: string; thumbnail?: string;
last_watched_at?: string | null; last_watched_at?: string | null;
created_at?: string;
} }
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
@ -29,10 +30,10 @@ export class LikesService {
return this.http.get<LikedVideoItem[]>(`${this.apiBase()}/user/likes`, { params, withCredentials: true }); return this.http.get<LikedVideoItem[]>(`${this.apiBase()}/user/likes`, { params, withCredentials: true });
} }
like(provider: string, videoId: string): Observable<{ provider: string; video_id: string }> { like(provider: string, videoId: string, title?: string, thumbnail?: string): Observable<{ provider: string; video_id: string }> {
return this.http.post<{ provider: string; video_id: string }>( return this.http.post<{ provider: string; video_id: string }>(
`${this.apiBase()}/user/likes`, `${this.apiBase()}/user/likes`,
{ provider, videoId }, { provider, videoId, title, thumbnail },
{ withCredentials: true } { withCredentials: true }
); );
} }