chore: update Angular cache and TypeScript build info
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
import { Observable, of, tap } from 'rxjs';
|
||||
import { AuthService } from './auth.service';
|
||||
|
||||
export interface SearchHistoryItem {
|
||||
id: string;
|
||||
@@ -24,7 +25,17 @@ export interface WatchHistoryItem {
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class HistoryService {
|
||||
constructor(private http: HttpClient) {}
|
||||
private http = inject(HttpClient);
|
||||
private auth = inject(AuthService);
|
||||
|
||||
private apiBase(): string {
|
||||
try {
|
||||
const port = window?.location?.port || '';
|
||||
return port && port !== '4000' ? '/proxy/api' : '/api';
|
||||
} catch {
|
||||
return '/api';
|
||||
}
|
||||
}
|
||||
|
||||
// --- Search ---
|
||||
recordSearch(query: string, filters?: Record<string, any>): Observable<{ id: string; created_at: string }> {
|
||||
@@ -43,19 +54,34 @@ export class HistoryService {
|
||||
}
|
||||
|
||||
// --- Watch ---
|
||||
recordWatchStart(provider: string, videoId: string, title?: string | null, watchedAt?: string, thumbnail?: string | null): Observable<WatchHistoryItem> {
|
||||
recordWatchStart(provider: string, videoId: string, title?: string | null, watchedAt?: string, thumbnail?: string | null, durationSeconds?: number): Observable<WatchHistoryItem> {
|
||||
return this.http.post<WatchHistoryItem>(
|
||||
'/proxy/api/user/history/watch',
|
||||
{ provider, videoId, title: title ?? null, watchedAt, thumbnail: thumbnail ?? null },
|
||||
{ provider, videoId, title: title ?? null, watchedAt, thumbnail: thumbnail ?? null, durationSeconds },
|
||||
{ withCredentials: true }
|
||||
);
|
||||
}
|
||||
|
||||
updateWatchProgress(id: string, progressSeconds: number, lastPositionSeconds?: number): Observable<WatchHistoryItem> {
|
||||
return this.http.patch<WatchHistoryItem>(
|
||||
`/proxy/api/user/history/watch/${encodeURIComponent(id)}`,
|
||||
{ progressSeconds, lastPositionSeconds },
|
||||
{ withCredentials: true }
|
||||
updateWatchProgress(id: string, progress_seconds: number, last_position_seconds: number): Observable<any> {
|
||||
if (!this.auth.currentUser()) {
|
||||
console.warn('[HistoryService] Not authenticated, skipping watch progress update');
|
||||
return of(null);
|
||||
}
|
||||
console.log('[HistoryService] Updating watch progress', { id, progress_seconds, last_position_seconds });
|
||||
return this.http.patch(`${this.apiBase()}/user/history/watch/${id}`, {
|
||||
progressSeconds: progress_seconds,
|
||||
lastPositionSeconds: last_position_seconds
|
||||
}, { withCredentials: true }).pipe(
|
||||
tap({
|
||||
next: (response) => {
|
||||
console.log('[HistoryService] Watch progress updated successfully', response);
|
||||
console.log('[HistoryService] Updated watch progress', { id, progress_seconds, last_position_seconds });
|
||||
},
|
||||
error: (e: any) => {
|
||||
console.error('[HistoryService] Failed to update watch progress', e);
|
||||
console.error('[HistoryService] Error updating watch progress', { id, progress_seconds, last_position_seconds, error: e });
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,235 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HistoryService } from './history.service';
|
||||
|
||||
function loadScriptOnce(src: string, globalFlag: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const anyWin: any = window as any;
|
||||
if (anyWin[globalFlag]) { resolve(); return; }
|
||||
const existing = document.querySelector(`script[src="${src}"]`) as HTMLScriptElement | null;
|
||||
if (existing) { existing.addEventListener('load', () => resolve()); existing.addEventListener('error', (e) => reject(e)); return; }
|
||||
const s = document.createElement('script');
|
||||
s.async = true;
|
||||
s.src = src;
|
||||
s.onload = () => resolve();
|
||||
s.onerror = (e) => reject(e);
|
||||
document.head.appendChild(s);
|
||||
});
|
||||
}
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class IframeProgressService {
|
||||
private ytIntervals = new Map<string, any>();
|
||||
private peertubeHandlers = new Map<string, (ev: MessageEvent) => void>();
|
||||
private lastSentAt = new Map<string, number>();
|
||||
private attached = new Map<string, HTMLIFrameElement>();
|
||||
private dmHandlers = new Map<string, (ev: MessageEvent) => void>();
|
||||
|
||||
constructor(private history: HistoryService) {}
|
||||
|
||||
async attach(provider: string, iframe: HTMLIFrameElement | null, watchId: string | null, startSeconds?: number | null) {
|
||||
console.log('[IframeProgress] attach called', { provider, hasIframe: !!iframe, watchId, startSeconds });
|
||||
if (!iframe || !watchId) {
|
||||
console.warn('[IframeProgress] Missing iframe or watchId, cannot attach', { hasIframe: !!iframe, watchId });
|
||||
return;
|
||||
}
|
||||
// Always allow re-attachment. We will clean up provider-specific handlers/intervals first.
|
||||
const p = (provider || '').toLowerCase();
|
||||
try {
|
||||
// Debug trace
|
||||
try { console.debug('[IframeProgress] attach', { provider: p, watchId, startSeconds }); } catch {}
|
||||
if (p === 'youtube') {
|
||||
await this.attachYouTube(iframe, watchId, startSeconds || 0);
|
||||
} else if (p === 'dailymotion') {
|
||||
await this.attachDailymotion(iframe, watchId, startSeconds || 0);
|
||||
} else if (p === 'peertube') {
|
||||
this.attachPeerTube(iframe, watchId);
|
||||
} else if (p === 'twitch') {
|
||||
// TODO: Twitch JS embed API could be integrated; limited time APIs.
|
||||
} else if (p === 'odysee' || p === 'rumble') {
|
||||
// No stable public APIs for progress; resume is handled via URL params where supported.
|
||||
}
|
||||
this.attached.set(watchId, iframe);
|
||||
} catch {
|
||||
// best-effort only
|
||||
}
|
||||
}
|
||||
|
||||
private async attachYouTube(iframe: HTMLIFrameElement, watchId: string, start: number) {
|
||||
console.log('[IframeProgress] Attaching YouTube player', { watchId, start });
|
||||
// Clear existing interval if any (idempotent attach)
|
||||
const prev = this.ytIntervals.get(watchId);
|
||||
if (prev) { try { clearInterval(prev); } catch {} this.ytIntervals.delete(watchId); }
|
||||
const win: any = window as any;
|
||||
console.log('[IframeProgress] Loading YouTube Iframe API');
|
||||
try {
|
||||
await loadScriptOnce('https://www.youtube.com/iframe_api', 'YT');
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
console.warn('[IframeProgress] YouTube Iframe API load timeout');
|
||||
reject(new Error('YouTube Iframe API load timeout'));
|
||||
}, 5000);
|
||||
|
||||
if (win.YT && win.YT.Player) {
|
||||
clearTimeout(timeout);
|
||||
console.log('[IframeProgress] YouTube Iframe API already loaded');
|
||||
return resolve();
|
||||
}
|
||||
|
||||
console.log('[IframeProgress] Waiting for YouTube Iframe API to be ready');
|
||||
(win as any).onYouTubeIframeAPIReady = () => {
|
||||
clearTimeout(timeout);
|
||||
console.log('[IframeProgress] YouTube Iframe API ready');
|
||||
resolve();
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[IframeProgress] Failed to load YouTube Iframe API', error);
|
||||
throw error;
|
||||
}
|
||||
if (!iframe.id) {
|
||||
iframe.id = 'yt-embed-' + Math.random().toString(36).slice(2, 8);
|
||||
console.log('[IframeProgress] Assigned ID to iframe:', iframe.id);
|
||||
}
|
||||
|
||||
console.log('[IframeProgress] Creating YT.Player instance');
|
||||
const player = new win.YT.Player(iframe, {
|
||||
host: 'https://www.youtube-nocookie.com',
|
||||
playerVars: { origin: window.location.origin || undefined },
|
||||
events: {
|
||||
onReady: () => {
|
||||
console.log('[IframeProgress] YouTube player ready');
|
||||
if (start && start > 0) {
|
||||
try {
|
||||
console.log(`[IframeProgress] Seeking to ${start} seconds`);
|
||||
player.seekTo(start, true);
|
||||
} catch (error) {
|
||||
console.error('[IframeProgress] Failed to seek', error);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[IframeProgress] Starting progress tracking interval');
|
||||
const interval = setInterval(() => {
|
||||
try {
|
||||
const t = Math.floor(player.getCurrentTime());
|
||||
const now = Date.now();
|
||||
const last = this.lastSentAt.get(watchId) || 0;
|
||||
|
||||
if (now - last >= 3000) {
|
||||
console.log(`[IframeProgress] Sending progress update for ${watchId}: ${t}s`);
|
||||
this.lastSentAt.set(watchId, now);
|
||||
this.history.updateWatchProgress(watchId, t, t).subscribe({
|
||||
next: (response) => {
|
||||
console.log(`[IframeProgress] Progress updated for ${watchId}:`, response);
|
||||
},
|
||||
error: (e) => {
|
||||
console.error(`[IframeProgress] Failed to update progress for ${watchId}:`, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch {}
|
||||
}, 1000);
|
||||
this.ytIntervals.set(watchId, interval);
|
||||
const obs = new MutationObserver(() => {
|
||||
if (!document.contains(iframe)) {
|
||||
clearInterval(interval);
|
||||
if (this.attached.get(watchId) === iframe) this.attached.delete(watchId);
|
||||
obs.disconnect();
|
||||
}
|
||||
});
|
||||
obs.observe(document.body, { childList: true, subtree: true });
|
||||
},
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async attachDailymotion(iframe: HTMLIFrameElement, watchId: string, start: number) {
|
||||
console.log('[IframeProgress] Attaching Dailymotion player (postMessage only)', { watchId, start });
|
||||
if (!iframe.id) {
|
||||
iframe.id = 'dm-embed-' + Math.random().toString(36).slice(2, 8);
|
||||
console.log('[IframeProgress] Assigned ID to Dailymotion iframe:', iframe.id);
|
||||
}
|
||||
|
||||
// 2) postMessage fallback (broadly log and handle DM message shapes)
|
||||
// Remove existing handler for this watchId to avoid duplicates
|
||||
const prevHandler = this.dmHandlers.get(watchId);
|
||||
if (prevHandler) {
|
||||
try { window.removeEventListener('message', prevHandler); } catch {}
|
||||
}
|
||||
const onMessage = (event: MessageEvent) => {
|
||||
try {
|
||||
const origin = String(event.origin || '');
|
||||
if (!/dailymotion\.com|dmcdn\.net/.test(origin)) return;
|
||||
let data: any = event.data;
|
||||
// Log the raw event once in a while to understand the shape
|
||||
try { console.debug('[IframeProgress] [DM PM] message', { origin, data }); } catch {}
|
||||
if (!data) return;
|
||||
if (typeof data === 'string') {
|
||||
try { data = JSON.parse(data); } catch { /* some messages are plain strings */ }
|
||||
}
|
||||
// Known shapes to extract event name and current time
|
||||
const ev = (data && (data.event || data.name || data.type)) || '';
|
||||
const timeVal = (data && (data.time ?? data.currentTime ?? data.current_time ?? data.position)) as any;
|
||||
if ((ev === 'timeupdate' || ev === 'progress' || ev === 'video_time_change' || ev === 'play_progress') && typeof timeVal === 'number') {
|
||||
const t = Math.floor(timeVal);
|
||||
const now = Date.now();
|
||||
const last = this.lastSentAt.get(watchId) || 0;
|
||||
if (now - last >= 3000) {
|
||||
console.log(`[IframeProgress] [DM PM] progress -> ${t}s for ${watchId}`);
|
||||
this.lastSentAt.set(watchId, now);
|
||||
this.history.updateWatchProgress(watchId, t, t).subscribe({ next: () => {}, error: (e) => { try { console.warn('[IframeProgress] DM PM update failed', e); } catch {} } });
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
};
|
||||
window.addEventListener('message', onMessage);
|
||||
this.dmHandlers.set(watchId, onMessage);
|
||||
const obs = new MutationObserver(() => {
|
||||
if (!document.contains(iframe)) {
|
||||
window.removeEventListener('message', onMessage);
|
||||
this.dmHandlers.delete(watchId);
|
||||
if (this.attached.get(watchId) === iframe) this.attached.delete(watchId);
|
||||
obs.disconnect();
|
||||
}
|
||||
});
|
||||
obs.observe(document.body, { childList: true, subtree: true });
|
||||
}
|
||||
|
||||
private attachPeerTube(iframe: HTMLIFrameElement, watchId: string) {
|
||||
console.log('[IframeProgress] Attaching PeerTube message listener', { watchId });
|
||||
const handler = (event: MessageEvent) => {
|
||||
try {
|
||||
console.log('[IframeProgress] Received PeerTube message', event);
|
||||
const data: any = event.data;
|
||||
if (!data || typeof data !== 'object') return;
|
||||
if (data.event === 'timeupdate' && typeof data.currentTime === 'number') {
|
||||
const t = Math.floor(data.currentTime);
|
||||
const now = Date.now();
|
||||
const last = this.lastSentAt.get(watchId) || 0;
|
||||
|
||||
if (now - last >= 3000) {
|
||||
console.log(`[IframeProgress] Sending PeerTube progress update for ${watchId}: ${t}s`);
|
||||
this.lastSentAt.set(watchId, now);
|
||||
this.history.updateWatchProgress(watchId, t, t).subscribe({
|
||||
next: (response) => {
|
||||
console.log(`[IframeProgress] PeerTube progress updated for ${watchId}:`, response);
|
||||
},
|
||||
error: (e) => {
|
||||
console.error(`[IframeProgress] Failed to update PeerTube progress for ${watchId}:`, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
};
|
||||
window.addEventListener('message', handler);
|
||||
this.peertubeHandlers.set(watchId, handler);
|
||||
const obs = new MutationObserver(() => {
|
||||
if (!document.contains(iframe)) {
|
||||
window.removeEventListener('message', handler);
|
||||
if (this.attached.get(watchId) === iframe) this.attached.delete(watchId);
|
||||
obs.disconnect();
|
||||
}
|
||||
});
|
||||
obs.observe(document.body, { childList: true, subtree: true });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user