refactor: remove all playlist support, fix message ordering
All checks were successful
CI / build (push) Successful in 54s
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:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user