mirror of
https://github.com/xpltdco/media-rip.git
synced 2026-04-03 02:53:58 -06:00
Best quality format, password UX, mobile columns
Best quality format: - Synthetic 'bestvideo+bestaudio/best' entry added at top of format list when the best separate video stream exceeds the best pre-muxed format. Shows as 'Best quality (1920x1080)' in Video+Audio group. - YouTube typically only has 360p pre-muxed but 1080p+ as separate streams — users can now select full quality with auto-merge. - Only appears when there's actually a quality advantage vs pre-muxed. Password change UX: - Enter key on confirm password field submits the change - Auto-logout 1.5s after successful password change - User sees '✓ Password changed' before being redirected home Mobile table: - Status column hidden on mobile (<640px) alongside Progress - Only Name + Actions columns shown — clean two-column layout - Removed mobile-specific status badge font tweaks (column gone)
This commit is contained in:
parent
5da223c5f8
commit
3d778246ca
3 changed files with 50 additions and 11 deletions
|
|
@ -229,6 +229,47 @@ class DownloadService:
|
||||||
key=lambda fi: _parse_resolution_height(fi.resolution),
|
key=lambda fi: _parse_resolution_height(fi.resolution),
|
||||||
reverse=True,
|
reverse=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Add synthetic "best quality" entries at the top.
|
||||||
|
# yt-dlp can merge separate video+audio streams for best quality,
|
||||||
|
# but those don't appear as pre-muxed formats in the format list.
|
||||||
|
best_video = None
|
||||||
|
best_audio = None
|
||||||
|
for f in formats_raw:
|
||||||
|
vcodec = f.get("vcodec", "none")
|
||||||
|
acodec = f.get("acodec", "none")
|
||||||
|
height = f.get("height") or 0
|
||||||
|
if vcodec and vcodec != "none" and height > 0:
|
||||||
|
if best_video is None or height > (best_video.get("height") or 0):
|
||||||
|
best_video = f
|
||||||
|
if acodec and acodec != "none" and (vcodec == "none" or not vcodec):
|
||||||
|
if best_audio is None:
|
||||||
|
best_audio = f
|
||||||
|
|
||||||
|
if best_video:
|
||||||
|
bv_height = best_video.get("height", 0)
|
||||||
|
bv_res = f"{best_video.get('width', '?')}x{bv_height}"
|
||||||
|
# Only add if the best separate video exceeds the best pre-muxed
|
||||||
|
best_premuxed_height = 0
|
||||||
|
for f in formats_raw:
|
||||||
|
vc = f.get("vcodec", "none")
|
||||||
|
ac = f.get("acodec", "none")
|
||||||
|
if vc and vc != "none" and ac and ac != "none":
|
||||||
|
h = f.get("height") or 0
|
||||||
|
if h > best_premuxed_height:
|
||||||
|
best_premuxed_height = h
|
||||||
|
|
||||||
|
if bv_height > best_premuxed_height:
|
||||||
|
result.insert(0, FormatInfo(
|
||||||
|
format_id="bestvideo+bestaudio/best",
|
||||||
|
ext=best_video.get("ext", "webm"),
|
||||||
|
resolution=bv_res,
|
||||||
|
codec=best_video.get("vcodec"),
|
||||||
|
format_note=f"Best quality ({bv_res})",
|
||||||
|
vcodec=best_video.get("vcodec"),
|
||||||
|
acodec="merged",
|
||||||
|
))
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
async def cancel(self, job_id: str) -> None:
|
async def cancel(self, job_id: str) -> None:
|
||||||
|
|
|
||||||
|
|
@ -95,13 +95,15 @@ async function changePassword() {
|
||||||
})
|
})
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
// Update stored credentials to use new password
|
passwordChanged.value = true
|
||||||
store.password = newPassword.value
|
|
||||||
currentPassword.value = ''
|
currentPassword.value = ''
|
||||||
newPassword.value = ''
|
newPassword.value = ''
|
||||||
confirmPassword.value = ''
|
confirmPassword.value = ''
|
||||||
passwordChanged.value = true
|
// Auto-logout after 1.5s so user sees the success message
|
||||||
setTimeout(() => { passwordChanged.value = false }, 3000)
|
setTimeout(() => {
|
||||||
|
store.logout()
|
||||||
|
router.push('/')
|
||||||
|
}, 1500)
|
||||||
} else {
|
} else {
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
passwordError.value = data.detail || 'Failed to change password'
|
passwordError.value = data.detail || 'Failed to change password'
|
||||||
|
|
@ -335,6 +337,7 @@ function formatFilesize(bytes: number | null): string {
|
||||||
placeholder="Confirm new password"
|
placeholder="Confirm new password"
|
||||||
autocomplete="new-password"
|
autocomplete="new-password"
|
||||||
class="settings-input"
|
class="settings-input"
|
||||||
|
@keydown.enter="changePassword"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="settings-actions" style="margin-top: var(--space-sm);">
|
<div class="settings-actions" style="margin-top: var(--space-sm);">
|
||||||
|
|
|
||||||
|
|
@ -515,7 +515,7 @@ async function clearJob(jobId: string): Promise<void> {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Mobile: hide speed, ETA, and progress columns */
|
/* Mobile: hide speed, ETA, progress, and status columns */
|
||||||
@media (max-width: 639px) {
|
@media (max-width: 639px) {
|
||||||
.hide-mobile {
|
.hide-mobile {
|
||||||
display: none;
|
display: none;
|
||||||
|
|
@ -527,7 +527,7 @@ async function clearJob(jobId: string): Promise<void> {
|
||||||
}
|
}
|
||||||
|
|
||||||
.col-status {
|
.col-status {
|
||||||
width: 75px;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.col-progress {
|
.col-progress {
|
||||||
|
|
@ -553,11 +553,6 @@ async function clearJob(jobId: string): Promise<void> {
|
||||||
.action-group {
|
.action-group {
|
||||||
gap: 2px;
|
gap: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-badge {
|
|
||||||
font-size: 0.625rem;
|
|
||||||
padding: 1px 4px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Toast notification */
|
/* Toast notification */
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue