feat: remove watchlist import/export functionality

This commit is contained in:
2026-05-07 00:52:56 +02:00
parent 2a6b5d5b8e
commit 4887088795
6 changed files with 1 additions and 347 deletions

View File

@@ -57,35 +57,6 @@ func (h *Handler) HandleUpdateWatchlist(w http.ResponseWriter, r *http.Request)
w.WriteHeader(http.StatusOK)
}
func (h *Handler) HandleImportWatchlist(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
user := middleware.GetUser(r.Context())
if user == nil {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
file, _, err := r.FormFile("file")
if err != nil {
http.Error(w, "failed to get file from request", http.StatusBadRequest)
return
}
defer file.Close()
if err := h.service.ImportWatchlist(r.Context(), user.ID, file); err != nil {
log.Printf("import failed: %v", err)
http.Error(w, "import failed: "+err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("HX-Redirect", "/watchlist")
w.WriteHeader(http.StatusOK)
}
func (h *Handler) HandleDeleteWatchlist(w http.ResponseWriter, r *http.Request) {
user := middleware.GetUser(r.Context())
if user == nil {

View File

@@ -3,12 +3,8 @@ package watchlist
import (
"context"
"database/sql"
"encoding/csv"
"errors"
"fmt"
"io"
"log"
"strconv"
"strings"
"github.com/google/uuid"
@@ -190,73 +186,3 @@ func (s *Service) DeleteContinueWatching(ctx context.Context, userID string, ani
return tx.Commit()
}
func (s *Service) ImportWatchlist(ctx context.Context, userID string, r io.Reader) error {
txQueries, tx, err := db.BeginTx(ctx, s.sqlDB)
if err != nil {
return fmt.Errorf("failed to begin transaction: %w", err)
}
defer tx.Rollback()
reader := csv.NewReader(r)
// Read header
if _, err := reader.Read(); err != nil {
return fmt.Errorf("failed to read csv header: %w", err)
}
records, err := reader.ReadAll()
if err != nil {
return fmt.Errorf("failed to read csv records: %w", err)
}
for i, record := range records {
// New format: anime_id,title,status,current_episode,current_time_seconds
// Old format: anime_id,status,current_episode,current_time_seconds
var animeIDStr, status, episodeStr, timeStr string
if len(record) >= 5 {
animeIDStr = record[0]
status = record[2]
episodeStr = record[3]
timeStr = record[4]
} else if len(record) >= 4 {
animeIDStr = record[0]
status = record[1]
episodeStr = record[2]
timeStr = record[3]
} else {
log.Printf("skipping row %d: insufficient columns", i+2)
continue
}
animeID, err := strconv.ParseInt(animeIDStr, 10, 64)
if err != nil {
return fmt.Errorf("row %d: invalid anime id: %w", i+2, err)
}
if _, ok := validStatuses[status]; !ok {
status = "plan_to_watch"
}
currentEpisode, _ := strconv.ParseInt(episodeStr, 10, 64)
currentTimeSeconds, _ := strconv.ParseFloat(timeStr, 64)
if err := s.ensureAnimeExists(ctx, animeID); err != nil {
return fmt.Errorf("row %d: failed to ensure anime: %w", i+2, err)
}
_, err = txQueries.UpsertWatchListEntry(ctx, db.UpsertWatchListEntryParams{
ID: uuid.New().String(),
UserID: userID,
AnimeID: animeID,
Status: status,
CurrentEpisode: sql.NullInt64{Int64: currentEpisode, Valid: currentEpisode > 0},
CurrentTimeSeconds: currentTimeSeconds,
})
if err != nil {
return fmt.Errorf("row %d: failed to upsert entry: %w", i+2, err)
}
}
return tx.Commit()
}

View File

@@ -2,14 +2,9 @@ package watchlist
import (
"context"
"database/sql"
"os"
"strings"
"testing"
"mal/internal/db"
_ "github.com/mattn/go-sqlite3"
)
type fakeQuerier struct {
@@ -74,87 +69,4 @@ func TestAddEntry_RejectsInvalidStatus(t *testing.T) {
if q.upsertAnimeCalled || q.upsertEntryCalled {
t.Fatal("expected no database writes for invalid status")
}
}
func TestImportWatchlist(t *testing.T) {
dbFile := "test_watchlist.db"
defer os.Remove(dbFile)
sqlDB, err := sql.Open("sqlite3", dbFile)
if err != nil {
t.Fatal(err)
}
defer sqlDB.Close()
// Minimal schema for testing
_, err = sqlDB.Exec(`
CREATE TABLE anime (
id INTEGER PRIMARY KEY,
title_original TEXT NOT NULL,
image_url TEXT NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
title_english TEXT,
title_japanese TEXT,
airing BOOLEAN,
status TEXT,
relations_synced_at DATETIME,
duration_seconds REAL
);
CREATE TABLE watch_list_entry (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
anime_id INTEGER NOT NULL REFERENCES anime(id),
status TEXT NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
current_episode INTEGER DEFAULT 0,
last_episode_at DATETIME,
current_time_seconds REAL NOT NULL DEFAULT 0,
UNIQUE(user_id, anime_id)
);
`)
if err != nil {
t.Fatal(err)
}
queries := db.New(sqlDB)
svc := NewService(queries, sqlDB, nil)
// Pre-insert anime so ensureAnimeExists succeeds
_, err = sqlDB.Exec(`INSERT INTO anime (id, title_original, image_url) VALUES (1, 'Test 1', '');`)
if err != nil {
t.Fatal(err)
}
_, err = sqlDB.Exec(`INSERT INTO anime (id, title_original, image_url) VALUES (2, 'Test 2', '');`)
if err != nil {
t.Fatal(err)
}
csvData := `anime_id,status,current_episode,current_time_seconds
1,watching,5,120.5
2,invalid,10,0
`
err = svc.ImportWatchlist(context.Background(), "user-1", strings.NewReader(csvData))
if err != nil {
t.Fatalf("ImportWatchlist failed: %v", err)
}
// Verify entries
var count int
err = sqlDB.QueryRow("SELECT COUNT(*) FROM watch_list_entry WHERE user_id = 'user-1'").Scan(&count)
if err != nil {
t.Fatal(err)
}
if count != 2 {
t.Errorf("expected 2 entries, got %d", count)
}
var status string
err = sqlDB.QueryRow("SELECT status FROM watch_list_entry WHERE anime_id = 2").Scan(&status)
if err != nil {
t.Fatal(err)
}
if status != "plan_to_watch" {
t.Errorf("expected status to be defaulted to plan_to_watch, got %s", status)
}
}
}