cli + tui to publish to leaflet (wip) & manage tasks, notes & watch/read lists 🍃
charm leaflet readability golang
at main 327 lines 8.2 kB view raw
1package handlers 2 3import ( 4 "context" 5 "fmt" 6 "io" 7 "slices" 8 "strconv" 9 "strings" 10 "time" 11 12 "github.com/stormlightlabs/noteleaf/internal/models" 13 "github.com/stormlightlabs/noteleaf/internal/repo" 14 "github.com/stormlightlabs/noteleaf/internal/services" 15 "github.com/stormlightlabs/noteleaf/internal/store" 16) 17 18// TVHandler handles all TV show-related commands. Implements [MediaHandler] for polymorphic media handling 19type TVHandler struct { 20 db *store.Database 21 config *store.Config 22 repos *repo.Repositories 23 service *services.TVService 24 reader io.Reader 25} 26 27var _ MediaHandler = (*TVHandler)(nil) 28 29// NewTVHandler creates a new TV handler 30func NewTVHandler() (*TVHandler, error) { 31 db, err := store.NewDatabase() 32 if err != nil { 33 return nil, fmt.Errorf("failed to initialize database: %w", err) 34 } 35 36 config, err := store.LoadConfig() 37 if err != nil { 38 return nil, fmt.Errorf("failed to load configuration: %w", err) 39 } 40 41 repos := repo.NewRepositories(db.DB) 42 service := services.NewTVService() 43 44 return &TVHandler{ 45 db: db, 46 config: config, 47 repos: repos, 48 service: service, 49 }, nil 50} 51 52// Close cleans up resources 53func (h *TVHandler) Close() error { 54 if err := h.service.Close(); err != nil { 55 return fmt.Errorf("failed to close service: %w", err) 56 } 57 return h.db.Close() 58} 59 60// SetInputReader sets the input reader 61func (h *TVHandler) SetInputReader(reader io.Reader) { 62 h.reader = reader 63} 64 65// SearchAndAdd searches for TV shows and allows user to select and add to queue 66func (h *TVHandler) SearchAndAdd(ctx context.Context, query string, interactive bool) error { 67 if query == "" { 68 return fmt.Errorf("search query cannot be empty") 69 } 70 71 fmt.Printf("Searching for TV shows: %s\n", query) 72 fmt.Print("Loading...") 73 74 results, err := h.service.Search(ctx, query, 1, 5) 75 if err != nil { 76 fmt.Println(" failed!") 77 return fmt.Errorf("search failed: %w", err) 78 } 79 80 fmt.Println(" done!") 81 fmt.Println() 82 83 if len(results) == 0 { 84 fmt.Println("No TV shows found.") 85 return nil 86 } 87 88 fmt.Printf("Found %d result(s):\n\n", len(results)) 89 for i, result := range results { 90 if show, ok := (*result).(*models.TVShow); ok { 91 fmt.Printf("[%d] %s", i+1, show.Title) 92 if show.Season > 0 { 93 fmt.Printf(" (Season %d)", show.Season) 94 } 95 if show.Rating > 0 { 96 fmt.Printf(" ★%.1f", show.Rating) 97 } 98 if show.Notes != "" { 99 notes := show.Notes 100 if len(notes) > 80 { 101 notes = notes[:77] + "..." 102 } 103 fmt.Printf("\n %s", notes) 104 } 105 fmt.Println() 106 } 107 } 108 109 fmt.Print("\nEnter number to add (1-", len(results), "), or 0 to cancel: ") 110 111 var choice int 112 if h.reader != nil { 113 if _, err := fmt.Fscanf(h.reader, "%d", &choice); err != nil { 114 return fmt.Errorf("invalid input") 115 } 116 } else { 117 if _, err := fmt.Scanf("%d", &choice); err != nil { 118 return fmt.Errorf("invalid input") 119 } 120 } 121 122 if choice == 0 { 123 fmt.Println("Cancelled.") 124 return nil 125 } 126 127 if choice < 1 || choice > len(results) { 128 return fmt.Errorf("invalid choice: %d", choice) 129 } 130 131 selectedShow, ok := (*results[choice-1]).(*models.TVShow) 132 if !ok { 133 return fmt.Errorf("error processing selected TV show") 134 } 135 136 if _, err := h.repos.TV.Create(ctx, selectedShow); err != nil { 137 return fmt.Errorf("failed to add TV show: %w", err) 138 } 139 140 fmt.Printf("✓ Added TV show: %s", selectedShow.Title) 141 if selectedShow.Season > 0 { 142 fmt.Printf(" (Season %d)", selectedShow.Season) 143 } 144 fmt.Println() 145 146 return nil 147} 148 149// List TV shows with status filtering 150func (h *TVHandler) List(ctx context.Context, status string) error { 151 var shows []*models.TVShow 152 var err error 153 154 switch status { 155 case "": 156 shows, err = h.repos.TV.List(ctx, repo.TVListOptions{}) 157 if err != nil { 158 return fmt.Errorf("failed to list TV shows: %w", err) 159 } 160 case "queued": 161 shows, err = h.repos.TV.GetQueued(ctx) 162 if err != nil { 163 return fmt.Errorf("failed to get queued TV shows: %w", err) 164 } 165 case "watching": 166 shows, err = h.repos.TV.GetWatching(ctx) 167 if err != nil { 168 return fmt.Errorf("failed to get watching TV shows: %w", err) 169 } 170 case "watched": 171 shows, err = h.repos.TV.GetWatched(ctx) 172 if err != nil { 173 return fmt.Errorf("failed to get watched TV shows: %w", err) 174 } 175 default: 176 return fmt.Errorf("invalid status: %s (use: queued, watching, watched, or leave empty for all)", status) 177 } 178 179 if len(shows) == 0 { 180 if status == "" { 181 fmt.Println("No TV shows found") 182 } else { 183 fmt.Printf("No %s TV shows found\n", status) 184 } 185 return nil 186 } 187 188 fmt.Printf("Found %d TV show(s):\n\n", len(shows)) 189 for _, show := range shows { 190 h.print(show) 191 } 192 193 return nil 194} 195 196// View displays detailed information about a specific TV show 197func (h *TVHandler) View(ctx context.Context, id string) error { 198 showID, err := strconv.ParseInt(id, 10, 64) 199 if err != nil { 200 return fmt.Errorf("invalid TV show ID: %s", id) 201 } 202 203 show, err := h.repos.TV.Get(ctx, showID) 204 if err != nil { 205 return fmt.Errorf("failed to get TV show %d: %w", showID, err) 206 } 207 208 fmt.Printf("TV Show: %s", show.Title) 209 if show.Season > 0 { 210 fmt.Printf(" (Season %d", show.Season) 211 if show.Episode > 0 { 212 fmt.Printf(", Episode %d", show.Episode) 213 } 214 fmt.Print(")") 215 } 216 fmt.Printf("\nID: %d\n", show.ID) 217 fmt.Printf("Status: %s\n", show.Status) 218 219 if show.Rating > 0 { 220 fmt.Printf("Rating: ★%.1f\n", show.Rating) 221 } 222 223 fmt.Printf("Added: %s\n", show.Added.Format("2006-01-02 15:04:05")) 224 225 if show.LastWatched != nil { 226 fmt.Printf("Last Watched: %s\n", show.LastWatched.Format("2006-01-02 15:04:05")) 227 } 228 229 if show.Notes != "" { 230 fmt.Printf("Notes: %s\n", show.Notes) 231 } 232 233 return nil 234} 235 236// UpdateStatus changes the status of a TV show 237func (h *TVHandler) UpdateStatus(ctx context.Context, id, status string) error { 238 showID, err := strconv.ParseInt(id, 10, 64) 239 if err != nil { 240 return fmt.Errorf("invalid tv show ID %w", err) 241 } 242 validStatuses := []string{"queued", "watching", "watched", "removed"} 243 if !slices.Contains(validStatuses, status) { 244 return fmt.Errorf("invalid status: %s (valid: %s)", status, strings.Join(validStatuses, ", ")) 245 } 246 247 show, err := h.repos.TV.Get(ctx, showID) 248 if err != nil { 249 return fmt.Errorf("TV show %d not found: %w", showID, err) 250 } 251 252 show.Status = status 253 if (status == "watching" || status == "watched") && show.LastWatched == nil { 254 now := time.Now() 255 show.LastWatched = &now 256 } 257 258 if err := h.repos.TV.Update(ctx, show); err != nil { 259 return fmt.Errorf("failed to update TV show status: %w", err) 260 } 261 262 fmt.Printf("✓ TV show '%s' marked as %s\n", show.Title, status) 263 return nil 264} 265 266// MarkWatching marks a TV show as currently watching 267func (h *TVHandler) MarkWatching(ctx context.Context, id string) error { 268 return h.UpdateStatus(ctx, id, "watching") 269} 270 271// MarkWatched marks a TV show as watched 272func (h *TVHandler) MarkWatched(ctx context.Context, id string) error { 273 return h.UpdateStatus(ctx, id, "watched") 274} 275 276// Remove removes a TV show from the queue 277func (h *TVHandler) Remove(ctx context.Context, id string) error { 278 showID, err := strconv.ParseInt(id, 10, 64) 279 if err != nil { 280 return fmt.Errorf("invalid TV show ID: %s", id) 281 } 282 283 show, err := h.repos.TV.Get(ctx, showID) 284 if err != nil { 285 return fmt.Errorf("TV show %d not found: %w", showID, err) 286 } 287 288 if err := h.repos.TV.Delete(ctx, showID); err != nil { 289 return fmt.Errorf("failed to remove TV show: %w", err) 290 } 291 292 fmt.Printf("✓ Removed TV show: %s", show.Title) 293 if show.Season > 0 { 294 fmt.Printf(" (Season %d)", show.Season) 295 } 296 fmt.Println() 297 298 return nil 299} 300 301func (h *TVHandler) print(show *models.TVShow) { 302 fmt.Printf("[%d] %s", show.ID, show.Title) 303 if show.Season > 0 { 304 fmt.Printf(" (Season %d", show.Season) 305 if show.Episode > 0 { 306 fmt.Printf(", Ep %d", show.Episode) 307 } 308 fmt.Print(")") 309 } 310 if show.Status != "queued" { 311 fmt.Printf(" (%s)", show.Status) 312 } 313 if show.Rating > 0 { 314 fmt.Printf(" ★%.1f", show.Rating) 315 } 316 fmt.Println() 317} 318 319// UpdateTVShowStatus changes the status of a TV show 320func (h *TVHandler) UpdateTVShowStatus(ctx context.Context, id, status string) error { 321 return h.UpdateStatus(ctx, id, status) 322} 323 324// MarkTVShowWatching marks a TV show as currently watching 325func (h *TVHandler) MarkTVShowWatching(ctx context.Context, id string) error { 326 return h.MarkWatching(ctx, id) 327}