package handler import ( "context" "fmt" "strconv" "uptodownBot/internal/domain" ) func (h *Handler) handlePlaylist(ctx context.Context, chatID int64, userID int64, url string, info *domain.MediaInfo) { state := &domain.PlaylistState{ Entries: info.PlaylistEntries, Page: 0, PerPage: 10, Selected: make(map[string]bool), } ss := &stepState{ URL: url, MediaInfo: info, PlaylistState: state, } h.setUserStepState(chatID, ss) h.sendMediaPreview(ctx, chatID, info) kb := playlistNavKeyboard(state, 0) h.sendWithKB(ctx, chatID, fmt.Sprintf("Playlist: %s (%d videos)\n\nSelect videos to download:", info.Title, info.PlaylistCount), kb) } func (h *Handler) getPlaylistState(chatID int64) *domain.PlaylistState { ss := h.getUserStepState(chatID) if ss == nil { return nil } return ss.PlaylistState } func (h *Handler) handlePlaylistPage(ctx context.Context, chatID int64, msgID int, userID int64, pageStr string) { state := h.getPlaylistState(chatID) if state == nil { return } page, err := strconv.Atoi(pageStr) if err != nil { return } totalPages := (len(state.Entries) + state.PerPage - 1) / state.PerPage if page < 0 || page >= totalPages { return } state.Page = page selectedCount := countSelected(state) kb := playlistNavKeyboard(state, selectedCount) ss := h.getUserStepState(chatID) title := "" if ss != nil { title = ss.MediaInfo.Title } h.editText(ctx, chatID, msgID, fmt.Sprintf("Playlist: %s (%d videos)", title, len(state.Entries)), &kb) } func (h *Handler) handlePlaylistEntry(ctx context.Context, chatID int64, msgID int, userID int64, entryID string) { state := h.getPlaylistState(chatID) if state == nil { return } state.Selected[entryID] = !state.Selected[entryID] selectedCount := countSelected(state) kb := playlistNavKeyboard(state, selectedCount) ss := h.getUserStepState(chatID) title := "" if ss != nil { title = ss.MediaInfo.Title } h.editText(ctx, chatID, msgID, fmt.Sprintf("Playlist: %s (%d videos)", title, len(state.Entries)), &kb) } func (h *Handler) handlePlaylistSelectAll(ctx context.Context, chatID int64, msgID int, userID int64) { state := h.getPlaylistState(chatID) if state == nil { return } allSelected := true for _, e := range state.Entries { if !state.Selected[e.ID] { allSelected = false break } } for _, e := range state.Entries { state.Selected[e.ID] = !allSelected } selectedCount := 0 if !allSelected { selectedCount = len(state.Entries) } kb := playlistNavKeyboard(state, selectedCount) ss := h.getUserStepState(chatID) title := "" if ss != nil { title = ss.MediaInfo.Title } h.editText(ctx, chatID, msgID, fmt.Sprintf("Playlist: %s (%d videos)", title, len(state.Entries)), &kb) } func (h *Handler) handlePlaylistConfirm(ctx context.Context, chatID int64, msgID int, userID int64) { state := h.getPlaylistState(chatID) if state == nil { return } var selected []domain.PlaylistEntry for _, e := range state.Entries { if state.Selected[e.ID] { selected = append(selected, e) } } if len(selected) == 0 { h.editText(ctx, chatID, msgID, "No videos selected.", nil) return } h.clearStepState(chatID) h.editText(ctx, chatID, msgID, fmt.Sprintf("Downloading %d videos sequentially...", len(selected)), nil) for i, entry := range selected { h.sendText(ctx, chatID, fmt.Sprintf("Downloading (%d/%d): %s", i+1, len(selected), entry.Title)) info, err := h.YtDlp.GetMediaInfo(entry.URL) if err != nil { h.sendText(ctx, chatID, fmt.Sprintf("Failed: %s", err.Error())) continue } prefs, prefErr := h.Repo.GetOrCreatePreferences(userID) if prefErr != nil { prefs = &domain.UserPreferences{ DefaultMediaType: domain.MediaTypeVideoAudio, DefaultAudioFormat: "best", DefaultVideoFormat: "best", } } formatID := prefs.DefaultVideoFormat if prefs.DefaultMediaType == domain.MediaTypeAudio { formatID = prefs.DefaultAudioFormat } format := &domain.Format{ID: formatID} for _, f := range info.Formats { if f.ID == formatID { format = &f break } } dlCtx, dlCancel := context.WithCancel(context.Background()) job := &domain.DownloadJob{ ID: h.generateJobID(), UserID: userID, ChatID: chatID, URL: entry.URL, MediaType: prefs.DefaultMediaType, SelectedFormat: format, Language: prefs.DefaultLanguage, Status: domain.StatusDownloading, Title: entry.Title, FileSize: format.Filesize, Done: make(chan struct{}), } h.setActiveJob(userID, job) h.setCancelFunc(userID, dlCancel) h.runDownload(dlCtx, job) // runDownload is synchronous — it blocks until the download completes. // The Done channel is closed inside runDownload when it exits. // No busy-polling needed. } h.sendText(ctx, chatID, "All playlist downloads completed.") } func countSelected(state *domain.PlaylistState) int { count := 0 for _, s := range state.Selected { if s { count++ } } return count }