Telegram bot for downloading media from any site using yt-dlp. Supports audio/video/both, quality selection, language picker, container format selection, playlist pagination, progress updates, per-user quotas, user preferences, download history, cancel at any step, polling and webhook modes, proxy support, and SQLite persistence.
257 lines
7.4 KiB
Go
257 lines
7.4 KiB
Go
package handler
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/go-telegram/bot/models"
|
|
|
|
"uptodownBot/internal/domain"
|
|
)
|
|
|
|
func mainKeyboard() models.InlineKeyboardMarkup {
|
|
return models.InlineKeyboardMarkup{
|
|
InlineKeyboard: [][]models.InlineKeyboardButton{
|
|
{
|
|
{Text: "Download", CallbackData: "download"},
|
|
{Text: "Settings", CallbackData: "settings"},
|
|
},
|
|
{
|
|
{Text: "History", CallbackData: "history"},
|
|
{Text: "Help", CallbackData: "help"},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func backKeyboard() models.InlineKeyboardMarkup {
|
|
return models.InlineKeyboardMarkup{
|
|
InlineKeyboard: [][]models.InlineKeyboardButton{
|
|
{{Text: "Back to Menu", CallbackData: "back_menu"}},
|
|
},
|
|
}
|
|
}
|
|
|
|
func settingsBackKeyboard() models.InlineKeyboardMarkup {
|
|
return models.InlineKeyboardMarkup{
|
|
InlineKeyboard: [][]models.InlineKeyboardButton{
|
|
{{Text: "Back to Settings", CallbackData: "back_settings"}},
|
|
},
|
|
}
|
|
}
|
|
|
|
// mediaTypeKeyboard builds the three-button row for media type selection + Cancel.
|
|
func mediaTypeKeyboard() models.InlineKeyboardMarkup {
|
|
return models.InlineKeyboardMarkup{
|
|
InlineKeyboard: [][]models.InlineKeyboardButton{
|
|
{
|
|
{Text: "Audio Only", CallbackData: "type_audio"},
|
|
{Text: "Video Only", CallbackData: "type_video"},
|
|
{Text: "Video+Audio", CallbackData: "type_both"},
|
|
},
|
|
{{Text: "Cancel", CallbackData: "cancel_dl"}},
|
|
},
|
|
}
|
|
}
|
|
|
|
// formatKeyboard builds a keyboard from a slice of format labels.
|
|
func formatKeyboard(prefix string, labels []string) models.InlineKeyboardMarkup {
|
|
rows := [][]models.InlineKeyboardButton{}
|
|
row := []models.InlineKeyboardButton{}
|
|
for i, label := range labels {
|
|
data := prefix + label
|
|
row = append(row, models.InlineKeyboardButton{Text: label, CallbackData: data})
|
|
if len(row) == 2 || i == len(labels)-1 {
|
|
rows = append(rows, row)
|
|
row = nil
|
|
}
|
|
}
|
|
rows = append(rows, []models.InlineKeyboardButton{
|
|
{Text: "Back", CallbackData: "back_step"},
|
|
{Text: "Cancel", CallbackData: "cancel_dl"},
|
|
})
|
|
return models.InlineKeyboardMarkup{InlineKeyboard: rows}
|
|
}
|
|
|
|
// languageKeyboard builds a keyboard from available languages.
|
|
func languageKeyboard(languages []string) models.InlineKeyboardMarkup {
|
|
rows := [][]models.InlineKeyboardButton{}
|
|
row := []models.InlineKeyboardButton{}
|
|
for i, lang := range languages {
|
|
data := "lang_" + lang
|
|
row = append(row, models.InlineKeyboardButton{Text: lang, CallbackData: data})
|
|
if len(row) == 2 || i == len(languages)-1 {
|
|
rows = append(rows, row)
|
|
row = nil
|
|
}
|
|
}
|
|
rows = append(rows, []models.InlineKeyboardButton{
|
|
{Text: "Back", CallbackData: "back_step"},
|
|
{Text: "Cancel", CallbackData: "cancel_dl"},
|
|
})
|
|
return models.InlineKeyboardMarkup{InlineKeyboard: rows}
|
|
}
|
|
|
|
// containerKeyboard builds keyboard for container format selection.
|
|
func containerKeyboard(containers []string) models.InlineKeyboardMarkup {
|
|
rows := [][]models.InlineKeyboardButton{}
|
|
row := []models.InlineKeyboardButton{}
|
|
for i, c := range containers {
|
|
data := "container_" + c
|
|
row = append(row, models.InlineKeyboardButton{Text: strings.ToUpper(c), CallbackData: data})
|
|
if len(row) == 3 || i == len(containers)-1 {
|
|
rows = append(rows, row)
|
|
row = nil
|
|
}
|
|
}
|
|
rows = append(rows, []models.InlineKeyboardButton{
|
|
{Text: "Back", CallbackData: "back_step"},
|
|
{Text: "Cancel", CallbackData: "cancel_dl"},
|
|
})
|
|
return models.InlineKeyboardMarkup{InlineKeyboard: rows}
|
|
}
|
|
|
|
// progressKeyboard builds the progress display with a dummy button and cancel.
|
|
func progressKeyboard(pct float64, speed, eta string) models.InlineKeyboardMarkup {
|
|
progressText := fmt.Sprintf("%.1f%%", pct)
|
|
if speed != "" {
|
|
progressText += fmt.Sprintf(" | %s/s", speed)
|
|
}
|
|
if eta != "" {
|
|
progressText += fmt.Sprintf(" | ETA %s", eta)
|
|
}
|
|
return models.InlineKeyboardMarkup{
|
|
InlineKeyboard: [][]models.InlineKeyboardButton{
|
|
{{Text: progressText, CallbackData: "noop"}},
|
|
{{Text: "Cancel", CallbackData: "cancel_dl"}},
|
|
},
|
|
}
|
|
}
|
|
|
|
// buildToggleKeyboard creates an on/off picker for a boolean setting.
|
|
// Matches worktimeBot's pattern with "> " prefix for the current selection.
|
|
func buildToggleKeyboard(userID int64, setting, label string, st *domain.UserPreferences) (string, models.InlineKeyboardMarkup) {
|
|
current := false
|
|
switch setting {
|
|
case "quality":
|
|
current = st.DefaultQuality == "best"
|
|
case "container":
|
|
current = st.DefaultContainer == "mp4"
|
|
}
|
|
options := []struct {
|
|
label string
|
|
data string
|
|
value bool
|
|
}{
|
|
{"Enabled", setting + "_enable", true},
|
|
{"Disabled", setting + "_disable", false},
|
|
}
|
|
rows := [][]models.InlineKeyboardButton{}
|
|
for _, opt := range options {
|
|
l := opt.label
|
|
if opt.value == current {
|
|
l = "> " + l
|
|
}
|
|
rows = append(rows, []models.InlineKeyboardButton{
|
|
{Text: l, CallbackData: opt.data},
|
|
})
|
|
}
|
|
rows = append(rows, []models.InlineKeyboardButton{
|
|
{Text: "Back to Settings", CallbackData: "back_settings"},
|
|
})
|
|
return label + ":", models.InlineKeyboardMarkup{InlineKeyboard: rows}
|
|
}
|
|
|
|
// playlistNavKeyboard builds the paginated navigation for playlist selection.
|
|
func playlistNavKeyboard(state *domain.PlaylistState, selectedCount int) models.InlineKeyboardMarkup {
|
|
totalPages := (len(state.Entries) + state.PerPage - 1) / state.PerPage
|
|
if totalPages < 1 {
|
|
totalPages = 1
|
|
}
|
|
|
|
// Build entry rows (2 per row)
|
|
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 += 2 {
|
|
row := []models.InlineKeyboardButton{}
|
|
// First entry
|
|
e := state.Entries[i]
|
|
prefix := " "
|
|
if state.Selected[e.ID] {
|
|
prefix = "> "
|
|
}
|
|
// Truncate title for button
|
|
title := e.Title
|
|
if len(title) > 30 {
|
|
title = title[:27] + "..."
|
|
}
|
|
row = append(row, models.InlineKeyboardButton{
|
|
Text: prefix + title, CallbackData: "pl_entry_" + e.ID,
|
|
})
|
|
// Second entry if available
|
|
if i+1 < end {
|
|
e2 := state.Entries[i+1]
|
|
prefix2 := " "
|
|
if state.Selected[e2.ID] {
|
|
prefix2 = "> "
|
|
}
|
|
title2 := e2.Title
|
|
if len(title2) > 30 {
|
|
title2 = title2[:27] + "..."
|
|
}
|
|
row = append(row, models.InlineKeyboardButton{
|
|
Text: prefix2 + title2, CallbackData: "pl_entry_" + e2.ID,
|
|
})
|
|
}
|
|
rows = append(rows, row)
|
|
}
|
|
|
|
// Navigation row
|
|
navRow := []models.InlineKeyboardButton{}
|
|
navRow = append(navRow, models.InlineKeyboardButton{
|
|
Text: "<", CallbackData: fmt.Sprintf("pl_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("pl_page_%d", state.Page+1),
|
|
})
|
|
rows = append(rows, navRow)
|
|
|
|
// Action row
|
|
actionRow := []models.InlineKeyboardButton{
|
|
{Text: "Select All", CallbackData: "pl_select_all"},
|
|
}
|
|
if selectedCount > 0 {
|
|
actionRow = append(actionRow, models.InlineKeyboardButton{
|
|
Text: fmt.Sprintf("Confirm (%d)", selectedCount), CallbackData: "pl_confirm",
|
|
})
|
|
}
|
|
rows = append(rows, actionRow)
|
|
|
|
// Cancel
|
|
rows = append(rows, []models.InlineKeyboardButton{
|
|
{Text: "Cancel", CallbackData: "cancel_dl"},
|
|
})
|
|
|
|
return models.InlineKeyboardMarkup{InlineKeyboard: rows}
|
|
}
|
|
|
|
// deleteConfirmKeyboard builds the confirmation prompt for account deletion.
|
|
func deleteConfirmKeyboard(userID int64) models.InlineKeyboardMarkup {
|
|
return models.InlineKeyboardMarkup{
|
|
InlineKeyboard: [][]models.InlineKeyboardButton{
|
|
{
|
|
{Text: "Yes, delete everything", CallbackData: fmt.Sprintf("delaccount_%d", userID)},
|
|
{Text: "Cancel", CallbackData: "back_settings"},
|
|
},
|
|
},
|
|
}
|
|
}
|