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
|
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{
|
args := []string{
|
||||||
"-J", "--flat-playlist", "--no-download",
|
"-J", "--flat-playlist", "--no-download",
|
||||||
"--no-warnings",
|
"--no-warnings",
|
||||||
@@ -206,29 +224,29 @@ func (c *Client) GetPlaylistInfo(url string) (*domain.MediaInfo, error) {
|
|||||||
args = append(args, "--proxy", c.proxyURL)
|
args = append(args, "--proxy", c.proxyURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
slog.Info("fetching playlist info", "url", url)
|
slog.Info("detecting playlist", "url", url)
|
||||||
out, err := c.run(args)
|
out, err := c.run(args)
|
||||||
if err != nil {
|
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
|
var rawInfo ytdlpInfo
|
||||||
if err := json.Unmarshal(out, &info); err != nil {
|
if err := json.Unmarshal(out, &rawInfo); err != nil {
|
||||||
return nil, fmt.Errorf("parse yt-dlp playlist output: %w", err)
|
return false, nil, fmt.Errorf("parse yt-dlp output: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
mi := &domain.MediaInfo{
|
mi := &domain.MediaInfo{
|
||||||
URL: url,
|
URL: url,
|
||||||
Title: info.Title,
|
Title: rawInfo.Title,
|
||||||
Uploader: info.Uploader,
|
Uploader: rawInfo.Uploader,
|
||||||
Thumbnail: info.Thumbnail,
|
Thumbnail: rawInfo.Thumbnail,
|
||||||
IsPlaylist: true,
|
Duration: rawInfo.Duration,
|
||||||
PlaylistCount: info.PlaylistCount,
|
IsPlaylist: rawInfo.Playlist != "",
|
||||||
PlaylistEntries: c.parsePlaylistEntries(info.Entries),
|
PlaylistCount: rawInfo.PlaylistCount,
|
||||||
|
PlaylistEntries: c.parsePlaylistEntries(rawInfo.Entries),
|
||||||
}
|
}
|
||||||
|
|
||||||
c.setCache(url, mi)
|
return mi.IsPlaylist, mi, nil
|
||||||
return mi, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) parsePlaylistEntries(entries []json.RawMessage) []domain.PlaylistEntry {
|
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
|
// Language selection if specified
|
||||||
if job.Language != "" {
|
if job.Language != "" {
|
||||||
args = append(args, "--sub-langs", job.Language)
|
args = append(args, "--sub-langs", job.Language)
|
||||||
args = append(args, "--audio-multi-streams")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cookies
|
// Cookies
|
||||||
|
|||||||
@@ -82,6 +82,14 @@ func (h *Handler) handleURLInput(ctx context.Context, chatID int64, text string,
|
|||||||
fetchingMsgID = fetchingMsg.ID
|
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)
|
info, err := h.YtDlp.GetMediaInfo(url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errMsg := err.Error()
|
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 {
|
if len(info.Formats) == 0 {
|
||||||
h.editText(ctx, chatID, fetchingMsgID, "No formats available for this URL.", nil)
|
h.editText(ctx, chatID, fetchingMsgID, "No formats available for this URL.", nil)
|
||||||
return
|
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)
|
h.sendMediaPreview(ctx, chatID, info)
|
||||||
|
|
||||||
ss := &stepState{
|
ss := &stepState{
|
||||||
@@ -181,7 +173,7 @@ func (h *Handler) handleURLInput(ctx context.Context, chatID int64, text string,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
kb := formatKeyboard("format_", labels, ss.FormatIDs)
|
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.
|
// handleFormatSelection processes a format pick from the unified list.
|
||||||
|
|||||||
Reference in New Issue
Block a user