chore: update Angular cache files and TypeScript build info
This commit is contained in:
parent
3dbfb04b15
commit
f3a78b7d7e
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.
@ -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)
|
||||||
);
|
);
|
||||||
|
@ -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
|
||||||
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -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">
|
||||||
|
@ -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);
|
||||||
|
@ -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">
|
||||||
|
@ -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) {
|
||||||
|
@ -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>
|
||||||
|
@ -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">
|
||||||
|
@ -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 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user