Files
tapir/cmd/tapir/list.go
T
mathiasandClaude Opus 4.8 d3498898c0 feat(cli): add read-only list and show subcommands
tapir list — table of stored summaries (date, title|id, channel, AI,
fallback), recent-first. tapir show <video-id> — full summary with
highlights and takeaways. DSN + user id from TAPIR_DB_DSN/TAPIR_USER_ID,
never hardcoded. main.go gains a minimal os.Args[1] dispatcher kept flat
so Worker F's auth/run cases union cleanly at merge.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 20:58:09 +02:00

106 lines
2.4 KiB
Go

package main
import (
"context"
"flag"
"fmt"
"io"
"os"
"strings"
"text/tabwriter"
"gitea.d-ma.be/mathias/tapir/internal/adapters/store"
)
// runList prints the user's stored summaries as a table, most recent first.
// Read-only. Usage: tapir list [-limit N].
func runList(ctx context.Context, args []string) error {
fs := flag.NewFlagSet("list", flag.ContinueOnError)
limit := fs.Int("limit", 50, "maximum number of summaries to show")
if err := fs.Parse(args); err != nil {
return err
}
s, userID, closeFn, err := openStoreFromEnv(ctx)
if err != nil {
return err
}
defer closeFn()
rows, err := s.ListSummaries(ctx, userID, *limit)
if err != nil {
return err
}
return formatList(os.Stdout, rows)
}
// formatList renders rows as an aligned table. Pure: no DB, no env — so the
// column layout and fallbacks are unit-testable without Postgres. Title falls
// back to the video id and Channel to "-" when the videos row is absent.
func formatList(w io.Writer, rows []store.SummaryRow) error {
if len(rows) == 0 {
_, err := fmt.Fprintln(w, "no summaries yet")
return err
}
// Writes to the tabwriter buffer; any error surfaces on Flush.
tw := tabwriter.NewWriter(w, 0, 4, 2, ' ', 0)
_, _ = fmt.Fprintln(tw, "DATE\tTITLE\tCHANNEL\tAI\tFALLBACK")
for _, r := range rows {
_, _ = fmt.Fprintf(tw, "%s\t%s\t%s\t%s\t%s\n",
listDate(r),
truncate(displayTitle(r), 60),
orDash(r.Channel),
orDash(r.AIProvider),
fallbackFlag(r.FallbackUsed),
)
}
return tw.Flush()
}
// listDate prefers the video's publish date; absent that, when the summary was
// created. Date only — the table is for scanning, not timestamps.
func listDate(r store.SummaryRow) string {
t := r.PublishedAt
if t.IsZero() {
t = r.CreatedAt
}
if t.IsZero() {
return "-"
}
return t.Format("2006-01-02")
}
// displayTitle falls back to the video id when no title is stored, so a summary
// produced before the run loop populated videos is still identifiable.
func displayTitle(r store.SummaryRow) string {
if strings.TrimSpace(r.Title) != "" {
return r.Title
}
return r.VideoID
}
func fallbackFlag(used bool) string {
if used {
return "yes"
}
return "no"
}
func orDash(s string) string {
if strings.TrimSpace(s) == "" {
return "-"
}
return s
}
func truncate(s string, max int) string {
if len(s) <= max {
return s
}
if max <= 1 {
return s[:max]
}
return s[:max-1] + "…"
}