diff --git a/api/watchlist/handler.go b/api/watchlist/handler.go index cfe31e7..b1e2a0a 100644 --- a/api/watchlist/handler.go +++ b/api/watchlist/handler.go @@ -1,7 +1,6 @@ package watchlist import ( - "encoding/json" "errors" "log" "net/http" @@ -308,67 +307,6 @@ func (h *Handler) HandleDeleteContinueWatching(w http.ResponseWriter, r *http.Re w.WriteHeader(http.StatusOK) } -func (h *Handler) HandleExportWatchlist(w http.ResponseWriter, r *http.Request) { - if !requireMethod(w, r, http.MethodGet) { - return - } - - user := middleware.GetUser(r.Context()) - if user == nil { - http.Error(w, "Unauthorized", http.StatusUnauthorized) - return - } - - export, err := h.svc.Export(r.Context(), user.ID) - if err != nil { - log.Printf("watchlist export failed: user_id=%s err=%v", user.ID, err) - http.Error(w, "failed to export", http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "application/json") - w.Header().Set("Content-Disposition", "attachment; filename=mal-watchlist.json") - json.NewEncoder(w).Encode(export) -} - -func (h *Handler) HandleImportWatchlist(w http.ResponseWriter, r *http.Request) { - if !requireMethod(w, r, http.MethodPost) { - return - } - - user := middleware.GetUser(r.Context()) - if user == nil { - http.Error(w, "Unauthorized", http.StatusUnauthorized) - return - } - - if err := r.ParseMultipartForm(10 << 20); err != nil { - http.Error(w, "failed to parse form", http.StatusBadRequest) - return - } - - file, _, err := r.FormFile("file") - if err != nil { - http.Error(w, "no file uploaded", http.StatusBadRequest) - return - } - defer file.Close() - - var export ExportData - if err := json.NewDecoder(file).Decode(&export); err != nil { - http.Error(w, "invalid JSON format", http.StatusBadRequest) - return - } - - if _, err := h.svc.Import(r.Context(), user.ID, export); err != nil { - http.Error(w, "failed to import", http.StatusInternalServerError) - return - } - - w.Header().Set("HX-Redirect", "/watchlist") - w.WriteHeader(http.StatusOK) -} - func (h *Handler) sortEntries(entries []database.GetUserWatchListRow, sortBy, sortOrder string) { isAsc := sortOrder == "asc" diff --git a/api/watchlist/service.go b/api/watchlist/service.go index 0704b64..f2f18fc 100644 --- a/api/watchlist/service.go +++ b/api/watchlist/service.go @@ -6,7 +6,6 @@ import ( "errors" "fmt" "strings" - "time" "github.com/google/uuid" @@ -165,70 +164,3 @@ func (s *Service) DeleteContinueWatching(ctx context.Context, userID string, ani return tx.Commit() } - -type ExportEntry struct { - AnimeID int64 `json:"anime_id"` - Title string `json:"title"` - ImageURL string `json:"image_url"` - Status string `json:"status"` - UpdatedAt string `json:"updated_at"` -} - -type ExportData struct { - ExportedAt string `json:"exported_at"` - Entries []ExportEntry `json:"entries"` -} - -func (s *Service) Export(ctx context.Context, userID string) (ExportData, error) { - entries, err := s.GetUserWatchlist(ctx, userID) - if err != nil { - return ExportData{}, err - } - - export := ExportData{ - ExportedAt: time.Now().UTC().Format(time.RFC3339), - Entries: make([]ExportEntry, len(entries)), - } - - for i, entry := range entries { - export.Entries[i] = ExportEntry{ - AnimeID: entry.AnimeID, - Title: database.DisplayTitle(entry.TitleEnglish, entry.TitleJapanese, entry.TitleOriginal), - ImageURL: entry.ImageUrl, - Status: entry.Status, - UpdatedAt: entry.UpdatedAt.Format(time.RFC3339), - } - } - - return export, nil -} - -func (s *Service) Import(ctx context.Context, userID string, export ExportData) (int, error) { - imported := 0 - for _, entry := range export.Entries { - _, err := s.db.UpsertAnime(ctx, database.UpsertAnimeParams{ - ID: entry.AnimeID, - TitleOriginal: entry.Title, - TitleEnglish: sql.NullString{}, - TitleJapanese: sql.NullString{}, - ImageUrl: entry.ImageURL, - }) - if err != nil { - continue // skip failures and keep going - } - - _, err = s.db.UpsertWatchListEntry(ctx, database.UpsertWatchListEntryParams{ - ID: uuid.New().String(), - UserID: userID, - AnimeID: entry.AnimeID, - Status: entry.Status, - CurrentEpisode: sql.NullInt64{Int64: 0, Valid: false}, - CurrentTimeSeconds: 0, - }) - if err != nil { - continue - } - imported++ - } - return imported, nil -} diff --git a/api/watchlist/service_test.go b/api/watchlist/service_test.go index f60193d..ddb2c2f 100644 --- a/api/watchlist/service_test.go +++ b/api/watchlist/service_test.go @@ -2,9 +2,7 @@ package watchlist import ( "context" - "database/sql" "testing" - "time" "mal/internal/db" ) @@ -69,57 +67,3 @@ func TestAddEntry_RejectsInvalidStatus(t *testing.T) { t.Fatal("expected no database writes for invalid status") } } - -func TestExport_UsesDisplayTitleFallbackOrder(t *testing.T) { - t.Parallel() - - q := &fakeQuerier{ - addRows: []database.GetUserWatchListRow{ - { - AnimeID: 101, - TitleOriginal: "Original", - TitleEnglish: sql.NullString{String: "English", Valid: true}, - Status: "watching", - ImageUrl: "https://img", - UpdatedAt: time.Date(2026, 1, 2, 3, 4, 5, 0, time.UTC), - }, - { - AnimeID: 102, - TitleOriginal: "Original 2", - TitleJapanese: sql.NullString{String: "JP Title", Valid: true}, - Status: "completed", - ImageUrl: "https://img2", - UpdatedAt: time.Date(2026, 1, 3, 3, 4, 5, 0, time.UTC), - }, - { - AnimeID: 103, - TitleOriginal: "Original 3", - Status: "on_hold", - ImageUrl: "https://img3", - UpdatedAt: time.Date(2026, 1, 4, 3, 4, 5, 0, time.UTC), - }, - }, - } - - svc := NewService(q, nil) - export, err := svc.Export(context.Background(), "user-1") - if err != nil { - t.Fatalf("expected no error, got %v", err) - } - - if len(export.Entries) != 3 { - t.Fatalf("expected 3 entries, got %d", len(export.Entries)) - } - - if export.Entries[0].Title != "English" { - t.Fatalf("expected english title first, got %q", export.Entries[0].Title) - } - - if export.Entries[1].Title != "JP Title" { - t.Fatalf("expected japanese title fallback, got %q", export.Entries[1].Title) - } - - if export.Entries[2].Title != "Original 3" { - t.Fatalf("expected original title fallback, got %q", export.Entries[2].Title) - } -} diff --git a/web/templates/watchlist.templ b/web/templates/watchlist.templ index e540ef8..ebe23f0 100644 --- a/web/templates/watchlist.templ +++ b/web/templates/watchlist.templ @@ -26,38 +26,6 @@ templ Watchlist( Track what you're watching with less noise.

-
- - Export - - - - -