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

@@ -8,5 +8,3 @@ Generated by Gemini CLI.
1. windows 上打包后运行命令会出现黑窗,而且还是会出现找不到 js-runtimes 的问题,但是 quickjs 是正常下载了 1. windows 上打包后运行命令会出现黑窗,而且还是会出现找不到 js-runtimes 的问题,但是 quickjs 是正常下载了
2. macos 上未测试 2. macos 上未测试
3. 增加日志内容,除了下载日志,把运行日志,包括调用的完整命令也输出一下
4. 解析多视频还是有问题,不显示视频列表,缩略图也不对

View File

@@ -2,9 +2,8 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Tauri + Vue + Typescript App</title> <title>Stream Capture</title>
</head> </head>
<body> <body>

BIN
public/placeholder.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -1,6 +0,0 @@
<svg width="206" height="231" viewBox="0 0 206 231" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M143.143 84C143.143 96.1503 133.293 106 121.143 106C108.992 106 99.1426 96.1503 99.1426 84C99.1426 71.8497 108.992 62 121.143 62C133.293 62 143.143 71.8497 143.143 84Z" fill="#FFC131"/>
<ellipse cx="84.1426" cy="147" rx="22" ry="22" transform="rotate(180 84.1426 147)" fill="#24C8DB"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M166.738 154.548C157.86 160.286 148.023 164.269 137.757 166.341C139.858 160.282 141 153.774 141 147C141 144.543 140.85 142.121 140.558 139.743C144.975 138.204 149.215 136.139 153.183 133.575C162.73 127.404 170.292 118.608 174.961 108.244C179.63 97.8797 181.207 86.3876 179.502 75.1487C177.798 63.9098 172.884 53.4021 165.352 44.8883C157.82 36.3744 147.99 30.2165 137.042 27.1546C126.095 24.0926 114.496 24.2568 103.64 27.6274C92.7839 30.998 83.1319 37.4317 75.8437 46.1553C74.9102 47.2727 74.0206 48.4216 73.176 49.5993C61.9292 50.8488 51.0363 54.0318 40.9629 58.9556C44.2417 48.4586 49.5653 38.6591 56.679 30.1442C67.0505 17.7298 80.7861 8.57426 96.2354 3.77762C111.685 -1.01901 128.19 -1.25267 143.769 3.10474C159.348 7.46215 173.337 16.2252 184.056 28.3411C194.775 40.457 201.767 55.4101 204.193 71.404C206.619 87.3978 204.374 103.752 197.73 118.501C191.086 133.25 180.324 145.767 166.738 154.548ZM41.9631 74.275L62.5557 76.8042C63.0459 72.813 63.9401 68.9018 65.2138 65.1274C57.0465 67.0016 49.2088 70.087 41.9631 74.275Z" fill="#FFC131"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M38.4045 76.4519C47.3493 70.6709 57.2677 66.6712 67.6171 64.6132C65.2774 70.9669 64 77.8343 64 85.0001C64 87.1434 64.1143 89.26 64.3371 91.3442C60.0093 92.8732 55.8533 94.9092 51.9599 97.4256C42.4128 103.596 34.8505 112.392 30.1816 122.756C25.5126 133.12 23.9357 144.612 25.6403 155.851C27.3449 167.09 32.2584 177.598 39.7906 186.112C47.3227 194.626 57.153 200.784 68.1003 203.846C79.0476 206.907 90.6462 206.743 101.502 203.373C112.359 200.002 122.011 193.568 129.299 184.845C130.237 183.722 131.131 182.567 131.979 181.383C143.235 180.114 154.132 176.91 164.205 171.962C160.929 182.49 155.596 192.319 148.464 200.856C138.092 213.27 124.357 222.426 108.907 227.222C93.458 232.019 76.9524 232.253 61.3736 227.895C45.7948 223.538 31.8055 214.775 21.0867 202.659C10.3679 190.543 3.37557 175.59 0.949823 159.596C-1.47592 143.602 0.768139 127.248 7.41237 112.499C14.0566 97.7497 24.8183 85.2327 38.4045 76.4519ZM163.062 156.711L163.062 156.711C162.954 156.773 162.846 156.835 162.738 156.897C162.846 156.835 162.954 156.773 163.062 156.711Z" fill="#24C8DB"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -12,7 +12,7 @@
"app": { "app": {
"windows": [ "windows": [
{ {
"title": "stream-capture", "title": "Stream Capture",
"width": 1300, "width": 1300,
"height": 850 "height": 850
} }

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

Before

Width:  |  Height:  |  Size: 496 B

View File

@@ -18,6 +18,21 @@ export const useAnalysisStore = defineStore('analysis', () => {
output_path: '' output_path: ''
}) })
function toggleEntry(id: string) {
if (metadata.value && metadata.value.entries) {
const entry = metadata.value.entries.find((e: any) => e.id === id)
if (entry) {
entry.selected = !entry.selected
}
}
}
function setAllEntries(selected: boolean) {
if (metadata.value && metadata.value.entries) {
metadata.value.entries.forEach((e: any) => e.selected = selected)
}
}
function reset() { function reset() {
url.value = '' url.value = ''
loading.value = false loading.value = false
@@ -27,5 +42,5 @@ export const useAnalysisStore = defineStore('analysis', () => {
scanMix.value = false scanMix.value = false
} }
return { url, loading, error, metadata, options, isMix, scanMix, reset } return { url, loading, error, metadata, options, isMix, scanMix, toggleEntry, setAllEntries, reset }
}) })

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 analysisStore.metadata = res
} catch (e: any) { } catch (e: any) {
analysisStore.error = e.toString() analysisStore.error = e.toString()
@@ -84,18 +90,36 @@ async function startDownload() {
} }
try { try {
const metaToSend = analysisStore.metadata.entries ? if (analysisStore.metadata.entries) {
{ title: analysisStore.metadata.title, thumbnail: "", id: analysisStore.metadata.id } : // Playlist Download
analysisStore.metadata; const selectedEntries = analysisStore.metadata.entries.filter((e: any) => e.selected)
// Note: We might want to pass the *cleaned* URL if it was cleaned during analyze if (selectedEntries.length === 0) {
// But for now we pass the original URL or whatever was scanned. analysisStore.error = "Please select at least one video to download."
// Actually, if we scanned as a single video (unchecked), we should probably download as single video. return
// 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.
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
})
queueStore.addTask({
id,
title: entry.title,
thumbnail: entry.thumbnail,
progress: 0,
speed: 'Pending...',
status: 'pending'
})
}
} else {
// Single Video Download
let urlToDownload = analysisStore.url; let urlToDownload = analysisStore.url;
// Clean URL if it was a mix but we didn't scan it as one
if (analysisStore.isMix && !analysisStore.scanMix) { if (analysisStore.isMix && !analysisStore.scanMix) {
try { try {
const u = new URL(urlToDownload); const u = new URL(urlToDownload);
@@ -111,17 +135,18 @@ async function startDownload() {
const id = await invoke<string>('start_download', { const id = await invoke<string>('start_download', {
url: urlToDownload, url: urlToDownload,
options: analysisStore.options, options: analysisStore.options,
metadata: metaToSend metadata: analysisStore.metadata
}) })
queueStore.addTask({ queueStore.addTask({
id, id,
title: metaToSend.title, title: analysisStore.metadata.title,
thumbnail: metaToSend.thumbnail, thumbnail: analysisStore.metadata.thumbnail,
progress: 0, progress: 0,
speed: 'Pending...', speed: 'Pending...',
status: 'pending' status: 'pending'
}) })
}
// Reset state after successful download start // Reset state after successful download start
analysisStore.reset() analysisStore.reset()
@@ -178,7 +203,86 @@ async function startDownload() {
<!-- Analysis Result --> <!-- 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 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 --> <!-- 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" /> <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"> <div class="flex-1">
<h2 class="text-xl font-bold text-zinc-900 dark:text-white line-clamp-2">{{ analysisStore.metadata.title }}</h2> <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.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 --> <!-- Options -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-6"> <div class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-6">
@@ -222,7 +325,7 @@ async function startDownload() {
@click="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" 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> </button>
</div> </div>
</div> </div>