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')