refactor: remove all playlist support, fix message ordering
All checks were successful
CI / build (push) Successful in 54s

- Remove playlist handler, keyboard, and all related callback routing
- Strip playlist parameter from YouTube Music radio URLs, reject pure playlists
- Send media preview before format selection prompt (correct ordering)
- Remove DetectPlaylist/GetPlaylistInfo methods (no longer needed)
- Remove IsPlaylist, PlaylistCount, PlaylistEntries from MediaInfo
- Remove PlaylistState struct, keep PlaylistEntry for search results
- Update README to reflect removal of playlist feature
This commit is contained in:
2026-06-26 12:56:10 +03:30
parent 3f56ffbba9
commit d0291d1e9e
7 changed files with 61 additions and 491 deletions

View File

@@ -9,6 +9,7 @@ import (
"net/http"
"os"
"path/filepath"
"regexp"
"strings"
"time"
@@ -20,18 +21,17 @@ import (
)
type stepState struct {
URL string
MediaInfo *domain.MediaInfo
Step int
MainFormatID string
MainFormat *domain.Format
SecondaryID string
SecondaryFmt *domain.Format
Language string
FormatIDs []string
PlaylistState *domain.PlaylistState
SearchState *domain.SearchState
CreatedAt time.Time
URL string
MediaInfo *domain.MediaInfo
Step int
MainFormatID string
MainFormat *domain.Format
SecondaryID string
SecondaryFmt *domain.Format
Language string
FormatIDs []string
SearchState *domain.SearchState
CreatedAt time.Time
}
const stepStateTTL = 30 * time.Minute
@@ -59,6 +59,25 @@ func (h *Handler) clearStepState(chatID int64) {
delete(h.stepStates, chatID)
}
// stripPlaylistParam removes the list parameter from YouTube/Music URLs.
// Playlists are not supported; this converts a video-with-playlist URL (e.g. a
// YouTube Music radio) to a plain video URL. Pure playlist URLs are rejected.
func stripPlaylistParam(rawURL string) (string, bool) {
// Reject pure playlist URLs (https://youtube.com/playlist?list=...)
if strings.Contains(rawURL, "/playlist") || strings.Contains(rawURL, "youtube.com/pL") {
return "", false
}
// Strip the list query parameter from watch URLs
url := rawURL
if strings.Contains(url, "list=") {
// Remove &list=... or ?list=...
url = regexp.MustCompile(`[?&]list=[^&]+`).ReplaceAllString(url, "")
// Clean up trailing ? or & if the param was the only one
url = strings.TrimRight(url, "?&")
}
return url, true
}
// handleURLInput is the entry point when a user sends a URL or types in Download mode.
func (h *Handler) handleURLInput(ctx context.Context, chatID int64, text string, userID int64) {
url, ok := domain.ValidateURL(text)
@@ -67,26 +86,15 @@ func (h *Handler) handleURLInput(ctx context.Context, chatID int64, text string,
return
}
if job := h.getActiveJob(userID); job != nil {
h.sendText(ctx, chatID, "You already have an active download. Cancel it first.")
// Strip playlist parameter and reject pure playlists.
url, ok = stripPlaylistParam(url)
if !ok {
h.sendText(ctx, chatID, "Playlists are not supported. Send a single video/audio URL.")
return
}
// Show a temporary status message that we will edit with results later.
fetchingMsg, _ := h.Bot.SendMessage(ctx, &tgbot.SendMessageParams{
ChatID: chatID,
Text: "Fetching metadata...",
})
fetchingMsgID := 0
if fetchingMsg != nil {
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)
if job := h.getActiveJob(userID); job != nil {
h.sendText(ctx, chatID, "You already have an active download. Cancel it first.")
return
}
@@ -104,33 +112,33 @@ func (h *Handler) handleURLInput(ctx context.Context, chatID int64, text string,
{{Text: "Cancel", CallbackData: "pending_cancel"}},
},
}
h.editText(ctx, chatID, fetchingMsgID, "This content is DRM protected and no YouTube alternative was found automatically. Try searching by name:", &kb)
h.sendWithKB(ctx, chatID, "This content is DRM protected and no YouTube alternative was found automatically. Try searching by name:", kb)
return
}
h.editText(ctx, chatID, fetchingMsgID, fmt.Sprintf("This content is DRM protected. Found on YouTube: %s", ytTitle), nil)
h.sendText(ctx, chatID, fmt.Sprintf("This content is DRM protected. Found on YouTube: %s", ytTitle))
info, err = h.YtDlp.GetMediaInfo(youtubeURL)
if err != nil {
h.editText(ctx, chatID, fetchingMsgID, fmt.Sprintf("YouTube fallback failed: %s", err.Error()), nil)
h.sendText(ctx, chatID, fmt.Sprintf("YouTube fallback failed: %s", err.Error()))
return
}
url = youtubeURL
} else {
switch {
case strings.Contains(errMsg, "Unsupported URL") || strings.Contains(errMsg, "is not a valid URL"):
h.editText(ctx, chatID, fetchingMsgID, "This URL is not supported.", nil)
h.sendText(ctx, chatID, "This URL is not supported.")
case strings.Contains(errMsg, "HTTP Error 403") || strings.Contains(errMsg, "cookies"):
h.editText(ctx, chatID, fetchingMsgID, "This content requires authentication (cookies).", nil)
h.sendText(ctx, chatID, "This content requires authentication (cookies).")
case strings.Contains(errMsg, "live"):
h.editText(ctx, chatID, fetchingMsgID, "Cannot download live streams.", nil)
h.sendText(ctx, chatID, "Cannot download live streams.")
default:
h.editText(ctx, chatID, fetchingMsgID, fmt.Sprintf("Error: %s", errMsg), nil)
h.sendText(ctx, chatID, fmt.Sprintf("Error: %s", errMsg))
}
return
}
}
if len(info.Formats) == 0 {
h.editText(ctx, chatID, fetchingMsgID, "No formats available for this URL.", nil)
h.sendText(ctx, chatID, "No formats available for this URL.")
return
}
@@ -141,9 +149,9 @@ func (h *Handler) handleURLInput(ctx context.Context, chatID int64, text string,
if (q.DailyLimitMB > 0 && q.DailyUsedMB >= q.DailyLimitMB) ||
(q.WeeklyLimitMB > 0 && q.WeeklyUsedMB >= q.WeeklyLimitMB) ||
(q.MonthlyLimitMB > 0 && q.MonthlyUsedMB >= q.MonthlyLimitMB) {
h.editText(ctx, chatID, fetchingMsgID, fmt.Sprintf(
h.sendText(ctx, chatID, fmt.Sprintf(
"Quota exceeded. Daily: %d/%d MB, Weekly: %d/%d MB, Monthly: %d/%d MB.",
q.DailyUsedMB, q.DailyLimitMB, q.WeeklyUsedMB, q.WeeklyLimitMB, q.MonthlyUsedMB, q.MonthlyLimitMB), nil)
q.DailyUsedMB, q.DailyLimitMB, q.WeeklyUsedMB, q.WeeklyLimitMB, q.MonthlyUsedMB, q.MonthlyLimitMB))
return
}
}
@@ -173,7 +181,7 @@ func (h *Handler) handleURLInput(ctx context.Context, chatID int64, text string,
}
}
kb := formatKeyboard("format_", labels, ss.FormatIDs)
h.editText(ctx, chatID, fetchingMsgID, "Select format:", &kb)
h.sendWithKB(ctx, chatID, "Select format:", kb)
}
// handleFormatSelection processes a format pick from the unified list.