diff --git a/backend/app/services/download.py b/backend/app/services/download.py index 444c333..36f319c 100644 --- a/backend/app/services/download.py +++ b/backend/app/services/download.py @@ -540,6 +540,20 @@ class DownloadService: url_lower = url.lower() return any(domain in url_lower for domain in audio_domains) + @staticmethod + def _get_auth_hint(url: str) -> str | None: + """Return a user-facing hint for sites that commonly need auth.""" + url_lower = url.lower() + if "instagram.com" in url_lower: + return "Instagram requires login. Upload a cookies.txt from a logged-in browser session." + if "twitter.com" in url_lower or "x.com" in url_lower: + return "Twitter/X often requires login for video. Try uploading a cookies.txt file." + if "tiktok.com" in url_lower: + return "TikTok may block server IPs. Try uploading a cookies.txt file." + if "facebook.com" in url_lower or "fb.watch" in url_lower: + return "Facebook requires login for most videos. Upload a cookies.txt file." + return None + @staticmethod def _guess_ext_from_url(url: str, is_audio: bool) -> str: """Guess the likely output extension based on the source URL.""" @@ -565,7 +579,15 @@ class DownloadService: url, ) if not info: - return {"type": "unknown", "title": None, "entries": [], "is_audio_only": False} + # Provide site-specific hints for known auth-required platforms + hint = self._get_auth_hint(url) + return { + "type": "unknown", + "title": None, + "entries": [], + "is_audio_only": False, + "hint": hint, + } # Domain-based audio detection (more reliable than format sniffing) domain_audio = self._is_audio_only_source(url) @@ -577,7 +599,13 @@ class DownloadService: unavailable_count = 0 for e in entries_raw: if isinstance(e, dict): - title = e.get("title") or e.get("id", "Unknown") + title = e.get("title") + if not title: + # Derive readable name from URL slug when title is absent + # (common in extract_flat mode for SoundCloud, etc.) + entry_url = e.get("url") or e.get("webpage_url", "") + slug = entry_url.rstrip("/").rsplit("/", 1)[-1] if entry_url else "" + title = slug.replace("-", " ").title() if slug else e.get("id", "Unknown") # Detect private/unavailable entries if title in ("[Private video]", "[Deleted video]", "[Unavailable]"): unavailable_count += 1 diff --git a/frontend/src/components/UrlInput.vue b/frontend/src/components/UrlInput.vue index 023196f..73ea1f3 100644 --- a/frontend/src/components/UrlInput.vue +++ b/frontend/src/components/UrlInput.vue @@ -194,7 +194,8 @@ function handlePaste(): void { 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.' + analyzeError.value = (urlInfo.value as any)?.hint + || 'No downloadable media found at this URL.' urlInfo.value = null } else if (!urlInfo.value && !extractError.value) { analyzeError.value = 'Could not reach this URL. Check the address and try again.'