feat: YouTube/Instagram/MP3 download with modal + yt-dlp support

Sync from gaming-hub soundboard plugin:
- Add yt-dlp URL detection (YouTube, Instagram) + direct MP3 support
- downloadWithYtDlp() with verbose logging, error detection, fallback scan
- handleUrlDownload() shared logic with custom filename + rename
- Download modal: filename input, progress spinner, success/error phases
- URL type badges (YT/IG/MP3) in toolbar input
- Auto-prepend https:// for URLs without protocol
- Fix Dockerfile: yt-dlp_linux standalone binary (no Python needed)
- download-url route (admin-only, save without playing)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Daniel 2026-03-07 00:13:48 +01:00
parent 4875747dc5
commit 3c8ad63f99
5 changed files with 579 additions and 61 deletions

View file

@ -641,6 +641,24 @@ input, select {
pointer-events: none;
}
.url-import-tag {
flex-shrink: 0;
padding: 1px 6px;
border-radius: 8px;
font-size: 10px;
font-weight: 800;
letter-spacing: .5px;
text-transform: uppercase;
}
.url-import-tag.valid {
background: rgba(46, 204, 113, .18);
color: #2ecc71;
}
.url-import-tag.invalid {
background: rgba(231, 76, 60, .18);
color: #e74c3c;
}
/* ── Toolbar Buttons ── */
.tb-btn {
display: flex;
@ -2063,6 +2081,141 @@ input, select {
margin-top: 2px;
}
/*
Download Modal
*/
.dl-modal-overlay {
position: fixed; inset: 0;
background: rgba(0, 0, 0, .55);
display: flex; align-items: center; justify-content: center;
z-index: 300;
animation: fade-in 150ms ease;
}
@keyframes fade-in { from { opacity: 0; } to { opacity: 1; } }
.dl-modal {
width: 420px; max-width: 92vw;
background: var(--bg-secondary);
border: 1px solid rgba(255, 255, 255, .1);
border-radius: 16px;
box-shadow: 0 12px 60px rgba(0, 0, 0, .5);
animation: scale-in 200ms cubic-bezier(.16, 1, .3, 1);
}
@keyframes scale-in { from { opacity: 0; transform: scale(.95); } to { opacity: 1; transform: scale(1); } }
.dl-modal-header {
display: flex; align-items: center; gap: 8px;
padding: 14px 16px;
border-bottom: 1px solid rgba(255, 255, 255, .06);
font-size: 14px; font-weight: 700;
color: var(--text-normal);
}
.dl-modal-header .material-icons { color: var(--accent); }
.dl-modal-close {
margin-left: auto;
display: flex; align-items: center; justify-content: center;
width: 26px; height: 26px; border-radius: 50%;
border: none; background: rgba(255,255,255,.06);
color: var(--text-muted); cursor: pointer;
transition: background var(--transition);
}
.dl-modal-close:hover { background: rgba(255,255,255,.14); color: var(--text-normal); }
.dl-modal-body { padding: 16px; display: flex; flex-direction: column; gap: 14px; }
/* URL display */
.dl-modal-url {
display: flex; align-items: center; gap: 8px;
padding: 8px 10px; border-radius: 8px;
background: rgba(0, 0, 0, .2);
overflow: hidden;
}
.dl-modal-tag {
flex-shrink: 0; padding: 2px 8px; border-radius: 6px;
font-size: 10px; font-weight: 800; letter-spacing: .5px; text-transform: uppercase;
}
.dl-modal-tag.youtube { background: rgba(255, 0, 0, .18); color: #ff4444; }
.dl-modal-tag.instagram { background: rgba(225, 48, 108, .18); color: #e1306c; }
.dl-modal-tag.mp3 { background: rgba(46, 204, 113, .18); color: #2ecc71; }
.dl-modal-url-text {
font-size: 11px; color: var(--text-faint);
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* Filename field */
.dl-modal-field { display: flex; flex-direction: column; gap: 5px; }
.dl-modal-label { font-size: 11px; font-weight: 600; color: var(--text-muted); text-transform: uppercase; letter-spacing: .5px; }
.dl-modal-input-wrap {
display: flex; align-items: center;
border: 1px solid rgba(255, 255, 255, .1); border-radius: 8px;
background: rgba(0, 0, 0, .15);
overflow: hidden;
transition: border-color var(--transition);
}
.dl-modal-input-wrap:focus-within { border-color: var(--accent); }
.dl-modal-input {
flex: 1; border: none; background: transparent;
padding: 8px 10px; color: var(--text-normal);
font-size: 13px; font-family: var(--font); outline: none;
}
.dl-modal-input::placeholder { color: var(--text-faint); }
.dl-modal-ext {
padding: 0 10px; font-size: 12px; font-weight: 600;
color: var(--text-faint); background: rgba(255, 255, 255, .04);
align-self: stretch; display: flex; align-items: center;
}
.dl-modal-hint { font-size: 10px; color: var(--text-faint); }
/* Progress spinner */
.dl-modal-progress {
display: flex; align-items: center; gap: 12px;
padding: 20px 0; justify-content: center;
font-size: 13px; color: var(--text-muted);
}
.dl-modal-spinner {
width: 24px; height: 24px; border-radius: 50%;
border: 3px solid rgba(var(--accent-rgb), .2);
border-top-color: var(--accent);
animation: spin 800ms linear infinite;
}
/* Success */
.dl-modal-success {
display: flex; align-items: center; gap: 10px;
padding: 16px 0; justify-content: center;
font-size: 13px; color: var(--text-normal);
}
.dl-modal-check { color: #2ecc71; font-size: 28px; }
/* Error */
.dl-modal-error {
display: flex; align-items: center; gap: 10px;
padding: 12px 0; justify-content: center;
font-size: 13px; color: #e74c3c;
}
/* Actions */
.dl-modal-actions {
display: flex; justify-content: flex-end; gap: 8px;
padding: 0 16px 14px;
}
.dl-modal-cancel {
padding: 7px 14px; border-radius: 8px;
border: 1px solid rgba(255, 255, 255, .1); background: transparent;
color: var(--text-muted); font-size: 12px; font-weight: 600;
cursor: pointer; transition: all var(--transition);
}
.dl-modal-cancel:hover { background: rgba(255,255,255,.06); color: var(--text-normal); }
.dl-modal-submit {
display: flex; align-items: center; gap: 5px;
padding: 7px 16px; border-radius: 8px;
border: none; background: var(--accent);
color: #fff; font-size: 12px; font-weight: 700;
cursor: pointer; transition: filter var(--transition);
}
.dl-modal-submit:hover { filter: brightness(1.15); }
/*
Utility
*/