package watchlist import ( "context" "database/sql" "fmt" "time" "github.com/google/uuid" "malago/internal/database" ) type Service struct { db database.Querier } func NewService(db database.Querier) *Service { return &Service{db: db} } type AddRequest struct { AnimeID int64 TitleOriginal string TitleEnglish string TitleJapanese string ImageURL string Status string } func (s *Service) AddEntry(ctx context.Context, userID string, req AddRequest) error { if req.AnimeID == 0 { return fmt.Errorf("invalid anime ID") } _, err := s.db.UpsertAnime(ctx, database.UpsertAnimeParams{ ID: req.AnimeID, TitleOriginal: req.TitleOriginal, TitleEnglish: sql.NullString{String: req.TitleEnglish, Valid: req.TitleEnglish != ""}, TitleJapanese: sql.NullString{String: req.TitleJapanese, Valid: req.TitleJapanese != ""}, ImageUrl: req.ImageURL, }) if err != nil { return fmt.Errorf("failed to save anime reference: %w", err) } entryID := uuid.New().String() _, err = s.db.UpsertWatchListEntry(ctx, database.UpsertWatchListEntryParams{ ID: entryID, UserID: userID, AnimeID: req.AnimeID, Status: req.Status, }) if err != nil { return fmt.Errorf("failed to update watchlist: %w", err) } return nil } func (s *Service) RemoveEntry(ctx context.Context, userID string, animeID int64) (database.Anime, error) { if animeID == 0 { return database.Anime{}, fmt.Errorf("invalid anime ID") } anime, err := s.db.GetAnime(ctx, animeID) if err != nil { return database.Anime{}, fmt.Errorf("anime not found: %w", err) } err = s.db.DeleteWatchListEntry(ctx, database.DeleteWatchListEntryParams{ UserID: userID, AnimeID: animeID, }) if err != nil { return database.Anime{}, fmt.Errorf("failed to delete from watchlist: %w", err) } return anime, nil } func (s *Service) GetUserWatchlist(ctx context.Context, userID string) ([]database.GetUserWatchListRow, error) { entries, err := s.db.GetUserWatchList(ctx, userID) if err != nil { return nil, fmt.Errorf("failed to fetch watchlist: %w", err) } return entries, nil } 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"` } // displayTitle returns the best available title func displayTitle(e database.GetUserWatchListRow) string { if e.TitleEnglish.Valid && e.TitleEnglish.String != "" { return e.TitleEnglish.String } if e.TitleJapanese.Valid && e.TitleJapanese.String != "" { return e.TitleJapanese.String } return e.TitleOriginal } 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: displayTitle(entry), 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, }) if err != nil { continue } imported++ } return imported, nil }