diff --git a/init.ts b/init.ts index e7cc79f..f5465ac 100644 --- a/init.ts +++ b/init.ts @@ -17,6 +17,8 @@ import { initYtdlp, setProgressCallback, setTrackReadyCallback, + skipSlowQueueItem, + getQueuedSlowItems, type QueueItem, } from "./ytdlp"; @@ -206,6 +208,60 @@ export async function init(): Promise { library.logActivity("scan_updated", { id: track.id, filename: track.filename, title: track.title }); }); + // Prescan slow queue to find tracks already in library + function prescanSlowQueue() { + const queuedItems = getQueuedSlowItems(); + if (queuedItems.length === 0) return; + + const tracks = library.getAllTracks(); + if (tracks.length === 0) return; + + for (const item of queuedItems) { + const itemTitle = normalizeForMatch(item.title); + + // Check if any library track matches + for (const track of tracks) { + const trackTitle = normalizeForMatch(track.title || ""); + const trackFilename = normalizeForMatch((track.filename || "").replace(/\.[^.]+$/, "")); + + const matches = + (trackTitle && trackTitle === itemTitle) || + (trackFilename && trackFilename === itemTitle) || + (trackTitle && itemTitle && trackTitle.includes(itemTitle)) || + (trackTitle && itemTitle && itemTitle.includes(trackTitle)) || + (trackFilename && itemTitle && trackFilename.includes(itemTitle)) || + (trackFilename && itemTitle && itemTitle.includes(trackFilename)); + + if (matches) { + console.log(`[ytdlp] Prescan: "${item.title}" already exists as "${track.title || track.filename}"`); + + // Skip download and add to playlist + const skipped = skipSlowQueueItem(item.id, track.id); + if (skipped && skipped.playlistId) { + try { + addTracksToPlaylist(skipped.playlistId, [track.id]); + sendToUser(skipped.userId, { + type: "toast", + message: `Already in library, added to: ${skipped.playlistName}`, + toastType: "info" + }); + } catch (e) { + console.error(`[ytdlp] Failed to add existing track to playlist:`, e); + } + } + break; + } + } + } + } + + // Run prescan periodically (every 30 seconds) + setInterval(prescanSlowQueue, 30000); + // Also run once after initial scan completes + library.onScanComplete(() => { + setTimeout(prescanSlowQueue, 1000); + }); + // Load channels from database const savedChannels = loadAllChannels(); let hasDefault = false; diff --git a/public/trackContainer.js b/public/trackContainer.js index 05ecc2b..da84765 100644 --- a/public/trackContainer.js +++ b/public/trackContainer.js @@ -760,6 +760,7 @@ else if (res.ok) { M.showToast("Removed"); clearSelection(); + render(); } else { M.showToast("Failed to remove from queue", "error"); } diff --git a/ytdlp.ts b/ytdlp.ts index 2fad06e..c428556 100644 --- a/ytdlp.ts +++ b/ytdlp.ts @@ -525,6 +525,36 @@ function notifyProgress(item: QueueItem): void { } } +// Mark a slow queue item as skipped (already in library) +export function skipSlowQueueItem(id: string, trackId: string): QueueItem | null { + const item = slowQueue.find(i => i.id === id && i.status === "queued"); + if (!item) return null; + + item.status = "complete"; + item.progress = 100; + item.completedAt = Date.now(); + item.trackId = trackId; + + // Update database + updateSlowQueueItem(id, { + status: "complete", + progress: 100, + completedAt: Math.floor(item.completedAt / 1000) + }); + + notifyProgress(item); + + // Remove from queue after brief delay + setTimeout(() => removeFromQueue(item), 1000); + + return item; +} + +// Get queued items from slow queue (for prescan) +export function getQueuedSlowItems(): QueueItem[] { + return slowQueue.filter(i => i.status === "queued"); +} + // Cleanup old completed/failed items export function cleanupOldItems(maxAge: number = 3600000): void { const now = Date.now();