diff --git a/backend/app/services/download.py b/backend/app/services/download.py
index ed12622..4828b3b 100644
--- a/backend/app/services/download.py
+++ b/backend/app/services/download.py
@@ -159,6 +159,7 @@ class DownloadService:
"no_warnings": True,
"noprogress": True,
"noplaylist": True, # Individual jobs — don't re-expand playlists
+ "overwrites": True, # Allow re-downloading same URL with different format
}
if job_create.format_id:
opts["format"] = job_create.format_id
diff --git a/frontend/src/App.vue b/frontend/src/App.vue
index 8979e75..9ea3a13 100644
--- a/frontend/src/App.vue
+++ b/frontend/src/App.vue
@@ -22,7 +22,17 @@ onMounted(async () => {
-
-
-
+
+
+
diff --git a/frontend/src/components/AdminPanel.vue b/frontend/src/components/AdminPanel.vue
index 9e492b3..2344c5d 100644
--- a/frontend/src/components/AdminPanel.vue
+++ b/frontend/src/components/AdminPanel.vue
@@ -2,10 +2,12 @@
import { onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import { useAdminStore } from '@/stores/admin'
+import { useConfigStore } from '@/stores/config'
import { api } from '@/api/client'
import AdminLogin from './AdminLogin.vue'
const store = useAdminStore()
+const configStore = useConfigStore()
const router = useRouter()
const activeTab = ref<'sessions' | 'storage' | 'purge' | 'settings'>('sessions')
@@ -52,6 +54,8 @@ async function saveSettings() {
default_audio_format: defaultAudioFormat.value,
})
if (ok) {
+ // Reload public config so main page picks up new defaults
+ await configStore.loadConfig()
settingsSaved.value = true
setTimeout(() => { settingsSaved.value = false }, 3000)
}
diff --git a/frontend/src/components/AppLayout.vue b/frontend/src/components/AppLayout.vue
index 715ca4a..6a5bd60 100644
--- a/frontend/src/components/AppLayout.vue
+++ b/frontend/src/components/AppLayout.vue
@@ -52,7 +52,7 @@ const activeTab = ref('submit')
.app-layout {
display: flex;
flex-direction: column;
- min-height: calc(100vh - var(--header-height));
+ flex: 1;
}
.layout-main {
diff --git a/frontend/src/components/DownloadQueue.vue b/frontend/src/components/DownloadQueue.vue
index be1b334..e429fd0 100644
--- a/frontend/src/components/DownloadQueue.vue
+++ b/frontend/src/components/DownloadQueue.vue
@@ -195,6 +195,7 @@ function handleClear(): void {
.btn-clear {
min-height: 36px;
+ min-width: 70px;
font-size: var(--font-size-sm);
padding: var(--space-xs) var(--space-md);
background: var(--color-surface);
diff --git a/frontend/src/components/DownloadTable.vue b/frontend/src/components/DownloadTable.vue
index 07baa8c..0244717 100644
--- a/frontend/src/components/DownloadTable.vue
+++ b/frontend/src/components/DownloadTable.vue
@@ -441,11 +441,13 @@ async function clearJob(jobId: string): Promise {
}
.action-btn {
- display: flex;
+ display: inline-flex;
align-items: center;
justify-content: center;
width: 30px;
height: 30px;
+ min-width: 30px;
+ min-height: 30px;
padding: 0;
background: transparent;
border: 1px solid var(--color-border);
@@ -454,6 +456,8 @@ async function clearJob(jobId: string): Promise {
cursor: pointer;
transition: all 0.15s ease;
text-decoration: none;
+ box-sizing: border-box;
+ line-height: 1;
}
.action-btn:hover {
diff --git a/frontend/src/components/UrlInput.vue b/frontend/src/components/UrlInput.vue
index b0e1d7d..f83a679 100644
--- a/frontend/src/components/UrlInput.vue
+++ b/frontend/src/components/UrlInput.vue
@@ -22,6 +22,7 @@ const audioLocked = ref(false) // true when source is audio-only
// Unified loading state for URL check + format extraction
const isAnalyzing = ref(false)
const analyzePhase = ref('')
+const analyzeError = ref(null)
const phaseMessages = [
'Peeking at the URL…',
'Interrogating the server…',
@@ -95,6 +96,7 @@ watch(url, (newUrl) => {
formats.value = []
selectedFormatId.value = null
extractError.value = null
+ analyzeError.value = null
audioLocked.value = false
showOptions.value = false
selectedEntries.value = new Set()
@@ -104,6 +106,7 @@ watch(url, (newUrl) => {
formats.value = []
selectedFormatId.value = null
extractError.value = null
+ analyzeError.value = null
audioLocked.value = false
selectedEntries.value = new Set()
}
@@ -115,6 +118,11 @@ const selectedEntries = ref>(new Set())
/** Whether formats have been fetched for the current URL. */
const formatsReady = computed(() => formats.value.length > 0)
+/** URL has been analyzed and content was found. */
+const urlReady = computed(() =>
+ !!urlInfo.value && urlInfo.value.type !== 'unknown'
+)
+
async function extractFormats(): Promise {
const trimmed = url.value.trim()
if (!trimmed) return
@@ -180,9 +188,16 @@ function handlePaste(): void {
setTimeout(async () => {
if (url.value.trim()) {
isAnalyzing.value = true
+ analyzeError.value = null
startAnalyzePhase()
try {
await Promise.all([extractFormats(), fetchUrlInfo()])
+ // Check if URL yielded anything useful
+ if (urlInfo.value?.type === 'unknown') {
+ analyzeError.value = 'No downloadable media found at this URL.'
+ } else if (!urlInfo.value && !extractError.value) {
+ analyzeError.value = 'Could not reach this URL. Check the address and try again.'
+ }
} finally {
isAnalyzing.value = false
stopAnalyzePhase()
@@ -325,7 +340,7 @@ function formatTooltip(fmt: string): string {
@@ -378,6 +393,10 @@ function formatTooltip(fmt: string): string {
+
+