Compare commits

..

2 Commits

Author SHA1 Message Date
Mattias Tall
83e2685c6a Quality selector auto-triggers re-download on change when video is saved
Changing the quality dropdown while a video is already downloaded now
immediately deletes the old file and starts a fresh download at the new
quality — no separate Re-download button needed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 17:41:06 +02:00
Mattias Tall
c24964a1ee Fix quality formats: drop AVC1/MP4 codec restrictions that caused 360p fallback
Most modern YouTube videos use VP9/AV1, so the old bestvideo[ext=mp4][vcodec^=avc1]
filter always failed and fell through to format codes 22/18 (720p/360p).
--merge-output-format mp4 handles the container; no need to restrict codec.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 17:39:55 +02:00
2 changed files with 17 additions and 22 deletions

View File

@@ -449,15 +449,15 @@ def fetch_dislike_count(youtube_video_id: str) -> int | None:
QUALITY_FORMATS = { QUALITY_FORMATS = {
"best": "bestvideo[ext=mp4][vcodec^=avc1]+bestaudio[ext=m4a]/bestvideo[ext=mp4]+bestaudio[ext=m4a]/22/18/bestvideo+bestaudio/best", "best": "bestvideo+bestaudio/best",
"2160p": "bestvideo[ext=mp4][height<=2160]+bestaudio[ext=m4a]/bestvideo[height<=2160]+bestaudio/best[height<=2160]", "2160p": "bestvideo[height<=2160]+bestaudio/best[height<=2160]",
"1440p": "bestvideo[ext=mp4][height<=1440]+bestaudio[ext=m4a]/bestvideo[height<=1440]+bestaudio/best[height<=1440]", "1440p": "bestvideo[height<=1440]+bestaudio/best[height<=1440]",
"1080p": "bestvideo[ext=mp4][vcodec^=avc1][height<=1080]+bestaudio[ext=m4a]/bestvideo[ext=mp4][height<=1080]+bestaudio[ext=m4a]/137+140/22/best[height<=1080]", "1080p": "bestvideo[height<=1080]+bestaudio/best[height<=1080]",
"720p": "bestvideo[ext=mp4][vcodec^=avc1][height<=720]+bestaudio[ext=m4a]/bestvideo[ext=mp4][height<=720]+bestaudio[ext=m4a]/22/best[height<=720]", "720p": "bestvideo[height<=720]+bestaudio/best[height<=720]",
"480p": "bestvideo[ext=mp4][vcodec^=avc1][height<=480]+bestaudio[ext=m4a]/bestvideo[ext=mp4][height<=480]+bestaudio[ext=m4a]/18/best[height<=480]", "480p": "bestvideo[height<=480]+bestaudio/best[height<=480]",
"360p": "bestvideo[ext=mp4][height<=360]+bestaudio[ext=m4a]/18/best[height<=360]", "360p": "bestvideo[height<=360]+bestaudio/best[height<=360]",
"240p": "bestvideo[ext=mp4][height<=240]+bestaudio[ext=m4a]/best[height<=240]", "240p": "bestvideo[height<=240]+bestaudio/best[height<=240]",
"144p": "bestvideo[ext=mp4][height<=144]+bestaudio[ext=m4a]/best[height<=144]", "144p": "bestvideo[height<=144]+bestaudio/best[height<=144]",
} }

View File

@@ -710,7 +710,7 @@ export default function Watch() {
}, [playRequested, fileReady, dlStatus?.status, dlStatus?.file_url, video?.is_downloaded, youtubeVideoId, pollForFile]); // eslint-disable-line react-hooks/exhaustive-deps }, [playRequested, fileReady, dlStatus?.status, dlStatus?.file_url, video?.is_downloaded, youtubeVideoId, pollForFile]); // eslint-disable-line react-hooks/exhaustive-deps
const downloadMut = useMutation({ const downloadMut = useMutation({
mutationFn: () => createDownload(youtubeVideoId, selectedQuality), mutationFn: (quality) => createDownload(youtubeVideoId, quality ?? selectedQuality),
onSuccess: (res) => { onSuccess: (res) => {
setDownloadId(res.data.id); setDownloadId(res.data.id);
refetchVideo(); refetchVideo();
@@ -722,7 +722,7 @@ export default function Watch() {
setPlayRequested(true); setPlayRequested(true);
downloadMut.mutate(); downloadMut.mutate();
}, [downloadMut]); }, [downloadMut]);
const handleRedownload = useCallback(async () => { const handleRedownload = useCallback(async (quality) => {
const dlId = downloadId ?? allDownloads.find( const dlId = downloadId ?? allDownloads.find(
d => d.youtube_video_id === youtubeVideoId && d.status === "complete" d => d.youtube_video_id === youtubeVideoId && d.status === "complete"
)?.id; )?.id;
@@ -736,7 +736,7 @@ export default function Watch() {
setIsRedownloading(false); setIsRedownloading(false);
qc.invalidateQueries({ queryKey: ["downloads"] }); qc.invalidateQueries({ queryKey: ["downloads"] });
refetchVideo(); refetchVideo();
downloadMut.mutate(); downloadMut.mutate(quality);
}, [downloadId, allDownloads, youtubeVideoId, downloadMut, refetchVideo, qc]); }, [downloadId, allDownloads, youtubeVideoId, downloadMut, refetchVideo, qc]);
const saveProgress = useCallback((secs) => { const saveProgress = useCallback((secs) => {
@@ -933,7 +933,11 @@ export default function Watch() {
{!isDownloading && !downloadMut.isPending && !isRedownloading && ( {!isDownloading && !downloadMut.isPending && !isRedownloading && (
<select <select
value={selectedQuality ?? "best"} value={selectedQuality ?? "best"}
onChange={(e) => setSelectedQuality(e.target.value)} onChange={(e) => {
const q = e.target.value;
setSelectedQuality(q);
if (dlComplete) handleRedownload(q);
}}
className="bg-zinc-800 text-zinc-300 text-xs rounded-full px-3 py-2 border border-zinc-700 focus:outline-none focus:border-accent" className="bg-zinc-800 text-zinc-300 text-xs rounded-full px-3 py-2 border border-zinc-700 focus:outline-none focus:border-accent"
> >
{[ {[
@@ -987,15 +991,6 @@ export default function Watch() {
</Chip> </Chip>
)} )}
{dlComplete && (
<Chip onClick={handleRedownload} disabled={isRedownloading || downloadMut.isPending}>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
</svg>
Re-download
</Chip>
)}
{video?.id && ( {video?.id && (
<> <>