First working POC
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
<script lang="ts">
|
||||
import { downloads, videoDownloads } from "$lib/stores/downloads";
|
||||
import { auth } from "$lib/stores/auth";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { QUALITY_PRESETS, type QualityPreset } from "$lib/api/quality-presets";
|
||||
|
||||
interface Props {
|
||||
seriesId: string;
|
||||
seriesName: string;
|
||||
episodeCount?: number;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
let { seriesId, seriesName, episodeCount, className = "" }: Props = $props();
|
||||
|
||||
let isProcessing = $state(false);
|
||||
let showQualityPicker = $state(false);
|
||||
|
||||
// Count downloads for this series
|
||||
const seriesDownloads = $derived(
|
||||
$videoDownloads.filter((d) => d.seriesName === seriesName)
|
||||
);
|
||||
|
||||
const completedCount = $derived(
|
||||
seriesDownloads.filter((d) => d.status === "completed").length
|
||||
);
|
||||
|
||||
const inProgressCount = $derived(
|
||||
seriesDownloads.filter((d) => d.status === "downloading" || d.status === "pending").length
|
||||
);
|
||||
|
||||
const hasDownloads = $derived(completedCount > 0 || inProgressCount > 0);
|
||||
const allDownloaded = $derived(episodeCount !== undefined && completedCount >= episodeCount);
|
||||
|
||||
async function startSeriesDownload(quality: QualityPreset) {
|
||||
showQualityPicker = false;
|
||||
if (isProcessing) return;
|
||||
|
||||
isProcessing = true;
|
||||
try {
|
||||
const userId = $auth.user?.id;
|
||||
if (!userId) {
|
||||
console.error("No user ID found");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("📺 Starting series download for:", seriesName, "quality:", quality);
|
||||
|
||||
// Get target directory
|
||||
const targetDir = await invoke<string>("storage_get_path");
|
||||
const basePath = `${targetDir}/videos`;
|
||||
|
||||
// Queue all episodes
|
||||
const downloadIds = await downloads.downloadSeries(
|
||||
seriesId,
|
||||
seriesName,
|
||||
userId,
|
||||
basePath,
|
||||
quality
|
||||
);
|
||||
|
||||
console.log(` Queued ${downloadIds.length} episodes for download`);
|
||||
|
||||
// Pin the series item
|
||||
await downloads.pinItem(seriesId);
|
||||
|
||||
// Start downloads (the backend will handle queuing)
|
||||
// For now, we'll rely on a download manager to pick them up
|
||||
// TODO: Implement batch download start
|
||||
} catch (error) {
|
||||
console.error("Failed to start series download:", error);
|
||||
} finally {
|
||||
isProcessing = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleClick() {
|
||||
if (isProcessing) return;
|
||||
showQualityPicker = true;
|
||||
}
|
||||
|
||||
function getButtonText(): string {
|
||||
if (allDownloaded) {
|
||||
return "Downloaded";
|
||||
}
|
||||
if (inProgressCount > 0) {
|
||||
return `Downloading... (${inProgressCount})`;
|
||||
}
|
||||
if (completedCount > 0 && episodeCount) {
|
||||
return `Download (${completedCount}/${episodeCount})`;
|
||||
}
|
||||
return "Download Series";
|
||||
}
|
||||
|
||||
function getButtonColor(): string {
|
||||
if (allDownloaded) {
|
||||
return "bg-green-600 hover:bg-green-700";
|
||||
}
|
||||
if (inProgressCount > 0) {
|
||||
return "bg-blue-600 hover:bg-blue-700";
|
||||
}
|
||||
return "bg-[var(--color-jellyfin)] hover:bg-[var(--color-jellyfin-hover)]";
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="relative {className}">
|
||||
<button
|
||||
onclick={handleClick}
|
||||
disabled={isProcessing || allDownloaded}
|
||||
class="flex items-center gap-2 px-4 py-2 rounded-lg text-white font-medium transition-colors {getButtonColor()} {isProcessing || allDownloaded ? 'opacity-70 cursor-not-allowed' : ''}"
|
||||
>
|
||||
{#if inProgressCount > 0}
|
||||
<!-- Spinner -->
|
||||
<svg class="w-5 h-5 animate-spin" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
{:else if allDownloaded}
|
||||
<!-- Checkmark -->
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
{:else}
|
||||
<!-- Download icon -->
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4v12m0 0l-4-4m4 4l4-4m-9 7h10" />
|
||||
</svg>
|
||||
{/if}
|
||||
<span>{getButtonText()}</span>
|
||||
</button>
|
||||
|
||||
<!-- Quality picker dropdown -->
|
||||
{#if showQualityPicker}
|
||||
<div class="absolute z-50 mt-2 left-0 w-48 bg-[var(--color-surface)] rounded-lg shadow-xl border border-gray-700 overflow-hidden">
|
||||
<div class="p-3 border-b border-gray-700">
|
||||
<div class="text-sm font-medium text-white">Download Quality</div>
|
||||
{#if episodeCount}
|
||||
<div class="text-xs text-gray-400 mt-1">{episodeCount} episodes</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#each Object.entries(QUALITY_PRESETS) as [key, preset]}
|
||||
<button
|
||||
onclick={() => startSeriesDownload(key as QualityPreset)}
|
||||
class="w-full px-4 py-3 text-left hover:bg-gray-700 transition-colors flex justify-between items-center"
|
||||
>
|
||||
<span class="text-sm text-white">{preset.label}</span>
|
||||
{#if preset.videoBitrate}
|
||||
<span class="text-xs text-gray-500">{Math.round(preset.videoBitrate / 1_000_000)}Mbps</span>
|
||||
{:else}
|
||||
<span class="text-xs text-gray-500">Direct</span>
|
||||
{/if}
|
||||
</button>
|
||||
{/each}
|
||||
<button
|
||||
onclick={() => showQualityPicker = false}
|
||||
class="w-full px-4 py-3 text-left text-sm text-gray-400 hover:bg-gray-700 border-t border-gray-700"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Click outside to close -->
|
||||
{#if showQualityPicker}
|
||||
<button
|
||||
class="fixed inset-0 z-40"
|
||||
onclick={() => showQualityPicker = false}
|
||||
aria-label="Close quality picker"
|
||||
></button>
|
||||
{/if}
|
||||
Reference in New Issue
Block a user