fix: resolve download failures and duplicate messages, add fast playlist detection
All checks were successful
CI / build (push) Successful in 39s
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:
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user