Infra ADR-0004 renamed the Gitea host. Bulk replace across go.mod and all .go import paths. Build and tests pass unchanged. Closes #20 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dt6aHEDWRjkK14Voi6HnGh
106 lines
2.4 KiB
Go
106 lines
2.4 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"strings"
|
|
"text/tabwriter"
|
|
|
|
"git.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] + "…"
|
|
}
|