Khaneja commited on
Commit ·
1cc8a00
1
Parent(s): 27820e7
feat: implement mp3 quality selection and fix uuid routes
Browse files- app.py +23 -1
- templates/index.html +15 -4
app.py
CHANGED
|
@@ -20,6 +20,8 @@ def run_download(job_id, url, format_choice, format_id):
|
|
| 20 |
cmd = ["yt-dlp", "--no-playlist", "-o", out_template]
|
| 21 |
|
| 22 |
if format_choice == "audio":
|
|
|
|
|
|
|
| 23 |
cmd += ["-x", "--audio-format", "mp3"]
|
| 24 |
elif format_id:
|
| 25 |
cmd += ["-f", f"{format_id}+bestaudio/best", "--merge-output-format", "mp4"]
|
|
@@ -95,7 +97,16 @@ def get_info():
|
|
| 95 |
|
| 96 |
# Build quality options — keep best format per resolution
|
| 97 |
best_by_height = {}
|
|
|
|
| 98 |
for f in info.get("formats", []):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
height = f.get("height")
|
| 100 |
if height and f.get("vcodec", "none") != "none":
|
| 101 |
tbr = f.get("tbr") or 0
|
|
@@ -111,12 +122,22 @@ def get_info():
|
|
| 111 |
})
|
| 112 |
formats.sort(key=lambda x: x["height"], reverse=True)
|
| 113 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 114 |
return jsonify({
|
| 115 |
"title": info.get("title", ""),
|
| 116 |
"thumbnail": info.get("thumbnail", ""),
|
| 117 |
"duration": info.get("duration"),
|
| 118 |
"uploader": info.get("uploader", ""),
|
| 119 |
"formats": formats,
|
|
|
|
| 120 |
})
|
| 121 |
except subprocess.TimeoutExpired:
|
| 122 |
return jsonify({"error": "Timed out fetching video info"}), 400
|
|
@@ -158,7 +179,8 @@ def check_status(job_id):
|
|
| 158 |
|
| 159 |
|
| 160 |
@app.route("/api/file/<job_id>")
|
| 161 |
-
|
|
|
|
| 162 |
job = jobs.get(job_id)
|
| 163 |
if not job or job["status"] != "done":
|
| 164 |
return jsonify({"error": "File not ready"}), 404
|
|
|
|
| 20 |
cmd = ["yt-dlp", "--no-playlist", "-o", out_template]
|
| 21 |
|
| 22 |
if format_choice == "audio":
|
| 23 |
+
if format_id:
|
| 24 |
+
cmd += ["-f", format_id]
|
| 25 |
cmd += ["-x", "--audio-format", "mp3"]
|
| 26 |
elif format_id:
|
| 27 |
cmd += ["-f", f"{format_id}+bestaudio/best", "--merge-output-format", "mp4"]
|
|
|
|
| 97 |
|
| 98 |
# Build quality options — keep best format per resolution
|
| 99 |
best_by_height = {}
|
| 100 |
+
audio_by_abr = {}
|
| 101 |
for f in info.get("formats", []):
|
| 102 |
+
if f.get("vcodec") == "none":
|
| 103 |
+
abr = f.get("abr")
|
| 104 |
+
if abr:
|
| 105 |
+
abr = int(abr)
|
| 106 |
+
if abr not in audio_by_abr or (f.get("asr", 0) > audio_by_abr[abr].get("asr", 0)):
|
| 107 |
+
audio_by_abr[abr] = f
|
| 108 |
+
continue
|
| 109 |
+
|
| 110 |
height = f.get("height")
|
| 111 |
if height and f.get("vcodec", "none") != "none":
|
| 112 |
tbr = f.get("tbr") or 0
|
|
|
|
| 122 |
})
|
| 123 |
formats.sort(key=lambda x: x["height"], reverse=True)
|
| 124 |
|
| 125 |
+
audio_formats = []
|
| 126 |
+
for abr, f in audio_by_abr.items():
|
| 127 |
+
audio_formats.append({
|
| 128 |
+
"id": f["format_id"],
|
| 129 |
+
"label": f"{abr}kbps",
|
| 130 |
+
"abr": abr,
|
| 131 |
+
})
|
| 132 |
+
audio_formats.sort(key=lambda x: x["abr"], reverse=True)
|
| 133 |
+
|
| 134 |
return jsonify({
|
| 135 |
"title": info.get("title", ""),
|
| 136 |
"thumbnail": info.get("thumbnail", ""),
|
| 137 |
"duration": info.get("duration"),
|
| 138 |
"uploader": info.get("uploader", ""),
|
| 139 |
"formats": formats,
|
| 140 |
+
"audioFormats": audio_formats,
|
| 141 |
})
|
| 142 |
except subprocess.TimeoutExpired:
|
| 143 |
return jsonify({"error": "Timed out fetching video info"}), 400
|
|
|
|
| 179 |
|
| 180 |
|
| 181 |
@app.route("/api/file/<job_id>")
|
| 182 |
+
@app.route("/api/file/<job_id>/<path:filename>")
|
| 183 |
+
def download_file(job_id, filename=None):
|
| 184 |
job = jobs.get(job_id)
|
| 185 |
if not job or job["status"] != "done":
|
| 186 |
return jsonify({"error": "File not ready"}), 404
|
templates/index.html
CHANGED
|
@@ -593,6 +593,8 @@
|
|
| 593 |
uploader: data.uploader || '',
|
| 594 |
formats: data.formats || [],
|
| 595 |
selectedFormatId: data.formats?.[0]?.id || null,
|
|
|
|
|
|
|
| 596 |
};
|
| 597 |
}
|
| 598 |
} catch (err) {
|
|
@@ -664,7 +666,11 @@
|
|
| 664 |
}
|
| 665 |
|
| 666 |
let qualityChips = '';
|
| 667 |
-
if (
|
|
|
|
|
|
|
|
|
|
|
|
|
| 668 |
qualityChips = c.formats.map(f =>
|
| 669 |
`<button class="q-chip${f.id === c.selectedFormatId ? ' active' : ''}" onclick="pickFormat(${idx}, '${f.id}')">${f.label}</button>`
|
| 670 |
).join('');
|
|
@@ -705,7 +711,11 @@
|
|
| 705 |
}
|
| 706 |
|
| 707 |
function pickFormat(idx, formatId) {
|
| 708 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 709 |
renderCard(idx);
|
| 710 |
}
|
| 711 |
|
|
@@ -716,13 +726,14 @@
|
|
| 716 |
renderCard(idx);
|
| 717 |
|
| 718 |
try {
|
|
|
|
| 719 |
const res = await fetch('/api/download', {
|
| 720 |
method: 'POST',
|
| 721 |
headers: { 'Content-Type': 'application/json' },
|
| 722 |
body: JSON.stringify({
|
| 723 |
url: c.url,
|
| 724 |
format: currentFormat,
|
| 725 |
-
format_id:
|
| 726 |
title: c.title || '',
|
| 727 |
}),
|
| 728 |
});
|
|
@@ -773,7 +784,7 @@
|
|
| 773 |
const c = cardData[idx];
|
| 774 |
if (!c.jobId) return;
|
| 775 |
const a = document.createElement('a');
|
| 776 |
-
a.href = `/api/file/${c.jobId}`;
|
| 777 |
a.download = c.filename || '';
|
| 778 |
a.click();
|
| 779 |
}
|
|
|
|
| 593 |
uploader: data.uploader || '',
|
| 594 |
formats: data.formats || [],
|
| 595 |
selectedFormatId: data.formats?.[0]?.id || null,
|
| 596 |
+
audioFormats: data.audioFormats || [],
|
| 597 |
+
selectedAudioFormatId: data.audioFormats?.[0]?.id || null,
|
| 598 |
};
|
| 599 |
}
|
| 600 |
} catch (err) {
|
|
|
|
| 666 |
}
|
| 667 |
|
| 668 |
let qualityChips = '';
|
| 669 |
+
if (isAudio && c.audioFormats && c.audioFormats.length > 1) {
|
| 670 |
+
qualityChips = c.audioFormats.map(f =>
|
| 671 |
+
`<button class="q-chip${f.id === c.selectedAudioFormatId ? ' active' : ''}" onclick="pickFormat(${idx}, '${f.id}')">${f.label}</button>`
|
| 672 |
+
).join('');
|
| 673 |
+
} else if (!isAudio && c.formats && c.formats.length > 1) {
|
| 674 |
qualityChips = c.formats.map(f =>
|
| 675 |
`<button class="q-chip${f.id === c.selectedFormatId ? ' active' : ''}" onclick="pickFormat(${idx}, '${f.id}')">${f.label}</button>`
|
| 676 |
).join('');
|
|
|
|
| 711 |
}
|
| 712 |
|
| 713 |
function pickFormat(idx, formatId) {
|
| 714 |
+
if (currentFormat === 'audio') {
|
| 715 |
+
cardData[idx].selectedAudioFormatId = formatId;
|
| 716 |
+
} else {
|
| 717 |
+
cardData[idx].selectedFormatId = formatId;
|
| 718 |
+
}
|
| 719 |
renderCard(idx);
|
| 720 |
}
|
| 721 |
|
|
|
|
| 726 |
renderCard(idx);
|
| 727 |
|
| 728 |
try {
|
| 729 |
+
const payloadFormatId = currentFormat === 'audio' ? c.selectedAudioFormatId : c.selectedFormatId;
|
| 730 |
const res = await fetch('/api/download', {
|
| 731 |
method: 'POST',
|
| 732 |
headers: { 'Content-Type': 'application/json' },
|
| 733 |
body: JSON.stringify({
|
| 734 |
url: c.url,
|
| 735 |
format: currentFormat,
|
| 736 |
+
format_id: payloadFormatId,
|
| 737 |
title: c.title || '',
|
| 738 |
}),
|
| 739 |
});
|
|
|
|
| 784 |
const c = cardData[idx];
|
| 785 |
if (!c.jobId) return;
|
| 786 |
const a = document.createElement('a');
|
| 787 |
+
a.href = `/api/file/${c.jobId}/${encodeURIComponent(c.filename || 'download.mp4')}`;
|
| 788 |
a.download = c.filename || '';
|
| 789 |
a.click();
|
| 790 |
}
|