diff --git a/internal/dl/ytdlp.go b/internal/dl/ytdlp.go index 5553697..09084fb 100644 --- a/internal/dl/ytdlp.go +++ b/internal/dl/ytdlp.go @@ -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 diff --git a/internal/handler/download.go b/internal/handler/download.go index 1ee70af..5e6bc21 100644 --- a/internal/handler/download.go +++ b/internal/handler/download.go @@ -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.