fix playlist
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user