cli + tui to publish to leaflet (wip) & manage tasks, notes & watch/read lists 🍃
charm
leaflet
readability
golang
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}