convert flacs to aac for streaming
This commit is contained in:
125
src/app/music/_components/AudioPlayer.tsx
Normal file
125
src/app/music/_components/AudioPlayer.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import { Spinner } from "~/components/ui/spinner";
|
||||
import AnimateTextIn from "../_components/Animated/AnimateIn";
|
||||
import { ScrollArea } from "~/components/ui/scroll-area";
|
||||
import AnimatePopUp from "../_components/Animated/AnimatePopUp";
|
||||
import AudioPlayer from "./_components/AudioPlayer";
|
||||
export default function MusicPage() {
|
||||
const { data: tracks, isLoading } = trpc.music.list.useQuery();
|
||||
useTimeLine(tracks)
|
||||
@@ -41,9 +42,11 @@ export default function MusicPage() {
|
||||
<p className="text-sm text-muted-foreground gsapant">{track.description}</p>
|
||||
)}
|
||||
<AnimatePopUp position={i + 1.3}>
|
||||
<audio controls className="w-full player" src={track.fileUrl}>
|
||||
Your browser does not support the audio element.
|
||||
</audio>
|
||||
<AudioPlayer
|
||||
src={track.streamUrl ?? track.fileUrl}
|
||||
downloadUrl={track.fileUrl}
|
||||
downloadName={track.fileName}
|
||||
/>
|
||||
</AnimatePopUp>
|
||||
</Card.CardContent>
|
||||
</Card.AnimatedCard>
|
||||
|
||||
Reference in New Issue
Block a user