diff --git a/api/watchlist/handler.go b/api/watchlist/handler.go index bab69f2..a000fe8 100644 --- a/api/watchlist/handler.go +++ b/api/watchlist/handler.go @@ -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 { diff --git a/api/watchlist/service.go b/api/watchlist/service.go index 3e3c8a4..7875baa 100644 --- a/api/watchlist/service.go +++ b/api/watchlist/service.go @@ -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() -} diff --git a/api/watchlist/service_test.go b/api/watchlist/service_test.go index d6cb094..b4499ec 100644 --- a/api/watchlist/service_test.go +++ b/api/watchlist/service_test.go @@ -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) - } -} +} \ No newline at end of file diff --git a/internal/server/routes.go b/internal/server/routes.go index aa01dac..8092e0d 100644 --- a/internal/server/routes.go +++ b/internal/server/routes.go @@ -135,7 +135,6 @@ func NewRouter(cfg Config) http.Handler { // Watchlist Endpoints mux.HandleFunc("/api/watchlist/card", watchlistHandler.HandleCardWatchlist) - mux.HandleFunc("/api/watchlist/import", watchlistHandler.HandleImportWatchlist) mux.HandleFunc("/api/watchlist", watchlistHandler.HandleUpdateWatchlist) mux.HandleFunc("/api/watchlist/", watchlistHandler.HandleDeleteWatchlist) mux.HandleFunc("/api/continue-watching/", watchlistHandler.HandleDeleteContinueWatching) diff --git a/templates/watchlist.gohtml b/templates/watchlist.gohtml index b839279..08e7e44 100644 --- a/templates/watchlist.gohtml +++ b/templates/watchlist.gohtml @@ -32,27 +32,6 @@ - - -
- -
- -
@@ -147,62 +126,6 @@ sortItems() } - function exportWatchlistCSV() { - const items = document.querySelectorAll('.watchlist-item'); - if (items.length === 0) { - alert('Watchlist is empty'); - return; - } - - let csv = 'anime_id,title,status,current_episode,current_time_seconds\n'; - items.forEach(function(item) { - const animeId = item.querySelector('a').href.split('/').pop(); - const title = (item.dataset.title || '').replace(/"/g, '""'); - const status = item.dataset.status || 'plan_to_watch'; - const episode = item.dataset.episode || '0'; - const time = item.dataset.time || '0'; - csv += `${animeId},"${title}",${status},${episode},${time}\n`; - }); - - const blob = new Blob([csv], { type: 'text/csv' }); - const url = window.URL.createObjectURL(blob); - const a = document.createElement('a'); - a.setAttribute('hidden', ''); - a.setAttribute('href', url); - a.setAttribute('download', 'watchlist.csv'); - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - } - - async function importWatchlistCSV(input) { - if (!input.files || input.files.length === 0) return; - - const formData = new FormData(); - formData.append('file', input.files[0]); - - try { - const resp = await fetch('/api/watchlist/import', { - method: 'POST', - body: formData, - headers: { - 'HX-Request': 'true' - } - }); - - if (resp.ok) { - const redirect = resp.headers.get('HX-Redirect'); - if (redirect) window.location.href = redirect; - else window.location.reload(); - } else { - const text = await resp.text(); - alert('Import failed: ' + text); - } - } catch (err) { - alert('Import error: ' + err); - } - } - function sortItems() { const grid = document.getElementById('watchlist-items') const items = Array.from(grid.querySelectorAll('.watchlist-item')) diff --git a/templates/watchlist_partial.gohtml b/templates/watchlist_partial.gohtml index a4a4470..47242d9 100644 --- a/templates/watchlist_partial.gohtml +++ b/templates/watchlist_partial.gohtml @@ -31,27 +31,6 @@ - - -
- -
- -
@@ -148,62 +127,6 @@ sortItems() } - function exportWatchlistCSV() { - const items = document.querySelectorAll('.watchlist-item'); - if (items.length === 0) { - alert('Watchlist is empty'); - return; - } - - let csv = 'anime_id,title,status,current_episode,current_time_seconds\n'; - items.forEach(function(item) { - const animeId = item.querySelector('a').href.split('/').pop(); - const title = (item.dataset.title || '').replace(/"/g, '""'); - const status = item.dataset.status || 'plan_to_watch'; - const episode = item.dataset.episode || '0'; - const time = item.dataset.time || '0'; - csv += `${animeId},"${title}",${status},${episode},${time}\n`; - }); - - const blob = new Blob([csv], { type: 'text/csv' }); - const url = window.URL.createObjectURL(blob); - const a = document.createElement('a'); - a.setAttribute('hidden', ''); - a.setAttribute('href', url); - a.setAttribute('download', 'watchlist.csv'); - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - } - - async function importWatchlistCSV(input) { - if (!input.files || input.files.length === 0) return; - - const formData = new FormData(); - formData.append('file', input.files[0]); - - try { - const resp = await fetch('/api/watchlist/import', { - method: 'POST', - body: formData, - headers: { - 'HX-Request': 'true' - } - }); - - if (resp.ok) { - const redirect = resp.headers.get('HX-Redirect'); - if (redirect) window.location.href = redirect; - else window.location.reload(); - } else { - const text = await resp.text(); - alert('Import failed: ' + text); - } - } catch (err) { - alert('Import error: ' + err); - } - } - function sortItems() { document.querySelectorAll('.watchlist-section').forEach(function(section) { const grid = section.querySelector('.grid')