feat: initial implementation of uptodownbot
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.
This commit is contained in:
246
internal/handler/settings.go
Normal file
246
internal/handler/settings.go
Normal file
@@ -0,0 +1,246 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user