fix playlist

This commit is contained in:
Julian Freeman
2025-12-08 08:52:49 -04:00
parent dda4e482c1
commit 091cf65dac
9 changed files with 160 additions and 53 deletions

View File

@@ -66,7 +66,13 @@ async function analyze() {
}
}
const res = await invoke('fetch_metadata', { url: urlToScan, parseMixPlaylist: parseMix })
const res = await invoke<any>('fetch_metadata', { url: urlToScan, parseMixPlaylist: parseMix })
// Initialize selected state for playlist entries
if (res.entries) {
res.entries = res.entries.map((e: any) => ({ ...e, selected: true }))
}
analysisStore.metadata = res
} catch (e: any) {
analysisStore.error = e.toString()
@@ -84,44 +90,63 @@ async function startDownload() {
}
try {
const metaToSend = analysisStore.metadata.entries ?
{ title: analysisStore.metadata.title, thumbnail: "", id: analysisStore.metadata.id } :
analysisStore.metadata;
if (analysisStore.metadata.entries) {
// Playlist Download
const selectedEntries = analysisStore.metadata.entries.filter((e: any) => e.selected)
if (selectedEntries.length === 0) {
analysisStore.error = "Please select at least one video to download."
return
}
// Note: We might want to pass the *cleaned* URL if it was cleaned during analyze
// But for now we pass the original URL or whatever was scanned.
// Actually, if we scanned as a single video (unchecked), we should probably download as single video.
// The user might expect the same result as analysis.
// Let's reconstruct the URL logic or just use what `analyze` used?
// Since `start_download` just takes a URL string, we should probably use the same logic.
let urlToDownload = analysisStore.url;
if (analysisStore.isMix && !analysisStore.scanMix) {
try {
const u = new URL(urlToDownload);
u.searchParams.delete('list');
u.searchParams.delete('index');
u.searchParams.delete('start_radio');
urlToDownload = u.toString();
} catch (e) {
urlToDownload = urlToDownload.replace(/&list=[^&]+/, '');
}
}
for (const entry of selectedEntries) {
const videoUrl = `https://www.youtube.com/watch?v=${entry.id}`
const id = await invoke<string>('start_download', {
url: videoUrl,
options: analysisStore.options,
metadata: entry // Pass the individual video metadata
})
const id = await invoke<string>('start_download', {
url: urlToDownload,
options: analysisStore.options,
metadata: metaToSend
})
queueStore.addTask({
id,
title: entry.title,
thumbnail: entry.thumbnail,
progress: 0,
speed: 'Pending...',
status: 'pending'
})
}
} else {
// Single Video Download
let urlToDownload = analysisStore.url;
// Clean URL if it was a mix but we didn't scan it as one
if (analysisStore.isMix && !analysisStore.scanMix) {
try {
const u = new URL(urlToDownload);
u.searchParams.delete('list');
u.searchParams.delete('index');
u.searchParams.delete('start_radio');
urlToDownload = u.toString();
} catch (e) {
urlToDownload = urlToDownload.replace(/&list=[^&]+/, '');
}
}
queueStore.addTask({
id,
title: metaToSend.title,
thumbnail: metaToSend.thumbnail,
progress: 0,
speed: 'Pending...',
status: 'pending'
})
const id = await invoke<string>('start_download', {
url: urlToDownload,
options: analysisStore.options,
metadata: analysisStore.metadata
})
queueStore.addTask({
id,
title: analysisStore.metadata.title,
thumbnail: analysisStore.metadata.thumbnail,
progress: 0,
speed: 'Pending...',
status: 'pending'
})
}
// Reset state after successful download start
analysisStore.reset()
@@ -178,7 +203,86 @@ async function startDownload() {
<!-- Analysis Result -->
<div v-if="analysisStore.metadata" class="bg-white dark:bg-zinc-900 rounded-2xl shadow-sm border border-gray-200 dark:border-zinc-800 overflow-hidden mb-8">
<div class="p-6 flex flex-col md:flex-row gap-6">
<!-- Playlist Header / Global Controls -->
<div v-if="analysisStore.metadata.entries" class="p-6 border-b border-gray-200 dark:border-zinc-800">
<div class="flex items-start justify-between mb-6">
<div>
<h2 class="text-xl font-bold text-zinc-900 dark:text-white">{{ analysisStore.metadata.title }}</h2>
<p class="text-blue-500 mt-1 font-medium">{{ analysisStore.metadata.entries.length }} videos found</p>
</div>
<div class="flex items-center gap-2">
<button @click="analysisStore.setAllEntries(true)" class="text-xs px-3 py-1.5 rounded-lg bg-gray-100 hover:bg-gray-200 dark:bg-zinc-800 dark:hover:bg-zinc-700 text-zinc-700 dark:text-gray-300 transition-colors">Select All</button>
<button @click="analysisStore.setAllEntries(false)" class="text-xs px-3 py-1.5 rounded-lg bg-gray-100 hover:bg-gray-200 dark:bg-zinc-800 dark:hover:bg-zinc-700 text-zinc-700 dark:text-gray-300 transition-colors">Select None</button>
</div>
</div>
<!-- Global Options -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 bg-gray-50 dark:bg-zinc-800/50 p-4 rounded-xl">
<!-- Audio Only Toggle -->
<div class="flex items-center justify-between">
<span class="font-medium text-sm text-zinc-700 dark:text-gray-300">Audio Only (All)</span>
<button
@click="analysisStore.options.is_audio_only = !analysisStore.options.is_audio_only"
class="w-10 h-5 rounded-full relative transition-colors duration-200 ease-in-out"
:class="analysisStore.options.is_audio_only ? 'bg-blue-600' : 'bg-gray-300 dark:bg-zinc-600'"
>
<span
class="absolute top-1 left-1 w-3 h-3 bg-white rounded-full transition-transform duration-200"
:class="analysisStore.options.is_audio_only ? 'translate-x-5' : 'translate-x-0'"
/>
</button>
</div>
<!-- Quality Dropdown -->
<div class="flex items-center justify-between gap-4">
<span class="font-medium text-sm text-zinc-700 dark:text-gray-300">Quality (All)</span>
<div class="w-40">
<AppSelect
v-model="analysisStore.options.quality"
:options="qualityOptions"
:disabled="analysisStore.options.is_audio_only"
/>
</div>
</div>
</div>
</div>
<!-- Video List (Playlist Mode) -->
<div v-if="analysisStore.metadata.entries" class="max-h-[500px] overflow-y-auto p-2 space-y-2 bg-gray-50/50 dark:bg-black/20">
<div
v-for="entry in analysisStore.metadata.entries"
:key="entry.id"
class="flex items-center gap-4 p-3 rounded-xl hover:bg-white dark:hover:bg-zinc-800 transition-colors border border-transparent hover:border-gray-200 dark:hover:border-zinc-700 group"
:class="entry.selected ? 'opacity-100' : 'opacity-60 grayscale'"
>
<!-- Checkbox -->
<button
@click="entry.selected = !entry.selected"
class="w-6 h-6 rounded-lg border-2 flex items-center justify-center transition-colors shrink-0"
:class="entry.selected ? 'bg-blue-600 border-blue-600 text-white' : 'border-gray-300 dark:border-zinc-600 hover:border-blue-400'"
>
<svg v-if="entry.selected" xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" />
</svg>
</button>
<!-- Thumb -->
<img :src="entry.thumbnail || '/placeholder.png'" class="w-24 h-14 object-cover rounded-lg bg-gray-200 dark:bg-zinc-700 shrink-0" />
<!-- Info -->
<div class="flex-1 min-w-0">
<h4 class="font-medium text-zinc-900 dark:text-white truncate text-sm" :title="entry.title">{{ entry.title }}</h4>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
{{ entry.duration ? Math.floor(entry.duration / 60) + ':' + String(Math.floor(entry.duration % 60)).padStart(2, '0') : '' }}
<span v-if="entry.uploader" class="mx-1"></span> {{ entry.uploader }}
</p>
</div>
</div>
</div>
<!-- Single Video Layout -->
<div v-else class="p-6 flex flex-col md:flex-row gap-6">
<!-- Thumbnail -->
<img :src="analysisStore.metadata.thumbnail || '/placeholder.png'" class="w-full md:w-64 aspect-video object-cover rounded-lg bg-gray-100 dark:bg-zinc-800" />
@@ -186,7 +290,6 @@ async function startDownload() {
<div class="flex-1">
<h2 class="text-xl font-bold text-zinc-900 dark:text-white line-clamp-2">{{ analysisStore.metadata.title }}</h2>
<p v-if="analysisStore.metadata.uploader" class="text-gray-500 dark:text-gray-400 mt-1">{{ analysisStore.metadata.uploader }}</p>
<p v-if="analysisStore.metadata.entries" class="text-blue-500 mt-1 font-medium">{{ analysisStore.metadata.entries.length }} videos in playlist</p>
<!-- Options -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-6">
@@ -222,7 +325,7 @@ async function startDownload() {
@click="startDownload"
class="bg-blue-600 hover:bg-blue-700 text-white px-8 py-3 rounded-xl font-bold transition-colors shadow-lg shadow-blue-600/20"
>
Download Now
Download Now {{ analysisStore.metadata.entries ? `(${analysisStore.metadata.entries.filter((e: any) => e.selected).length})` : '' }}
</button>
</div>
</div>