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.
247 lines
7.6 KiB
Go
247 lines
7.6 KiB
Go
package handler
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log/slog"
|
|
"strings"
|
|
|
|
"github.com/go-telegram/bot/models"
|
|
|
|
"uptodownBot/internal/domain"
|
|
)
|
|
|
|
func (h *Handler) buildSettingsKeyboard(userID int64, prefs *domain.UserPreferences) (string, models.InlineKeyboardMarkup) {
|
|
mediaTypeLabel := "Video+Audio"
|
|
switch prefs.DefaultMediaType {
|
|
case domain.MediaTypeAudio:
|
|
mediaTypeLabel = "Audio Only"
|
|
case domain.MediaTypeVideo:
|
|
mediaTypeLabel = "Video Only"
|
|
}
|
|
qualityLabel := prefs.DefaultQuality
|
|
if qualityLabel == "" {
|
|
qualityLabel = "best"
|
|
}
|
|
containerLabel := prefs.DefaultContainer
|
|
if containerLabel == "" {
|
|
containerLabel = "mp4"
|
|
}
|
|
langLabel := prefs.DefaultLanguage
|
|
if langLabel == "" {
|
|
langLabel = "Default"
|
|
}
|
|
|
|
rows := [][]models.InlineKeyboardButton{
|
|
{
|
|
{Text: fmt.Sprintf("Type: %s", mediaTypeLabel), CallbackData: "settings_media_type"},
|
|
{Text: fmt.Sprintf("Quality: %s", qualityLabel), CallbackData: "settings_quality"},
|
|
},
|
|
{
|
|
{Text: fmt.Sprintf("Container: %s", containerLabel), CallbackData: "settings_container"},
|
|
{Text: fmt.Sprintf("Language: %s", langLabel), CallbackData: "settings_language"},
|
|
},
|
|
{{Text: "Delete my data", CallbackData: "delaccount_prompt"}},
|
|
{{Text: "Back to Menu", CallbackData: "back_menu"}},
|
|
}
|
|
return "Settings:", models.InlineKeyboardMarkup{InlineKeyboard: rows}
|
|
}
|
|
|
|
func (h *Handler) settingsCallback(ctx context.Context, chatID int64, msgID int, cbID string, userID int64) {
|
|
defer h.answerCb(ctx, cbID)
|
|
prefs, err := h.Repo.GetOrCreatePreferences(userID)
|
|
if err != nil {
|
|
return
|
|
}
|
|
text, kb := h.buildSettingsKeyboard(userID, prefs)
|
|
h.editText(ctx, chatID, msgID, text, &kb)
|
|
}
|
|
|
|
func (h *Handler) backToSettings(ctx context.Context, chatID int64, msgID int, userID int64) {
|
|
prefs, err := h.Repo.GetOrCreatePreferences(userID)
|
|
if err != nil {
|
|
return
|
|
}
|
|
text, kb := h.buildSettingsKeyboard(userID, prefs)
|
|
h.editText(ctx, chatID, msgID, text, &kb)
|
|
}
|
|
|
|
func (h *Handler) handleSettingsSelection(ctx context.Context, chatID int64, msgID int, userID int64, setting string) {
|
|
prefs, err := h.Repo.GetOrCreatePreferences(userID)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
switch setting {
|
|
case "media_type":
|
|
text, kb := h.buildMediaTypePicker(prefs)
|
|
h.editText(ctx, chatID, msgID, text, &kb)
|
|
case "quality":
|
|
text, kb := h.buildQualityPicker(prefs)
|
|
h.editText(ctx, chatID, msgID, text, &kb)
|
|
case "container":
|
|
text, kb := h.buildContainerPicker(prefs)
|
|
h.editText(ctx, chatID, msgID, text, &kb)
|
|
case "language":
|
|
text, kb := h.buildLanguagePicker(prefs)
|
|
h.editText(ctx, chatID, msgID, text, &kb)
|
|
}
|
|
}
|
|
|
|
func (h *Handler) buildMediaTypePicker(prefs *domain.UserPreferences) (string, models.InlineKeyboardMarkup) {
|
|
options := []struct {
|
|
label string
|
|
value domain.MediaType
|
|
data string
|
|
}{
|
|
{"Audio Only", domain.MediaTypeAudio, "settings_set_type_audio"},
|
|
{"Video Only", domain.MediaTypeVideo, "settings_set_type_video"},
|
|
{"Video+Audio", domain.MediaTypeVideoAudio, "settings_set_type_both"},
|
|
}
|
|
rows := [][]models.InlineKeyboardButton{}
|
|
for _, opt := range options {
|
|
label := opt.label
|
|
if opt.value == prefs.DefaultMediaType {
|
|
label = "> " + label
|
|
}
|
|
rows = append(rows, []models.InlineKeyboardButton{
|
|
{Text: label, CallbackData: opt.data},
|
|
})
|
|
}
|
|
rows = append(rows, []models.InlineKeyboardButton{
|
|
{Text: "Back to Settings", CallbackData: "back_settings"},
|
|
})
|
|
return "Select default media type:", models.InlineKeyboardMarkup{InlineKeyboard: rows}
|
|
}
|
|
|
|
func (h *Handler) buildQualityPicker(prefs *domain.UserPreferences) (string, models.InlineKeyboardMarkup) {
|
|
qualities := []string{"best", "2160p", "1080p", "720p", "480p", "360p"}
|
|
rows := [][]models.InlineKeyboardButton{}
|
|
row := []models.InlineKeyboardButton{}
|
|
for i, q := range qualities {
|
|
label := q
|
|
if q == prefs.DefaultQuality {
|
|
label = "> " + label
|
|
}
|
|
row = append(row, models.InlineKeyboardButton{
|
|
Text: label, CallbackData: "settings_set_quality_" + q,
|
|
})
|
|
if len(row) == 3 || i == len(qualities)-1 {
|
|
rows = append(rows, row)
|
|
row = nil
|
|
}
|
|
}
|
|
rows = append(rows, []models.InlineKeyboardButton{
|
|
{Text: "Back to Settings", CallbackData: "back_settings"},
|
|
})
|
|
return "Select default quality:", models.InlineKeyboardMarkup{InlineKeyboard: rows}
|
|
}
|
|
|
|
func (h *Handler) buildContainerPicker(prefs *domain.UserPreferences) (string, models.InlineKeyboardMarkup) {
|
|
containers := []string{"mp4", "mkv", "webm", "mp3", "m4a", "opus"}
|
|
rows := [][]models.InlineKeyboardButton{}
|
|
row := []models.InlineKeyboardButton{}
|
|
for i, c := range containers {
|
|
label := c
|
|
if c == prefs.DefaultContainer {
|
|
label = "> " + label
|
|
}
|
|
row = append(row, models.InlineKeyboardButton{
|
|
Text: label, CallbackData: "settings_set_container_" + c,
|
|
})
|
|
if len(row) == 3 || i == len(containers)-1 {
|
|
rows = append(rows, row)
|
|
row = nil
|
|
}
|
|
}
|
|
rows = append(rows, []models.InlineKeyboardButton{
|
|
{Text: "Back to Settings", CallbackData: "back_settings"},
|
|
})
|
|
return "Select default container:", models.InlineKeyboardMarkup{InlineKeyboard: rows}
|
|
}
|
|
|
|
func (h *Handler) buildLanguagePicker(prefs *domain.UserPreferences) (string, models.InlineKeyboardMarkup) {
|
|
languages := []string{"", "en", "es", "ja", "ko", "fr", "de", "zh", "ar", "pt", "ru"}
|
|
langLabels := map[string]string{
|
|
"": "Default", "en": "English", "es": "Spanish", "ja": "Japanese",
|
|
"ko": "Korean", "fr": "French", "de": "German", "zh": "Chinese",
|
|
"ar": "Arabic", "pt": "Portuguese", "ru": "Russian",
|
|
}
|
|
rows := [][]models.InlineKeyboardButton{}
|
|
row := []models.InlineKeyboardButton{}
|
|
for i, l := range languages {
|
|
label := langLabels[l]
|
|
if l == prefs.DefaultLanguage {
|
|
label = "> " + label
|
|
}
|
|
row = append(row, models.InlineKeyboardButton{
|
|
Text: label, CallbackData: "settings_set_language_" + l,
|
|
})
|
|
if len(row) == 2 || i == len(languages)-1 {
|
|
rows = append(rows, row)
|
|
row = nil
|
|
}
|
|
}
|
|
rows = append(rows, []models.InlineKeyboardButton{
|
|
{Text: "Back to Settings", CallbackData: "back_settings"},
|
|
})
|
|
return "Select default language:", models.InlineKeyboardMarkup{InlineKeyboard: rows}
|
|
}
|
|
|
|
func (h *Handler) handleSettingsSet(ctx context.Context, chatID int64, msgID int, userID int64, setting string) {
|
|
parts := strings.SplitN(setting, "_", 2)
|
|
if len(parts) < 2 {
|
|
return
|
|
}
|
|
action := parts[0]
|
|
value := parts[1]
|
|
|
|
prefs, err := h.Repo.GetOrCreatePreferences(userID)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
changed := false
|
|
switch action {
|
|
case "type":
|
|
switch value {
|
|
case "audio":
|
|
prefs.DefaultMediaType = domain.MediaTypeAudio
|
|
case "video":
|
|
prefs.DefaultMediaType = domain.MediaTypeVideo
|
|
case "both":
|
|
prefs.DefaultMediaType = domain.MediaTypeVideoAudio
|
|
}
|
|
changed = true
|
|
case "quality":
|
|
prefs.DefaultQuality = value
|
|
changed = true
|
|
case "container":
|
|
prefs.DefaultContainer = value
|
|
changed = true
|
|
case "language":
|
|
prefs.DefaultLanguage = value
|
|
changed = true
|
|
}
|
|
if changed {
|
|
_ = h.Repo.UpdatePreferences(userID, prefs)
|
|
}
|
|
h.backToSettings(ctx, chatID, msgID, userID)
|
|
}
|
|
|
|
// deleteAccountPrompt shows the deletion confirmation prompt.
|
|
func (h *Handler) deleteAccountPrompt(ctx context.Context, chatID int64, msgID int, userID int64) {
|
|
kb := deleteConfirmKeyboard(userID)
|
|
h.editText(ctx, chatID, msgID, "This will permanently delete ALL your data (preferences, quota, history).\nAre you sure?", &kb)
|
|
}
|
|
|
|
// deleteAccountConfirm permanently deletes all user data.
|
|
func (h *Handler) deleteAccountConfirm(ctx context.Context, chatID int64, msgID int, userID int64) {
|
|
if err := h.Repo.DeleteUserData(userID); err != nil {
|
|
slog.Error("delete user data", "user_id", userID, "error", err)
|
|
h.editText(ctx, chatID, msgID, "Error deleting data. Please try again.", nil)
|
|
return
|
|
}
|
|
h.editText(ctx, chatID, msgID, "All your data has been deleted. You can start fresh by sending /start.", nil)
|
|
}
|