diff --git a/internal/domain/types.go b/internal/domain/types.go index 4fa5063..e37365a 100644 --- a/internal/domain/types.go +++ b/internal/domain/types.go @@ -134,11 +134,12 @@ type DownloadJob struct { // UserPreferences holds per-user default preferences. type UserPreferences struct { - DefaultMediaType MediaType - DefaultAudioFormat string - DefaultVideoFormat string - DefaultContainer string - DefaultLanguage string + DefaultMediaType MediaType + DefaultAudioFormat string + DefaultVideoFormat string + DefaultAudioContainer string + DefaultVideoContainer string + DefaultLanguage string } // UserQuotas holds the quota usage and limits for a user. diff --git a/internal/handler/handler.go b/internal/handler/handler.go index 7c68e04..4643f32 100644 --- a/internal/handler/handler.go +++ b/internal/handler/handler.go @@ -266,8 +266,10 @@ func (h *Handler) routePrefixedCallback(ctx context.Context, chatID int64, msgID if uid, err := strconv.ParseInt(data[11:], 10, 64); err == nil && uid > 0 { h.deleteAccountConfirm(ctx, chatID, msgID, uid) } - case data == "back_format": - h.handleSettingsSelection(ctx, chatID, msgID, userID, "format") + case data == "back_quality": + h.handleSettingsSelection(ctx, chatID, msgID, userID, "quality") + case data == "back_container": + h.handleSettingsSelection(ctx, chatID, msgID, userID, "container") case data == "back_settings": h.backToSettings(ctx, chatID, msgID, userID) case strings.HasPrefix(data, "settings_set_"): diff --git a/internal/handler/playlist.go b/internal/handler/playlist.go index 66f30b7..b934ad0 100644 --- a/internal/handler/playlist.go +++ b/internal/handler/playlist.go @@ -142,16 +142,19 @@ func (h *Handler) handlePlaylistConfirm(ctx context.Context, chatID int64, msgID prefs, prefErr := h.Repo.GetOrCreatePreferences(userID) if prefErr != nil { prefs = &domain.UserPreferences{ - DefaultMediaType: domain.MediaTypeVideoAudio, - DefaultAudioFormat: "best", - DefaultVideoFormat: "best", - DefaultContainer: "mp4", + DefaultMediaType: domain.MediaTypeVideoAudio, + DefaultAudioFormat: "best", + DefaultVideoFormat: "best", + DefaultAudioContainer: "mp3", + DefaultVideoContainer: "mp4", } } formatID := prefs.DefaultVideoFormat + container := prefs.DefaultVideoContainer if prefs.DefaultMediaType == domain.MediaTypeAudio { formatID = prefs.DefaultAudioFormat + container = prefs.DefaultAudioContainer } format := &domain.Format{ID: formatID} for _, f := range info.Formats { @@ -168,7 +171,7 @@ func (h *Handler) handlePlaylistConfirm(ctx context.Context, chatID int64, msgID URL: entry.URL, MediaType: prefs.DefaultMediaType, SelectedFormat: format, - Container: prefs.DefaultContainer, + Container: container, Language: prefs.DefaultLanguage, Status: domain.StatusDownloading, Title: entry.Title, diff --git a/internal/handler/settings.go b/internal/handler/settings.go index bd4fa2c..5256cd0 100644 --- a/internal/handler/settings.go +++ b/internal/handler/settings.go @@ -20,9 +20,13 @@ func (h *Handler) buildSettingsKeyboard(userID int64, prefs *domain.UserPreferen if videoFmt == "" { videoFmt = "best" } - containerLabel := prefs.DefaultContainer - if containerLabel == "" { - containerLabel = "mp4" + audioCont := prefs.DefaultAudioContainer + if audioCont == "" { + audioCont = "mp3" + } + videoCont := prefs.DefaultVideoContainer + if videoCont == "" { + videoCont = "mp4" } langLabel := prefs.DefaultLanguage if langLabel == "" { @@ -31,10 +35,10 @@ func (h *Handler) buildSettingsKeyboard(userID int64, prefs *domain.UserPreferen rows := [][]models.InlineKeyboardButton{ { - {Text: fmt.Sprintf("Format: %s/%s", audioFmt, videoFmt), CallbackData: "settings_format"}, + {Text: fmt.Sprintf("Quality: %s/%s", audioFmt, videoFmt), CallbackData: "settings_quality"}, }, { - {Text: fmt.Sprintf("Container: %s", containerLabel), CallbackData: "settings_container"}, + {Text: fmt.Sprintf("Container: %s/%s", audioCont, videoCont), CallbackData: "settings_container"}, {Text: fmt.Sprintf("Language: %s", langLabel), CallbackData: "settings_language"}, }, {{Text: "Delete all data", CallbackData: "deleteaccount", Style: "danger"}}, @@ -69,17 +73,23 @@ func (h *Handler) handleSettingsSelection(ctx context.Context, chatID int64, msg } switch setting { - case "format": - text, kb := h.buildFormatPicker(prefs) + case "quality": + text, kb := h.buildQualityAudioVideoPicker(prefs) h.editText(ctx, chatID, msgID, text, &kb) - case "format_audio": - text, kb := h.buildAudioFormatPicker(prefs) + case "quality_audio": + text, kb := h.buildAudioQualityPicker(prefs) h.editText(ctx, chatID, msgID, text, &kb) - case "format_video": - text, kb := h.buildVideoFormatPicker(prefs) + case "quality_video": + text, kb := h.buildVideoQualityPicker(prefs) h.editText(ctx, chatID, msgID, text, &kb) case "container": - text, kb := h.buildContainerPicker(prefs) + text, kb := h.buildContainerAudioVideoPicker(prefs) + h.editText(ctx, chatID, msgID, text, &kb) + case "container_audio": + text, kb := h.buildAudioContainerPicker(prefs) + h.editText(ctx, chatID, msgID, text, &kb) + case "container_video": + text, kb := h.buildVideoContainerPicker(prefs) h.editText(ctx, chatID, msgID, text, &kb) case "language": text, kb := h.buildLanguagePicker(prefs) @@ -87,16 +97,16 @@ func (h *Handler) handleSettingsSelection(ctx context.Context, chatID int64, msg } } -func (h *Handler) buildFormatPicker(prefs *domain.UserPreferences) (string, models.InlineKeyboardMarkup) { +func (h *Handler) buildQualityAudioVideoPicker(prefs *domain.UserPreferences) (string, models.InlineKeyboardMarkup) { rows := [][]models.InlineKeyboardButton{ - {{Text: "Audio", CallbackData: "settings_format_audio"}}, - {{Text: "Video", CallbackData: "settings_format_video"}}, + {{Text: "Audio", CallbackData: "settings_quality_audio"}}, + {{Text: "Video", CallbackData: "settings_quality_video"}}, {{Text: "Back to Settings", CallbackData: "back_settings"}}, } - return "Choose format type to set default:", models.InlineKeyboardMarkup{InlineKeyboard: rows} + return "Choose quality type to set default:", models.InlineKeyboardMarkup{InlineKeyboard: rows} } -func (h *Handler) buildAudioFormatPicker(prefs *domain.UserPreferences) (string, models.InlineKeyboardMarkup) { +func (h *Handler) buildAudioQualityPicker(prefs *domain.UserPreferences) (string, models.InlineKeyboardMarkup) { bitrates := []string{"best", "128k", "192k", "256k", "320k"} rows := [][]models.InlineKeyboardButton{} for _, b := range bitrates { @@ -105,16 +115,16 @@ func (h *Handler) buildAudioFormatPicker(prefs *domain.UserPreferences) (string, label = "> " + label } rows = append(rows, []models.InlineKeyboardButton{ - {Text: label, CallbackData: "settings_set_audioformat_" + b}, + {Text: label, CallbackData: "settings_set_audioquality_" + b}, }) } rows = append(rows, []models.InlineKeyboardButton{ - {Text: "Back", CallbackData: "back_format"}, + {Text: "Back", CallbackData: "back_quality"}, }) - return "Select default audio format:", models.InlineKeyboardMarkup{InlineKeyboard: rows} + return "Select default audio quality:", models.InlineKeyboardMarkup{InlineKeyboard: rows} } -func (h *Handler) buildVideoFormatPicker(prefs *domain.UserPreferences) (string, models.InlineKeyboardMarkup) { +func (h *Handler) buildVideoQualityPicker(prefs *domain.UserPreferences) (string, models.InlineKeyboardMarkup) { resolutions := []string{"best", "2160p", "1440p", "1080p", "720p", "480p", "360p"} rows := [][]models.InlineKeyboardButton{} row := []models.InlineKeyboardButton{} @@ -124,7 +134,7 @@ func (h *Handler) buildVideoFormatPicker(prefs *domain.UserPreferences) (string, label = "> " + label } row = append(row, models.InlineKeyboardButton{ - Text: label, CallbackData: "settings_set_videoformat_" + r, + Text: label, CallbackData: "settings_set_videoquality_" + r, }) if len(row) == 3 || i == len(resolutions)-1 { rows = append(rows, row) @@ -132,22 +142,49 @@ func (h *Handler) buildVideoFormatPicker(prefs *domain.UserPreferences) (string, } } rows = append(rows, []models.InlineKeyboardButton{ - {Text: "Back", CallbackData: "back_format"}, + {Text: "Back", CallbackData: "back_quality"}, }) - return "Select default video format:", models.InlineKeyboardMarkup{InlineKeyboard: rows} + return "Select default video quality:", models.InlineKeyboardMarkup{InlineKeyboard: rows} } -func (h *Handler) buildContainerPicker(prefs *domain.UserPreferences) (string, models.InlineKeyboardMarkup) { - containers := []string{"mp4", "mkv", "webm", "mp3", "m4a", "opus"} +func (h *Handler) buildContainerAudioVideoPicker(prefs *domain.UserPreferences) (string, models.InlineKeyboardMarkup) { + rows := [][]models.InlineKeyboardButton{ + {{Text: "Audio", CallbackData: "settings_container_audio"}}, + {{Text: "Video", CallbackData: "settings_container_video"}}, + {{Text: "Back to Settings", CallbackData: "back_settings"}}, + } + return "Choose container type to set default:", models.InlineKeyboardMarkup{InlineKeyboard: rows} +} + +func (h *Handler) buildAudioContainerPicker(prefs *domain.UserPreferences) (string, models.InlineKeyboardMarkup) { + containers := []string{"mp3", "m4a", "opus"} + rows := [][]models.InlineKeyboardButton{} + for _, c := range containers { + label := c + if c == prefs.DefaultAudioContainer { + label = "> " + label + } + rows = append(rows, []models.InlineKeyboardButton{ + {Text: label, CallbackData: "settings_set_audiocontainer_" + c}, + }) + } + rows = append(rows, []models.InlineKeyboardButton{ + {Text: "Back", CallbackData: "back_container"}, + }) + return "Select default audio container:", models.InlineKeyboardMarkup{InlineKeyboard: rows} +} + +func (h *Handler) buildVideoContainerPicker(prefs *domain.UserPreferences) (string, models.InlineKeyboardMarkup) { + containers := []string{"mp4", "mkv", "webm"} rows := [][]models.InlineKeyboardButton{} row := []models.InlineKeyboardButton{} for i, c := range containers { label := c - if c == prefs.DefaultContainer { + if c == prefs.DefaultVideoContainer { label = "> " + label } row = append(row, models.InlineKeyboardButton{ - Text: label, CallbackData: "settings_set_container_" + c, + Text: label, CallbackData: "settings_set_videocontainer_" + c, }) if len(row) == 3 || i == len(containers)-1 { rows = append(rows, row) @@ -155,9 +192,9 @@ func (h *Handler) buildContainerPicker(prefs *domain.UserPreferences) (string, m } } rows = append(rows, []models.InlineKeyboardButton{ - {Text: "Back to Settings", CallbackData: "back_settings"}, + {Text: "Back", CallbackData: "back_container"}, }) - return "Select default container:", models.InlineKeyboardMarkup{InlineKeyboard: rows} + return "Select default video container:", models.InlineKeyboardMarkup{InlineKeyboard: rows} } func (h *Handler) buildLanguagePicker(prefs *domain.UserPreferences) (string, models.InlineKeyboardMarkup) { @@ -203,14 +240,17 @@ func (h *Handler) handleSettingsSet(ctx context.Context, chatID int64, msgID int changed := false switch action { - case "audioformat": + case "audioquality": prefs.DefaultAudioFormat = value changed = true - case "videoformat": + case "videoquality": prefs.DefaultVideoFormat = value changed = true - case "container": - prefs.DefaultContainer = value + case "audiocontainer": + prefs.DefaultAudioContainer = value + changed = true + case "videocontainer": + prefs.DefaultVideoContainer = value changed = true case "language": prefs.DefaultLanguage = value diff --git a/internal/repo/migrations/003_preferences_container.sql b/internal/repo/migrations/003_preferences_container.sql new file mode 100644 index 0000000..f7c1d5e --- /dev/null +++ b/internal/repo/migrations/003_preferences_container.sql @@ -0,0 +1,7 @@ +-- +goose Up +ALTER TABLE user_preferences ADD COLUMN default_audio_container TEXT NOT NULL DEFAULT 'mp3'; +ALTER TABLE user_preferences ADD COLUMN default_video_container TEXT NOT NULL DEFAULT 'mp4'; + +-- +goose Down +ALTER TABLE user_preferences DROP COLUMN default_audio_container; +ALTER TABLE user_preferences DROP COLUMN default_video_container; diff --git a/internal/repo/store.go b/internal/repo/store.go index 11296f3..655afa7 100644 --- a/internal/repo/store.go +++ b/internal/repo/store.go @@ -76,13 +76,13 @@ func (s *Store) GetOrCreatePreferences(userID int64) (*domain.UserPreferences, e func (s *Store) getPreferences(userID int64) (*domain.UserPreferences, error) { row := s.db.QueryRow(` SELECT default_media_type, default_audio_format, default_video_format, - default_container, default_language + default_audio_container, default_video_container, default_language FROM user_preferences WHERE user_id = ? `, userID) var p domain.UserPreferences var mediaType string if err := row.Scan(&mediaType, &p.DefaultAudioFormat, &p.DefaultVideoFormat, - &p.DefaultContainer, &p.DefaultLanguage); err != nil { + &p.DefaultAudioContainer, &p.DefaultVideoContainer, &p.DefaultLanguage); err != nil { return nil, err } switch mediaType { @@ -110,10 +110,12 @@ func mediaTypeToString(mt domain.MediaType) string { func (s *Store) UpdatePreferences(userID int64, prefs *domain.UserPreferences) error { _, err := s.db.Exec(` UPDATE user_preferences SET default_media_type=?, default_audio_format=?, - default_video_format=?, default_container=?, default_language=? + default_video_format=?, default_audio_container=?, + default_video_container=?, default_language=? WHERE user_id=? `, mediaTypeToString(prefs.DefaultMediaType), prefs.DefaultAudioFormat, - prefs.DefaultVideoFormat, prefs.DefaultContainer, prefs.DefaultLanguage, userID) + prefs.DefaultVideoFormat, prefs.DefaultAudioContainer, + prefs.DefaultVideoContainer, prefs.DefaultLanguage, userID) return err } diff --git a/internal/repo/store_test.go b/internal/repo/store_test.go index 4b36a4e..f1f420a 100644 --- a/internal/repo/store_test.go +++ b/internal/repo/store_test.go @@ -66,7 +66,8 @@ func TestPreferences(t *testing.T) { prefs.DefaultMediaType = domain.MediaTypeAudio prefs.DefaultAudioFormat = "best" prefs.DefaultVideoFormat = "1080p" - prefs.DefaultContainer = "mp3" + prefs.DefaultAudioContainer = "mp3" + prefs.DefaultVideoContainer = "mp4" prefs.DefaultLanguage = "en" if err := s.UpdatePreferences(userID, prefs); err != nil { t.Fatalf("UpdatePreferences() failed: %v", err) @@ -85,8 +86,11 @@ func TestPreferences(t *testing.T) { if got.DefaultVideoFormat != "1080p" { t.Errorf("video format = %q, want 1080p", got.DefaultVideoFormat) } - if got.DefaultContainer != "mp3" { - t.Errorf("container = %q, want mp3", got.DefaultContainer) + if got.DefaultAudioContainer != "mp3" { + t.Errorf("audio container = %q, want mp3", got.DefaultAudioContainer) + } + if got.DefaultVideoContainer != "mp4" { + t.Errorf("video container = %q, want mp4", got.DefaultVideoContainer) } if got.DefaultLanguage != "en" { t.Errorf("language = %q, want en", got.DefaultLanguage)