convert flacs to aac for streaming

This commit is contained in:
2026-06-16 12:49:30 +02:00
parent f5e8b87846
commit 7aa1746f97
12 changed files with 401 additions and 8 deletions

View File

@@ -0,0 +1,125 @@
'use client'
import { useRef, useState } from "react";
import { Download, Loader2, Pause, Play } from "lucide-react";
import { Slider } from "~/components/ui/slider";
import { Button } from "~/components/ui/button";
import { toast } from "sonner";
function formatTime(seconds: number) {
if (!Number.isFinite(seconds)) return "0:00";
const m = Math.floor(seconds / 60);
const s = Math.floor(seconds % 60);
return `${m}:${s.toString().padStart(2, "0")}`;
}
export default function AudioPlayer(props: {
/** Streaming-friendly source the player actually plays. */
src: string;
/** Original high-quality file offered via the download button. */
downloadUrl: string;
downloadName: string;
}) {
const audioRef = useRef<HTMLAudioElement>(null);
const [playing, setPlaying] = useState(false);
const [current, setCurrent] = useState(0);
const [duration, setDuration] = useState(0);
const [seeking, setSeeking] = useState(false);
const [downloading, setDownloading] = useState(false);
function togglePlay() {
const audio = audioRef.current;
if (!audio) return;
if (audio.paused) {
audio.play();
} else {
audio.pause();
}
}
async function handleDownload() {
setDownloading(true);
try {
// The download file is cross-origin, so the <a download> attribute is
// ignored — fetch it as a blob to force a real download.
const res = await fetch(props.downloadUrl);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const blob = await res.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = props.downloadName;
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(url);
} catch (e) {
toast(`Download failed: ${e instanceof Error ? e.message : "unknown error"}`);
} finally {
setDownloading(false);
}
}
return (
<div className="flex items-center gap-3 rounded-lg border bg-transparent px-3 py-2">
<audio
ref={audioRef}
src={props.src}
preload="metadata"
onPlay={() => setPlaying(true)}
onPause={() => setPlaying(false)}
onLoadedMetadata={(e) => setDuration(e.currentTarget.duration)}
onTimeUpdate={(e) => {
if (!seeking) setCurrent(e.currentTarget.currentTime);
}}
onEnded={() => setPlaying(false)}
/>
<Button
type="button"
size="icon"
variant="ghost"
aria-label={playing ? "Pause" : "Play"}
onClick={togglePlay}
>
{playing ? <Pause /> : <Play />}
</Button>
<span className="w-10 shrink-0 text-right font-mono text-xs text-muted-foreground tabular-nums">
{formatTime(current)}
</span>
<Slider
className="flex-1"
min={0}
max={duration || 1}
step={0.1}
value={[current]}
onValueChange={([v]) => {
setSeeking(true);
setCurrent(v ?? 0);
}}
onValueCommit={([v]) => {
if (audioRef.current) audioRef.current.currentTime = v ?? 0;
setSeeking(false);
}}
/>
<span className="w-10 shrink-0 font-mono text-xs text-muted-foreground tabular-nums">
{formatTime(duration)}
</span>
<Button
type="button"
size="icon"
variant="ghost"
aria-label="Download lossless file"
title="Download lossless file"
disabled={downloading}
onClick={handleDownload}
>
{downloading ? <Loader2 className="animate-spin" /> : <Download />}
</Button>
</div>
);
}