fix: resolve download failures and duplicate messages, add fast playlist detection
All checks were successful
CI / build (push) Successful in 39s

- 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
This commit is contained in:
2026-06-26 12:39:11 +03:30
parent f5204b9554
commit 3f56ffbba9
2 changed files with 40 additions and 31 deletions

View File

@@ -194,6 +194,24 @@ func (c *Client) GetPlaylistInfo(url string) (*domain.MediaInfo, error) {
return cached, nil
}
isPlaylist, mi, err := c.DetectPlaylist(url)
if err != nil {
return nil, err
}
if !isPlaylist {
mi.IsPlaylist = false
mi.PlaylistCount = 0
mi.PlaylistEntries = nil
}
c.setCache(url, mi)
return mi, nil
}
// DetectPlaylist runs a fast flat yt-dlp check to determine if the URL is a playlist.
// It returns basic info without format details. For non-playlist URLs it returns false.
// Does NOT cache the result so a subsequent GetMediaInfo call fetches full format info.
func (c *Client) DetectPlaylist(url string) (bool, *domain.MediaInfo, error) {
args := []string{
"-J", "--flat-playlist", "--no-download",
"--no-warnings",
@@ -206,29 +224,29 @@ func (c *Client) GetPlaylistInfo(url string) (*domain.MediaInfo, error) {
args = append(args, "--proxy", c.proxyURL)
}
slog.Info("fetching playlist info", "url", url)
slog.Info("detecting playlist", "url", url)
out, err := c.run(args)
if err != nil {
return nil, fmt.Errorf("yt-dlp playlist info failed: %w", err)
return false, nil, fmt.Errorf("yt-dlp playlist detection failed: %w", err)
}
var info ytdlpInfo
if err := json.Unmarshal(out, &info); err != nil {
return nil, fmt.Errorf("parse yt-dlp playlist output: %w", err)
var rawInfo ytdlpInfo
if err := json.Unmarshal(out, &rawInfo); err != nil {
return false, nil, fmt.Errorf("parse yt-dlp output: %w", err)
}
mi := &domain.MediaInfo{
URL: url,
Title: info.Title,
Uploader: info.Uploader,
Thumbnail: info.Thumbnail,
IsPlaylist: true,
PlaylistCount: info.PlaylistCount,
PlaylistEntries: c.parsePlaylistEntries(info.Entries),
Title: rawInfo.Title,
Uploader: rawInfo.Uploader,
Thumbnail: rawInfo.Thumbnail,
Duration: rawInfo.Duration,
IsPlaylist: rawInfo.Playlist != "",
PlaylistCount: rawInfo.PlaylistCount,
PlaylistEntries: c.parsePlaylistEntries(rawInfo.Entries),
}
c.setCache(url, mi)
return mi, nil
return mi.IsPlaylist, mi, nil
}
func (c *Client) parsePlaylistEntries(entries []json.RawMessage) []domain.PlaylistEntry {
@@ -319,7 +337,6 @@ func (c *Client) StartDownload(job *domain.DownloadJob, downloadDir string) (<-c
// Language selection if specified
if job.Language != "" {
args = append(args, "--sub-langs", job.Language)
args = append(args, "--audio-multi-streams")
}
// Cookies

View File

@@ -82,6 +82,14 @@ func (h *Handler) handleURLInput(ctx context.Context, chatID int64, text string,
fetchingMsgID = fetchingMsg.ID
}
// Fast playlist detection first — avoids expensive full metadata fetch for large playlists.
isPlaylist, plInfo, detectErr := h.YtDlp.DetectPlaylist(url)
if detectErr == nil && isPlaylist {
h.editText(ctx, chatID, fetchingMsgID, fmt.Sprintf("Playlist detected: %s (%d videos)", plInfo.Title, plInfo.PlaylistCount), nil)
h.handlePlaylist(ctx, chatID, userID, url, plInfo)
return
}
info, err := h.YtDlp.GetMediaInfo(url)
if err != nil {
errMsg := err.Error()
@@ -121,12 +129,6 @@ func (h *Handler) handleURLInput(ctx context.Context, chatID int64, text string,
}
}
if info.IsPlaylist {
h.editText(ctx, chatID, fetchingMsgID, fmt.Sprintf("Playlist detected: %s (%d videos)", info.Title, info.PlaylistCount), nil)
h.handlePlaylist(ctx, chatID, userID, url, info)
return
}
if len(info.Formats) == 0 {
h.editText(ctx, chatID, fetchingMsgID, "No formats available for this URL.", nil)
return
@@ -146,16 +148,6 @@ func (h *Handler) handleURLInput(ctx context.Context, chatID int64, text string,
}
}
// Edit the fetching message to show the media title, then send a separate photo preview.
previewText := fmt.Sprintf("Title: %s", info.Title)
if info.Uploader != "" {
previewText += fmt.Sprintf("\nChannel: %s", info.Uploader)
}
if info.Duration > 0 {
previewText += fmt.Sprintf("\nDuration: %s", formatDuration(info.Duration))
}
h.editText(ctx, chatID, fetchingMsgID, previewText, nil)
h.sendMediaPreview(ctx, chatID, info)
ss := &stepState{
@@ -181,7 +173,7 @@ func (h *Handler) handleURLInput(ctx context.Context, chatID int64, text string,
}
}
kb := formatKeyboard("format_", labels, ss.FormatIDs)
h.sendWithKB(ctx, chatID, "Select format:", kb)
h.editText(ctx, chatID, fetchingMsgID, "Select format:", &kb)
}
// handleFormatSelection processes a format pick from the unified list.