- Remove --audio-multi-streams flag (not supported by Alpine yt-dlp 2026.03.17)
- Reuse the fetching message as the format selection message to avoid duplicate text
- Add DetectPlaylist method for fast flat playlist detection before expensive GetMediaInfo
- This avoids stalls on YouTube Music radio URLs with large playlists
- Prioritize IsAudioOnly check in formatLabelForDisplay so bitrate
is shown instead of yt-dlp's generic 'audio only' resolution
- Only collect languages from formats with audio/video to prevent
subtitle-only tracks from triggering the language selection prompt
- Show 'Fetching metadata...' message before GetMediaInfo and edit
with results, improving UX for URL input
- Add test coverage for audio-only format labels
- Add --continue and --max-filesize to yt-dlp for download resilience
- Route thumbnail downloads through HTTP_PROXY
- Show per-video progress (i/N) with cancel for playlist downloads
- Run playlist asynchronously so cancel callbacks can be processed
- Guard handlePlaylistConfirm against concurrent active jobs
- Close Done channel in playlist entry lifecycle
- Add stepState TTL (30 min) to prevent stale states
- Wrap webhook server for graceful shutdown on SIGTERM
- Refactor formatBytes to loop-based implementation
- Show first line of stderr in user-facing error messages
- Fix format pointer reuse in playlist loop
- Add CreatedAt field to stepState for TTL tracking
- Add MaxFileSize and ProgressPrefix fields to DownloadJob
yt-dlp suppresses progress output by default when not in a TTY.
The --progress flag overrides this. Without it, readProgress sees
only 4 stderr lines (no [download] progress) and reports 0 matches.
This was the root cause of progress always showing 0.0%.
--progress-template output is suppressed in non-TTY subprocesses
(yt-dlp issue #13649). The default [download] progress format
works reliably in pipes. Changes:
- Remove --progress-template from yt-dlp args entirely
- Keep --newline for newline-delimited output in pipe
- Fix progressRE regex to handle HLS format: ~ with leading
spaces before size (e.g. '~ 1.00KiB')
- Fix speed parsing in [download] fallback to handle multiple
leading spaces (e.g. 'at 976.20B/s')
- Fix ETA parsing to strip (frag n/m) suffix
- Filter Unknown/N/A speed/eta in [download] fallback
- Add test cases for real HLS fragment output format
- Fix progress template: remove spurious stderr: prefix and switch to
_percent_str/_speed_str/_eta_str keys (progress.percent outputs NA
for HLS streams with unknown total size)
- Update progress regex to match new template format:
DLPROG| 45.2%| 1.05MiB/s|00:45
- Fix context cancellation bug: clearActiveJob was cancelling dlCtx
before handleDownloadCompletion could send the document
- Update progress test cases to match new template output format
- Remove container picker UI, keyboard, settings, and ContainerOptions
entirely — yt-dlp handles container format automatically
- Remove Container field from DownloadJob struct and related references
- Extract buildFormatList helper to deduplicate format list building
between handleURLInput and handleBackStep
- Break runDownload into trackDownloadProgress + handleDownloadCompletion
- Add HTTP timeout (30s) to downloadFile helper
- Log applyQuota and AddHistory errors instead of discarding
- Log nil stepState warnings in all handler entry points
- Include job.StderrLog in download failure messages
- Add tests for buildFormatList, findFormatByID, determineMediaType,
and buildFormatString
- Update README with search, DRM fallback, and quota feature docs
- Remove unused strings import from keyboard.go
- Split single sync.Mutex into per-map mutexes (activeJobs, stepStates,
rateLimiter, pendingInput, cancelFuncs) to reduce contention
- Add download semaphore (channel-buffered, 3 concurrent) as worker pool
- Add cancelFuncs map with context cancellation wired into download
goroutines for clean shutdown
- Replace playlist busy-poll (500ms sleep loop) with synchronous
runDownload + Done channel for completion tracking
- Use full UUID for job IDs instead of 8-char prefix
- Download thumbnail URL to temp file before sending (InputFileUpload)
- Change MAX_FILE_SIZE_MB default from 2000 to 50
- Clean up cancel funcs after normal download completion
- Add in-memory LRU cache (200 entries, 30 min TTL) to yt-dlp Client for
GetMediaInfo and GetPlaylistInfo results, avoiding redundant yt-dlp calls
for the same URL within a session
- Add YouTube (/search) and YouTube Music (/music) search with paginated
results (5 per page), user selects a result to start the download flow
- Add SearchState type, new search.go handler, search results keyboard with
< > navigation and New Search button
- Add Search button to main menu keyboard
- Update help text with new commands
- Fix handleBackStep to redirect to download prompt when step state is nil
(previously silent no-op after container selection cleared the state)
- Improve formatLabel for audio-only entries without bitrate: use ID + extension
instead of generic 'audio', fixing duplicate entries in format list
- Add yt-dlp --progress-template for reliable JSON progress parsing across all
sites (fixes 0.0% progress on sites like xnxx.com where [download] lines
are absent); keep regex parser as fallback for older yt-dlp versions
- Add file size pre-check in handleContainerSelection before download starts
(previously checked only after download completed, wasting bandwidth)
- Clear stale active jobs and step state on /start for a clean slate
Pre-check now skips periods where limit is 0, allowing users to have
unlimited quota by setting daily_limit_mb/weekly_limit_mb/monthly_limit_mb
to 0 in the database.
Changed pre-check to only reject when used >= limit (already exhausted),
not when the max format size alone would exceed the limit. The actual
quota is applied at 50% progress using the selected format's size.
yt-dlp is a Python script. When stderr is a pipe (not a TTY), Python
buffers output, causing progress updates to arrive in large chunks or
only at the end. Setting PYTHONUNBUFFERED=1 forces line-buffered output,
giving real-time progress.
- Always show the first nonzero progress update so users don't see 0% for
extended periods on slow downloads or fragment-based downloads
- Keep 5% throttle for subsequent updates to avoid excessive API calls
- Add readProgress summary logging (lines, matches, stderr length)
- Fix progressKeyboard not being updated until progress >= 5%
Add StderrLog field to DownloadJob and collect all stderr lines during
download. Log the captured stderr output when a download fails to help
diagnose exit code 2 errors (geo-restrictions, auth, etc.).
When SearchYoutube with the Spotify URL fails for individual tracks,
offer a 'Search on YouTube' button that lets the user enter a search
query (artist/song name) to find the content on YouTube.
- When content is audio-only (IsAudioOnlyContent), 'best' format is marked as
IsAudioOnly so determineMediaType returns MediaTypeAudio
- Previously 'best' was hardcoded with HasVideo:true, causing video containers
(mp4/mkv) to be shown instead of audio containers (mp3/m4a/opus)
- Rename 'Format' button to 'Quality' in settings (audio_quality/video_quality)
- Add DefaultAudioContainer and DefaultVideoContainer to UserPreferences
- Container settings now has Audio/Video sub-pickers (mp3/m4a/opus vs mp4/mkv/webm)
- Add migration 003 for new container columns
- Update repo layer and playlist flow for new fields
- Update back routing: back_quality, back_container
- Replace Type/Quality settings with single Format button -> Audio/Video sub-pickers
- Each sub-picker shows format options (bitrates for audio, resolutions for video)
- Replace DefaultQuality with DefaultAudioFormat/DefaultVideoFormat in UserPreferences
- Add DB migration 002 for new preference columns
- Update repo layer SQL queries for new schema
- Update playlist flow to use new preference fields
- Fix missing deleteaccount and delaccount_ callback routing
- Add back_format routing for format picker navigation
- Remove --no-progress from yt-dlp args that suppressed all progress output
- Add --merge-output-format when FormatString is set (container was ignored for combined formats)
- Improve formatLabelForDisplay for audio-only formats missing bitrate (show ID+extension)
- Detect DRM errors (Spotify) and fall back to YouTube Music search via ytsearch1:
- Replace type/quality selection flow with unified format list (video+audio combined)
- Add independent audio/video format picking: video-only formats prompt for audio track,
audio-only formats offer optional video addition (skip when content is audio-only)
- Fix formatKeyboard to use format IDs as callback data instead of display labels
- Remove dead mediaTypeKeyboard function
- Fix double-close panic in readProgress (caller is sole owner of updates channel)
- Add FormatString field to DownloadJob for composite format specs (e.g. '137+140')
- Change quota calculation from sum of all format sizes to max single format size
- Fix 'Delete all data' button to use danger style with proper text
- Clean up routePrefixedCallback: remove type_/quality_, add format_/secondary_ routes
- Fix back navigation to rebuild format IDs list
Telegram bot for downloading media from any site using yt-dlp.
Supports audio/video/both, quality selection, language picker,
container format selection, playlist pagination, progress updates,
per-user quotas, user preferences, download history, cancel at any step,
polling and webhook modes, proxy support, and SQLite persistence.