Files
uptodownbot/internal/handler/playlist.go
db123 cd27b4d28d feat: add thumbnail preview, audio-only detection, /download command, tests, and bugfixes
- Add thumbnail preview with metadata (title, uploader, duration) before download
- Auto-detect audio-only sites (Spotify, SoundCloud, etc.) and skip type selection
- Add /download command for feature parity with UI button
- Fix yt-dlp defer bug: StatusFailed was overwritten to StatusCompleted
- Fix handleURLInput using editText with msgID=0 (now uses sendWithKB)
- Fix filename detection: use job ID prefix for reliable file finding
- Fix double answerCb between handleBackStep and handleDownloadPrompt
- Fix runDownload unused msgID parameter
- Fix context for download goroutine (use context.Background)
- Remove unused startWebhook h parameter
- Remove dead buildToggleKeyboard function
- Add formatBytes, formatDuration, formatLabelForDisplay tests
- Add IsAudioOnlyContent, MediaType.String tests
- Add repo integration tests (users, preferences, quotas, history, delete)
- Add cmd/bot tests (parseAllowedUsers, env helpers)
- Add edge case tests for progress parser
- Add zero/negative safety guard in formatDuration
- Add .gitea, TODO.md, opencode.json to gitignore
2026-06-25 15:07:31 +03:30

198 lines
4.9 KiB
Go

package handler
import (
"context"
"fmt"
"strconv"
"time"
"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,
DefaultQuality: "best",
DefaultContainer: "mp4",
}
}
format := &domain.Format{ID: prefs.DefaultQuality}
for _, f := range info.Formats {
if f.ID == prefs.DefaultQuality {
format = &f
break
}
}
job := &domain.DownloadJob{
ID: h.generateJobID(),
UserID: userID,
ChatID: chatID,
URL: entry.URL,
MediaType: prefs.DefaultMediaType,
SelectedFormat: format,
Container: prefs.DefaultContainer,
Language: prefs.DefaultLanguage,
Status: domain.StatusDownloading,
Title: entry.Title,
FileSize: format.Filesize,
}
h.setActiveJob(userID, job)
h.runDownload(context.Background(), job)
// Wait for active job to complete
for {
j := h.getActiveJob(userID)
if j == nil || j.Status == domain.StatusCompleted || j.Status == domain.StatusFailed || j.Status == domain.StatusCancelled {
break
}
time.Sleep(500 * time.Millisecond)
}
}
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
}