All checks were successful
CI / build (push) Successful in 48s
- Replace Type/Quality settings with single Format button -> Audio/Video sub-pickers - Each sub-picker shows format options (bitrates for audio, resolutions for video) - Replace DefaultQuality with DefaultAudioFormat/DefaultVideoFormat in UserPreferences - Add DB migration 002 for new preference columns - Update repo layer SQL queries for new schema - Update playlist flow to use new preference fields - Fix missing deleteaccount and delaccount_ callback routing - Add back_format routing for format picker navigation
222 lines
5.8 KiB
Go
222 lines
5.8 KiB
Go
package repo
|
|
|
|
import (
|
|
"database/sql"
|
|
"embed"
|
|
"log/slog"
|
|
|
|
_ "modernc.org/sqlite"
|
|
|
|
"github.com/pressly/goose/v3"
|
|
|
|
"uptodownBot/internal/domain"
|
|
)
|
|
|
|
//go:embed migrations/*.sql
|
|
var migrationFS embed.FS
|
|
|
|
type Store struct {
|
|
db *sql.DB
|
|
}
|
|
|
|
func NewStore(path string) (*Store, error) {
|
|
db, err := sql.Open("sqlite", path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := db.Ping(); err != nil {
|
|
return nil, err
|
|
}
|
|
if _, err := db.Exec("PRAGMA journal_mode=WAL"); err != nil {
|
|
slog.Warn("failed to set WAL mode", "error", err)
|
|
}
|
|
if err := runMigrations(db); err != nil {
|
|
return nil, err
|
|
}
|
|
return &Store{db: db}, nil
|
|
}
|
|
|
|
func runMigrations(db *sql.DB) error {
|
|
goose.SetBaseFS(migrationFS)
|
|
if err := goose.SetDialect("sqlite3"); err != nil {
|
|
return err
|
|
}
|
|
if err := goose.Up(db, "migrations"); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Store) Close() error {
|
|
return s.db.Close()
|
|
}
|
|
|
|
func (s *Store) GetOrCreateUser(chatID int64) (int64, error) {
|
|
if _, err := s.db.Exec(
|
|
"INSERT OR IGNORE INTO users (chat_id) VALUES (?)",
|
|
chatID,
|
|
); err != nil {
|
|
return 0, err
|
|
}
|
|
var id int64
|
|
err := s.db.QueryRow("SELECT id FROM users WHERE chat_id = ?", chatID).Scan(&id)
|
|
return id, err
|
|
}
|
|
|
|
func (s *Store) GetOrCreatePreferences(userID int64) (*domain.UserPreferences, error) {
|
|
if _, err := s.db.Exec(
|
|
"INSERT OR IGNORE INTO user_preferences (user_id) VALUES (?)",
|
|
userID,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
return s.getPreferences(userID)
|
|
}
|
|
|
|
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
|
|
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 {
|
|
return nil, err
|
|
}
|
|
switch mediaType {
|
|
case "audio":
|
|
p.DefaultMediaType = domain.MediaTypeAudio
|
|
case "video":
|
|
p.DefaultMediaType = domain.MediaTypeVideo
|
|
default:
|
|
p.DefaultMediaType = domain.MediaTypeVideoAudio
|
|
}
|
|
return &p, nil
|
|
}
|
|
|
|
func mediaTypeToString(mt domain.MediaType) string {
|
|
switch mt {
|
|
case domain.MediaTypeAudio:
|
|
return "audio"
|
|
case domain.MediaTypeVideo:
|
|
return "video"
|
|
default:
|
|
return "video_audio"
|
|
}
|
|
}
|
|
|
|
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=?
|
|
WHERE user_id=?
|
|
`, mediaTypeToString(prefs.DefaultMediaType), prefs.DefaultAudioFormat,
|
|
prefs.DefaultVideoFormat, prefs.DefaultContainer, prefs.DefaultLanguage, userID)
|
|
return err
|
|
}
|
|
|
|
func (s *Store) GetOrCreateQuotas(userID int64) (*domain.UserQuotas, error) {
|
|
if _, err := s.db.Exec(
|
|
"INSERT OR IGNORE INTO user_quotas (user_id) VALUES (?)",
|
|
userID,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
return s.getQuotas(userID)
|
|
}
|
|
|
|
func (s *Store) getQuotas(userID int64) (*domain.UserQuotas, error) {
|
|
row := s.db.QueryRow(`
|
|
SELECT daily_limit_mb, daily_used_mb, daily_date,
|
|
weekly_limit_mb, weekly_used_mb, week_start,
|
|
monthly_limit_mb, monthly_used_mb, month_start
|
|
FROM user_quotas WHERE user_id = ?
|
|
`, userID)
|
|
var q domain.UserQuotas
|
|
err := row.Scan(
|
|
&q.DailyLimitMB, &q.DailyUsedMB, &q.DailyDate,
|
|
&q.WeeklyLimitMB, &q.WeeklyUsedMB, &q.WeekStart,
|
|
&q.MonthlyLimitMB, &q.MonthlyUsedMB, &q.MonthStart,
|
|
)
|
|
return &q, err
|
|
}
|
|
|
|
func (s *Store) UpdateQuotas(userID int64, q *domain.UserQuotas) error {
|
|
_, err := s.db.Exec(`
|
|
UPDATE user_quotas SET daily_limit_mb=?, daily_used_mb=?, daily_date=?,
|
|
weekly_limit_mb=?, weekly_used_mb=?, week_start=?,
|
|
monthly_limit_mb=?, monthly_used_mb=?, month_start=?,
|
|
updated_at=datetime('now')
|
|
WHERE user_id=?
|
|
`, q.DailyLimitMB, q.DailyUsedMB, q.DailyDate,
|
|
q.WeeklyLimitMB, q.WeeklyUsedMB, q.WeekStart,
|
|
q.MonthlyLimitMB, q.MonthlyUsedMB, q.MonthStart,
|
|
userID)
|
|
return err
|
|
}
|
|
|
|
func (s *Store) AddHistory(userID int64, record *domain.DownloadRecord) error {
|
|
_, err := s.db.Exec(`
|
|
INSERT INTO download_history (user_id, url, title, media_type, format_id, container, file_size, status)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
`, userID, record.URL, record.Title, record.MediaType, record.FormatID,
|
|
record.Container, record.FileSize, record.Status)
|
|
return err
|
|
}
|
|
|
|
func (s *Store) GetHistory(userID int64, limit, offset int) ([]domain.DownloadRecord, error) {
|
|
rows, err := s.db.Query(`
|
|
SELECT id, user_id, url, title, media_type, format_id, container, file_size, status, created_at
|
|
FROM download_history
|
|
WHERE user_id = ?
|
|
ORDER BY created_at DESC
|
|
LIMIT ? OFFSET ?
|
|
`, userID, limit, offset)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var records []domain.DownloadRecord
|
|
for rows.Next() {
|
|
var r domain.DownloadRecord
|
|
if err := rows.Scan(
|
|
&r.ID, &r.UserID, &r.URL, &r.Title, &r.MediaType,
|
|
&r.FormatID, &r.Container, &r.FileSize, &r.Status, &r.CreatedAt,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
records = append(records, r)
|
|
}
|
|
return records, rows.Err()
|
|
}
|
|
|
|
func (s *Store) GetHistoryCount(userID int64) (int, error) {
|
|
var count int
|
|
err := s.db.QueryRow("SELECT COUNT(1) FROM download_history WHERE user_id=?", userID).Scan(&count)
|
|
return count, err
|
|
}
|
|
|
|
func (s *Store) DeleteUserData(userID int64) error {
|
|
tx, err := s.db.Begin()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer tx.Rollback()
|
|
|
|
if _, err := tx.Exec("DELETE FROM download_history WHERE user_id=?", userID); err != nil {
|
|
return err
|
|
}
|
|
if _, err := tx.Exec("DELETE FROM user_quotas WHERE user_id=?", userID); err != nil {
|
|
return err
|
|
}
|
|
if _, err := tx.Exec("DELETE FROM user_preferences WHERE user_id=?", userID); err != nil {
|
|
return err
|
|
}
|
|
if _, err := tx.Exec("DELETE FROM users WHERE id=?", userID); err != nil {
|
|
return err
|
|
}
|
|
return tx.Commit()
|
|
}
|