Something went wrong. Try again.
let's backfill your watched movies on popfeed
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222package main
import ( "context" "fmt" "log/slog" "sort" "strconv" "strings" "time"
tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss")
type tuiRunner interface { Run(stream *tmdbMovieStream, ctx context.Context) (results map[int]Entry, quit bool, err error)}
type realTUIRunner struct{}
func (realTUIRunner) Run(stream *tmdbMovieStream, ctx context.Context) (map[int]Entry, bool, error) { p := tea.NewProgram(newTUIModel(stream, ctx)) result, err := p.Run() if err != nil { return nil, false, fmt.Errorf("TUI: %w", err) } model, ok := result.(tuiModel) if !ok { return nil, false, fmt.Errorf("unexpected TUI result type: %T", result) } return model.results, model.quitting, nil}
var boldStyle = lipgloss.NewStyle().Bold(true)
type moviesMsg []Movie
type fetchErrMsg struct{ err error }
type tuiModel struct { ctx context.Context stream *tmdbMovieStream movies []Movie pos int // -1 until first movie arrives results map[int]Entry fetching bool exhausted bool quitting bool err error}
func newTUIModel(stream *tmdbMovieStream, ctx context.Context) tuiModel { return tuiModel{ ctx: ctx, stream: stream, pos: -1, results: make(map[int]Entry), fetching: true, }}
func (m tuiModel) Init() tea.Cmd { return m.fetchCmd()}
func (m tuiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case moviesMsg: m.fetching = false if len(msg) == 0 { m.exhausted = true if m.pos < 0 { return m, tea.Quit } return m, nil } m.movies = append(m.movies, msg...) if m.pos < 0 { m.pos = 0 } return m, nil
case fetchErrMsg: m.fetching = false m.err = msg.err return m, tea.Quit
case tea.KeyMsg: key := msg.String() if key == "q" || key == "ctrl+c" { m.quitting = true return m, tea.Quit } if m.pos < 0 || m.pos >= len(m.movies) { return m, nil } if key == "b" { if m.pos > 0 { m.pos-- } return m, nil } updated, ok := m.handleAction(key) if !ok { return m, nil } return updated.advanceCmd() } return m, nil}
func (m tuiModel) advanceCmd() (tuiModel, tea.Cmd) { m.pos++
if m.pos >= len(m.movies) { if m.exhausted { return m, tea.Quit } if !m.fetching { m.fetching = true return m, m.fetchCmd() } return m, nil }
if !m.fetching && !m.exhausted && len(m.movies)-m.pos < 10 { m.fetching = true return m, m.fetchCmd() } return m, nil}
func (m tuiModel) fetchCmd() tea.Cmd { return func() tea.Msg { ctx, cancel := context.WithTimeout(m.ctx, 10*time.Second) defer cancel() movies, err := m.stream.FetchNextPage(ctx) if err != nil { return fetchErrMsg{err} } return moviesMsg(movies) }}
func (m tuiModel) handleAction(key string) (tuiModel, bool) { id := m.movies[m.pos].ID
switch key { case "n": m.results[id] = Entry{ID: id, Seen: false} return m, true case "s": m.results[id] = Entry{ID: id, Seen: true} return m, true case "0": m.results[id] = Entry{ID: id, Rating: 10, Seen: true} return m, true default: r, err := strconv.Atoi(key) if err != nil || r < 1 || r > 9 { return m, false } m.results[id] = Entry{ID: id, Rating: r, Seen: true} return m, true }}
func (m tuiModel) View() string { if m.err != nil { return fmt.Sprintf("\n Error: %v\n\n", m.err) } if m.pos >= 0 && m.pos < len(m.movies) { mv := m.movies[m.pos] return fmt.Sprintf( "\n %s (%s)\n %.1f ★ · %s\n\n [0=10,1-9] Rate [s] Seen [n] Skip [b] Back [q] Quit\n %d / %d\n\n", boldStyle.Render(mv.Title), mv.Year, mv.Score, strings.Join(mv.Genres, ", "), m.pos+1, len(m.movies), ) } if m.fetching { return "\n Loading...\n\n" } if m.exhausted { return fmt.Sprintf("\n No more movies · rated %d\n\n", len(m.results)) } return "\n"}
func runViewed(ctx context.Context, tmdb tmdbClient, store Storer, tui tuiRunner, output string) error { skip, err := store.LoadSkipSet(output) if err != nil { return fmt.Errorf("load skip set: %w", err) } stream := newTMDBMovieStream(tmdb, skip)
results, _, err := tui.Run(stream, ctx) if err != nil { return err } if len(results) == 0 { return nil }
newEntries := make([]Entry, 0, len(results)) for _, e := range results { newEntries = append(newEntries, e) } sort.Slice(newEntries, func(i, j int) bool { return newEntries[i].ID < newEntries[j].ID })
existing, err := store.ReadEntries(output) if err != nil { return fmt.Errorf("read existing entries: %w", err) } existing = append(existing, newEntries...) if err := store.WriteEntries(output, existing); err != nil { return fmt.Errorf("write CSV: %w", err) } slog.Info("wrote entries", "count", len(newEntries), "output", output) return nil}