Overview
OfflineTube’s Download Manager provides complete control over your downloads with real-time progress tracking, quality selection, and format options. All downloads run in the background using yt-dlp and FFmpeg.
Starting a Download
From the Explorer
- Search for a video in the Explorer tab
- Click on a video to see its details
- Choose your preferred quality and format
- Click Descargar Video or Descargar Audio
Quality Options
OfflineTube shows all available formats for each video:
Video formats:
- Resolution options from 240p to 4K (2160p)
- Codec information (h264, av1, vp9)
- File size estimates
- Automatically merges best audio with selected video quality
Audio formats:
- Bitrate options (48kbps to 256kbps+)
- Codec details (opus, m4a, mp3)
- Audio-only extraction for music
Video downloads automatically include the best available audio track using format selector: {format_id}+bestaudio/best
Download API
Starting a Download
// From page.tsx:192-221
const handleExplorerDownload = async (
url: string,
options: Omit<DownloadOptions, 'url' | 'subtitle_langs' | 'download_thumbnail'>
) => {
const response = await fetch(`${API_URL}/api/download`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
url,
quality: options.quality,
format_id: options.format_id,
download_type: options.download_type,
subtitle_langs: [],
download_thumbnail: false,
}),
});
if (response.ok) {
toast.success('Descarga iniciada');
setActiveTab('downloads');
fetchDownloads(true);
}
}
Backend Download Endpoint
# From main.py:435-446
@app.post("/api/download")
async def start_download(request: DownloadRequest, background_tasks: BackgroundTasks):
download_id = str(uuid.uuid4())
downloads[download_id] = DownloadTask(
id=download_id,
url=request.url,
quality=request.quality,
download_type=request.download_type,
format_id=request.format_id or "",
)
background_tasks.add_task(download_video, download_id)
return {"download_id": download_id, "id": download_id}
Real-Time Progress Tracking
Download Statuses
Downloads progress through several states:
- Pending: Queued, waiting to start
- Downloading: Actively fetching video data
- Processing: Merging video/audio, converting format
- Completed: Ready to play
- Error: Failed (with retry option)
- Cancelled: Manually stopped by user
Progress Updates
The app polls the backend every 5 seconds for updates:
// From page.tsx:108-112
useEffect(() => {
fetchDownloads();
const timer = setInterval(() => fetchDownloads(true), 5000);
return () => clearInterval(timer);
}, [fetchDownloads]);
Progress Hook (Backend)
# From main.py:271-292
def progress_hook(d):
if d["status"] == "downloading":
total = d.get("total_bytes") or d.get("total_bytes_estimate") or 0
downloaded = d.get("downloaded_bytes", 0)
task.downloaded_bytes = downloaded
task.filesize = total
if total:
task.progress = round(downloaded / total * 100, 2)
raw_speed = d.get("speed")
task.speed = (
f"{raw_speed / 1024 / 1024:.1f} MB/s"
if raw_speed and raw_speed > 1_048_576
else f"{raw_speed / 1024:.0f} KB/s"
if raw_speed
else ""
)
eta = d.get("eta")
task.eta = f"{eta}s" if eta else ""
elif d["status"] == "finished":
task.status = DownloadStatus.PROCESSING
task.progress = 99
Download Manager UI
Filtering Downloads
The Downloads tab provides filter buttons:
// From DownloadManager.tsx:72-77
const filteredDownloads = downloads.filter(d => {
if (filter === 'active') return ['pending', 'downloading', 'processing'].includes(d.status);
if (filter === 'completed') return d.status === 'completed';
if (filter === 'error') return d.status === 'error';
return true;
});
Progress Visualization
- Circular progress for active downloads (MiniCircularProgress component)
- Linear progress bar with downloaded/total bytes
- Speed and ETA display during download
- Large circular detail view when selecting a download
Download Actions
Cancel Download
// From page.tsx:223-231
const handleCancelDownload = async (id: string) => {
await fetch(`${API_URL}/api/downloads/${id}`, { method: 'DELETE' });
toast.info('Descarga cancelada');
fetchDownloads(true);
}
Remove Download
// From page.tsx:233-242
const handleRemoveDownload = async (id: string) => {
await fetch(`${API_URL}/api/downloads/${id}/remove`, { method: 'DELETE' });
setDownloads((prev) => prev.filter((d) => d.id !== id));
toast.info('Descarga eliminada');
}
Retry Failed Download
// From page.tsx:244-251
const handleRetryDownload = async (id: string) => {
const download = downloads.find((d) => d.id === id);
if (download) {
await handleRemoveDownload(id);
handleStartDownload(download.url, download.quality || '720p');
}
}
Download to Disk
// From page.tsx:253-255
const handleDownloadFile = (id: string) => {
window.open(`${API_URL}/api/download/${id}/file`, '_blank');
}
yt-dlp Configuration
# From main.py:256-269
def get_ydl_opts(task: DownloadTask) -> dict:
if task.download_type == "audio":
if task.format_id:
format_selector = task.format_id
else:
format_selector = "bestaudio/best"
else:
if task.format_id:
# merge selected video stream with best audio
format_selector = f"{task.format_id}+bestaudio/best"
else:
height = int(task.quality.replace("p", "")) if task.quality else 720
format_selector = (
f"bv*[height<={height}]+ba/best[height<={height}]/best"
)
Post-Processing
# From main.py:302-307
"postprocessors": [
{"key": "FFmpegMerger"},
{"key": "FFmpegVideoConvertor", "preferedformat": "mp4"},
{"key": "EmbedThumbnail", "already_have_thumbnail": False},
]
This ensures:
- Video and audio streams are merged
- Output is always MP4 (browser-compatible)
- Thumbnails are embedded in the file
Configuration Options
Set default preferences in the Settings tab:
Default Quality
// From page.tsx:54-57
const [defaultQuality, setDefaultQuality] = useState<string>(() => {
if (typeof window !== 'undefined') return localStorage.getItem('yt-default-quality') || '720p';
return '720p';
});
Default Download Type
// From page.tsx:58-61
const [defaultDownloadType, setDefaultDownloadType] = useState<'video' | 'audio'>(() => {
if (typeof window !== 'undefined') return (localStorage.getItem('yt-default-type') as 'video' | 'audio') || 'video';
return 'video';
});
Tips and Best Practices
Choose the right quality: Higher quality means larger files. 720p is optimal for most devices.
Audio extraction: Select “Audio” mode in the Explorer to download music without video, saving significant disk space.
Downloads require FFmpeg to be installed on the backend server. The app will show an error if FFmpeg is missing (main.py:319).
Playlist Support
OfflineTube automatically detects playlists:
# From main.py:333-334
task.is_playlist = info.get("_type") == "playlist"
task.playlist_total = len(info.get("entries", [])) if task.is_playlist else 1
When downloading a playlist, each video is queued as a separate download task.
Error Recovery
The backend includes intelligent error recovery:
# From main.py:367-395
except Exception as e:
# yt-dlp sometimes raises cleanup errors even though
# the output .mp4 was merged successfully.
# Check if the file actually landed on disk before marking as error.
recovered = False
if not task.filename:
for f in DOWNLOAD_DIR.glob(f"*__{task.id}.*"):
if f.suffix in [".mp4", ".mkv", ".webm", ".m4a", ".mp3", ".opus"]:
task.filename = f.name
break
if task.filename and (DOWNLOAD_DIR / task.filename).exists():
# Recover from false error
task.status = DownloadStatus.COMPLETED
task.progress = 100
recovered = True
if not recovered:
task.status = DownloadStatus.ERROR
task.error_message = str(e)