package playback import ( "context" "database/sql" "errors" "fmt" "strings" "github.com/google/uuid" "mal/internal/db" ) // SaveProgress updates watch progress and continue-watching state in a transaction. func (s *Service) SaveProgress(ctx context.Context, userID string, animeID int64, episode int, timeSeconds float64, animeSeed *db.UpsertAnimeParams) error { if strings.TrimSpace(userID) == "" || animeID <= 0 || episode <= 0 { return errors.New("invalid save progress input") } txQueries, tx, err := db.BeginTx(ctx, s.sqlDB) if err != nil { return err } defer tx.Rollback() if animeSeed != nil { if _, err := txQueries.UpsertAnime(ctx, *animeSeed); err != nil { return fmt.Errorf("failed to save anime reference: %w", err) } } watchListEntry, watchListErr := txQueries.GetWatchListEntry(ctx, db.GetWatchListEntryParams{ UserID: userID, AnimeID: animeID, }) if watchListErr != nil && !errors.Is(watchListErr, sql.ErrNoRows) { return fmt.Errorf("failed to load watchlist entry: %w", watchListErr) } isCompleted := watchListErr == nil && watchListEntry.Status == "completed" if !isCompleted { if err := txQueries.SaveWatchProgress(ctx, db.SaveWatchProgressParams{ CurrentEpisode: sql.NullInt64{Int64: int64(episode), Valid: true}, CurrentTimeSeconds: timeSeconds, UserID: userID, AnimeID: animeID, }); err != nil && !errors.Is(err, sql.ErrNoRows) { return fmt.Errorf("failed to save watchlist progress: %w", err) } } if isCompleted { return tx.Commit() } var durationSeconds sql.NullFloat64 if animeSeed != nil { durationSeconds = animeSeed.DurationSeconds } if _, err := txQueries.UpsertContinueWatchingEntry(ctx, db.UpsertContinueWatchingEntryParams{ ID: uuid.New().String(), UserID: userID, AnimeID: animeID, CurrentEpisode: sql.NullInt64{Int64: int64(episode), Valid: true}, CurrentTimeSeconds: timeSeconds, DurationSeconds: durationSeconds, }); err != nil { return fmt.Errorf("failed to upsert continue entry: %w", err) } if err := tx.Commit(); err != nil { return fmt.Errorf("failed to commit save progress transaction: %w", err) } return nil } // CompleteAnime marks an anime as completed in the watchlist and clears continue-watching. func (s *Service) CompleteAnime(ctx context.Context, userID string, animeID int64, episode int, animeSeed *db.UpsertAnimeParams) error { if strings.TrimSpace(userID) == "" || animeID <= 0 || episode <= 0 { return errors.New("invalid complete anime input") } txQueries, tx, err := db.BeginTx(ctx, s.sqlDB) if err != nil { return err } defer tx.Rollback() watchListEntry, watchListErr := txQueries.GetWatchListEntry(ctx, db.GetWatchListEntryParams{ UserID: userID, AnimeID: animeID, }) if watchListErr != nil && !errors.Is(watchListErr, sql.ErrNoRows) { return fmt.Errorf("failed to load watchlist entry: %w", watchListErr) } alreadyCompleted := watchListErr == nil && watchListEntry.Status == "completed" if !alreadyCompleted { if animeSeed != nil { if _, err := txQueries.UpsertAnime(ctx, *animeSeed); err != nil { return fmt.Errorf("failed to save anime reference: %w", err) } } if _, err := txQueries.UpsertWatchListEntry(ctx, db.UpsertWatchListEntryParams{ ID: uuid.New().String(), UserID: userID, AnimeID: animeID, Status: "completed", CurrentEpisode: sql.NullInt64{Int64: 0, Valid: false}, CurrentTimeSeconds: 0, }); err != nil { return fmt.Errorf("failed to mark watchlist as completed: %w", err) } if err := txQueries.SaveWatchProgress(ctx, db.SaveWatchProgressParams{ CurrentEpisode: sql.NullInt64{Int64: 0, Valid: false}, CurrentTimeSeconds: 0, UserID: userID, AnimeID: animeID, }); err != nil && !errors.Is(err, sql.ErrNoRows) { return fmt.Errorf("failed to reset watch progress: %w", err) } } if err := txQueries.DeleteContinueWatchingEntry(ctx, db.DeleteContinueWatchingEntryParams{ UserID: userID, AnimeID: animeID, }); err != nil { return fmt.Errorf("failed to clear continue entry: %w", err) } if err := tx.Commit(); err != nil { return fmt.Errorf("failed to commit complete anime transaction: %w", err) } return nil }