All checks were successful
CI / build (push) Successful in 52s
- Add --continue and --max-filesize to yt-dlp for download resilience - Route thumbnail downloads through HTTP_PROXY - Show per-video progress (i/N) with cancel for playlist downloads - Run playlist asynchronously so cancel callbacks can be processed - Guard handlePlaylistConfirm against concurrent active jobs - Close Done channel in playlist entry lifecycle - Add stepState TTL (30 min) to prevent stale states - Wrap webhook server for graceful shutdown on SIGTERM - Refactor formatBytes to loop-based implementation - Show first line of stderr in user-facing error messages - Fix format pointer reuse in playlist loop - Add CreatedAt field to stepState for TTL tracking - Add MaxFileSize and ProgressPrefix fields to DownloadJob
138 lines
4.0 KiB
Go
138 lines
4.0 KiB
Go
package handler
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/go-telegram/bot/models"
|
|
|
|
"uptodownBot/internal/domain"
|
|
)
|
|
|
|
func (h *Handler) handleSearchCommand(ctx context.Context, msg *models.Message, userID int64) {
|
|
h.promptForInput(ctx, msg.Chat.ID, 0, userID, PendingSearch,
|
|
"Enter a YouTube search query:", map[string]string{"source": "youtube"})
|
|
}
|
|
|
|
func (h *Handler) handleMusicSearchCommand(ctx context.Context, msg *models.Message, userID int64) {
|
|
h.promptForInput(ctx, msg.Chat.ID, 0, userID, PendingSearch,
|
|
"Enter a YouTube Music search query:", map[string]string{"source": "ytmusic"})
|
|
}
|
|
|
|
func (h *Handler) handleSearchQuery(ctx context.Context, chatID int64, text string, userID int64, source string) {
|
|
results, err := h.YtDlp.Search(text, source, 10)
|
|
if err != nil {
|
|
h.sendText(ctx, chatID, fmt.Sprintf("Search failed: %s", err.Error()))
|
|
return
|
|
}
|
|
if len(results) == 0 {
|
|
h.sendText(ctx, chatID, "No results found.")
|
|
return
|
|
}
|
|
|
|
state := &domain.SearchState{
|
|
Query: text,
|
|
Entries: results,
|
|
Page: 0,
|
|
PerPage: 5,
|
|
Source: source,
|
|
}
|
|
ss := &stepState{SearchState: state, CreatedAt: time.Now()}
|
|
h.setUserStepState(chatID, ss)
|
|
|
|
kb := searchResultsKeyboard(state)
|
|
h.sendWithKB(ctx, chatID, fmt.Sprintf("Search results for '%s':", text), kb)
|
|
}
|
|
|
|
func (h *Handler) handleSearchCallback(ctx context.Context, chatID int64, msgID int, userID int64, data string) {
|
|
switch {
|
|
case data == "refresh":
|
|
ss := h.getUserStepState(chatID)
|
|
if ss == nil || ss.SearchState == nil {
|
|
return
|
|
}
|
|
h.handleSearchQuery(ctx, chatID, ss.SearchState.Query, userID, ss.SearchState.Source)
|
|
case strings.HasPrefix(data, "page_"):
|
|
h.handleSearchPage(ctx, chatID, msgID, userID, data[5:])
|
|
case strings.HasPrefix(data, "select_"):
|
|
h.handleSearchSelect(ctx, chatID, msgID, userID, data[7:])
|
|
}
|
|
}
|
|
|
|
func (h *Handler) handleSearchPage(ctx context.Context, chatID int64, msgID int, userID int64, pageStr string) {
|
|
ss := h.getUserStepState(chatID)
|
|
if ss == nil || ss.SearchState == nil {
|
|
return
|
|
}
|
|
page, err := strconv.Atoi(pageStr)
|
|
if err != nil {
|
|
return
|
|
}
|
|
totalPages := (len(ss.SearchState.Entries) + ss.SearchState.PerPage - 1) / ss.SearchState.PerPage
|
|
if page < 0 || page >= totalPages {
|
|
return
|
|
}
|
|
ss.SearchState.Page = page
|
|
h.setUserStepState(chatID, ss)
|
|
|
|
kb := searchResultsKeyboard(ss.SearchState)
|
|
h.editText(ctx, chatID, msgID, fmt.Sprintf("Search results for '%s':", ss.SearchState.Query), &kb)
|
|
}
|
|
|
|
func (h *Handler) handleSearchSelect(ctx context.Context, chatID int64, msgID int, userID int64, entryURL string) {
|
|
h.clearStepState(chatID)
|
|
h.editText(ctx, chatID, msgID, "Fetching media info...", nil)
|
|
h.handleURLInput(ctx, chatID, entryURL, userID)
|
|
}
|
|
|
|
func searchResultsKeyboard(state *domain.SearchState) models.InlineKeyboardMarkup {
|
|
totalPages := (len(state.Entries) + state.PerPage - 1) / state.PerPage
|
|
if totalPages < 1 {
|
|
totalPages = 1
|
|
}
|
|
|
|
rows := [][]models.InlineKeyboardButton{}
|
|
start := state.Page * state.PerPage
|
|
end := start + state.PerPage
|
|
if end > len(state.Entries) {
|
|
end = len(state.Entries)
|
|
}
|
|
|
|
for i := start; i < end; i++ {
|
|
e := state.Entries[i]
|
|
title := e.Title
|
|
if len(title) > 40 {
|
|
title = title[:37] + "..."
|
|
}
|
|
label := title
|
|
if e.Duration > 0 {
|
|
label += fmt.Sprintf(" (%s)", formatDuration(e.Duration))
|
|
}
|
|
rows = append(rows, []models.InlineKeyboardButton{
|
|
{Text: label, CallbackData: "search_select_" + e.URL},
|
|
})
|
|
}
|
|
|
|
navRow := []models.InlineKeyboardButton{}
|
|
navRow = append(navRow, models.InlineKeyboardButton{
|
|
Text: "<", CallbackData: fmt.Sprintf("search_page_%d", state.Page-1),
|
|
})
|
|
navRow = append(navRow, models.InlineKeyboardButton{
|
|
Text: fmt.Sprintf("Page %d/%d", state.Page+1, totalPages), CallbackData: "noop",
|
|
})
|
|
navRow = append(navRow, models.InlineKeyboardButton{
|
|
Text: ">", CallbackData: fmt.Sprintf("search_page_%d", state.Page+1),
|
|
})
|
|
rows = append(rows, navRow)
|
|
|
|
rows = append(rows, []models.InlineKeyboardButton{
|
|
{Text: "New Search", CallbackData: "search_refresh"},
|
|
{Text: "Cancel", CallbackData: "cancel_dl"},
|
|
})
|
|
|
|
return models.InlineKeyboardMarkup{InlineKeyboard: rows}
|
|
}
|