cli + tui to publish to leaflet (wip) & manage tasks, notes & watch/read lists 馃崈
charm
leaflet
readability
golang
1package models
2
3import (
4 "encoding/json"
5 "fmt"
6 "net/url"
7 "slices"
8 "time"
9)
10
11type TaskStatus string
12type TaskPriority string
13type TaskWeight int
14
15// TODO: Use [TaskStatus]
16const (
17 StatusTodo = "todo"
18 StatusInProgress = "in-progress"
19 StatusBlocked = "blocked"
20 StatusDone = "done"
21 StatusAbandoned = "abandoned"
22 StatusPending = "pending"
23 StatusCompleted = "completed"
24 StatusDeleted = "deleted"
25)
26
27// TODO: Use [TaskPriority]
28const (
29 PriorityHigh = "High"
30 PriorityMedium = "Medium"
31 PriorityLow = "Low"
32)
33
34// TODO: Use [TaskWeight]
35const (
36 PriorityNumericMin = 1
37 PriorityNumericMax = 5
38)
39
40// RRule represents a recurrence rule (RFC 5545).
41// Example: "FREQ=DAILY;INTERVAL=1" or "FREQ=WEEKLY;BYDAY=MO,WE,FR".
42type RRule string
43
44// Model defines the common interface that all domain models must implement
45type Model interface {
46 GetID() int64 // GetID returns the primary key identifier
47 SetID(id int64) // SetID sets the primary key identifier
48 GetTableName() string // GetTableName returns the database table name for this model
49 GetCreatedAt() time.Time // GetCreatedAt returns when the model was created
50 SetCreatedAt(t time.Time) // SetCreatedAt sets when the model was created
51 GetUpdatedAt() time.Time // GetUpdatedAt returns when the model was last updated
52 SetUpdatedAt(t time.Time) // SetUpdatedAt sets when the model was last updated
53}
54
55// Stateful represents entities with status management behavior
56//
57// Implemented by: [Book], [Movie], [TVShow], [Task]
58type Stateful interface {
59 GetStatus() string
60 ValidStatuses() []string
61}
62
63// Queueable represents media that can be queued for later consumption
64//
65// Implemented by: [Book], [Movie], [TVShow]
66type Queueable interface {
67 Stateful
68 IsQueued() bool
69}
70
71// Completable represents media that can be marked as completed/finished/watched. It tracks completion timestamps for media consumption.
72//
73// Implemented by: [Book] (finished), [Movie] (watched), [TVShow] (watched)
74type Completable interface {
75 Stateful
76 IsCompleted() bool
77 GetCompletionTime() *time.Time
78}
79
80// Progressable represents media with measurable progress tracking
81//
82// Implemented by: [Book] (percentage-based reading progress)
83type Progressable interface {
84 Completable
85 GetProgress() int
86 SetProgress(progress int) error
87}
88
89// Compile-time interface checks
90var (
91 _ Stateful = (*Task)(nil)
92 _ Stateful = (*Book)(nil)
93 _ Stateful = (*Movie)(nil)
94 _ Stateful = (*TVShow)(nil)
95 _ Queueable = (*Book)(nil)
96 _ Queueable = (*Movie)(nil)
97 _ Queueable = (*TVShow)(nil)
98 _ Completable = (*Book)(nil)
99 _ Completable = (*Movie)(nil)
100 _ Completable = (*TVShow)(nil)
101 _ Progressable = (*Book)(nil)
102)
103
104// Task represents a task item with TaskWarrior-inspired fields
105type Task struct {
106 ID int64 `json:"id"`
107 UUID string `json:"uuid"`
108 Description string `json:"description"`
109 Status string `json:"status"` // pending, completed, deleted
110 Priority string `json:"priority,omitempty"` // A-Z or empty
111 Project string `json:"project,omitempty"`
112 Context string `json:"context,omitempty"`
113 Tags []string `json:"tags,omitempty"`
114 Due *time.Time `json:"due,omitempty"`
115 Wait *time.Time `json:"wait,omitempty"` // Task is not actionable until this date
116 Scheduled *time.Time `json:"scheduled,omitempty"` // Task is scheduled to start on this date
117 Entry time.Time `json:"entry"`
118 Modified time.Time `json:"modified"`
119 End *time.Time `json:"end,omitempty"` // Completion time
120 Start *time.Time `json:"start,omitempty"` // When the task was started
121 Annotations []string `json:"annotations,omitempty"`
122 Recur RRule `json:"recur,omitempty"`
123 Until *time.Time `json:"until,omitempty"` // End date for recurrence
124 ParentUUID *string `json:"parent_uuid,omitempty"` // ID of parent/template task
125 DependsOn []string `json:"depends_on,omitempty"` // IDs of tasks this task depends on
126}
127
128// Movie represents a movie in the watch queue
129type Movie struct {
130 ID int64 `json:"id"`
131 Title string `json:"title"`
132 Year int `json:"year,omitempty"`
133 Status string `json:"status"` // queued, watched, removed
134 Rating float64 `json:"rating,omitempty"`
135 Notes string `json:"notes,omitempty"`
136 Added time.Time `json:"added"`
137 Watched *time.Time `json:"watched,omitempty"`
138}
139
140// TVShow represents a TV show in the watch queue
141type TVShow struct {
142 ID int64 `json:"id"`
143 Title string `json:"title"`
144 Season int `json:"season,omitempty"`
145 Episode int `json:"episode,omitempty"`
146 Status string `json:"status"` // queued, watching, watched, removed
147 Rating float64 `json:"rating,omitempty"`
148 Notes string `json:"notes,omitempty"`
149 Added time.Time `json:"added"`
150 LastWatched *time.Time `json:"last_watched,omitempty"`
151}
152
153// Book represents a book in the reading list
154type Book struct {
155 ID int64 `json:"id"`
156 Title string `json:"title"`
157 Author string `json:"author,omitempty"`
158 Status string `json:"status"` // queued, reading, finished, removed
159 Progress int `json:"progress"` // percentage 0-100
160 Pages int `json:"pages,omitempty"`
161 Rating float64 `json:"rating,omitempty"`
162 Notes string `json:"notes,omitempty"`
163 Added time.Time `json:"added"`
164 Started *time.Time `json:"started,omitempty"`
165 Finished *time.Time `json:"finished,omitempty"`
166}
167
168// Note represents a markdown note
169type Note struct {
170 ID int64 `json:"id"`
171 Title string `json:"title"`
172 Content string `json:"content"`
173 Tags []string `json:"tags,omitempty"`
174 Archived bool `json:"archived"`
175 Created time.Time `json:"created"`
176 Modified time.Time `json:"modified"`
177 FilePath string `json:"file_path,omitempty"`
178 LeafletRKey *string `json:"leaflet_rkey,omitempty"` // Leaflet record key
179 LeafletCID *string `json:"leaflet_cid,omitempty"` // Leaflet content identifier
180 PublishedAt *time.Time `json:"published_at,omitempty"` // Publication timestamp
181 IsDraft bool `json:"is_draft"` // Draft vs published status
182}
183
184// Album represents a music album
185type Album struct {
186 ID int64 `json:"id"`
187 Title string `json:"title"`
188 Artist string `json:"artist"`
189 Genre string `json:"genre,omitempty"`
190 ReleaseYear int `json:"release_year,omitempty"`
191 Tracks []string `json:"tracks,omitempty"`
192 DurationSeconds int `json:"duration_seconds,omitempty"`
193 AlbumArtPath string `json:"album_art_path,omitempty"`
194 Rating int `json:"rating,omitempty"`
195 Created time.Time `json:"created"`
196 Modified time.Time `json:"modified"`
197}
198
199// TimeEntry represents a time tracking entry for a task
200type TimeEntry struct {
201 ID int64 `json:"id"`
202 TaskID int64 `json:"task_id"`
203 StartTime time.Time `json:"start_time"`
204 EndTime *time.Time `json:"end_time,omitempty"`
205 DurationSeconds int64 `json:"duration_seconds,omitempty"`
206 Description string `json:"description,omitempty"`
207 Created time.Time `json:"created"`
208 Modified time.Time `json:"modified"`
209}
210
211// Article represents a parsed article from a web URL
212type Article struct {
213 ID int64 `json:"id"`
214 URL string `json:"url"`
215 Title string `json:"title"`
216 Author string `json:"author,omitempty"`
217 Date string `json:"date,omitempty"`
218 MarkdownPath string `json:"markdown_path"`
219 HTMLPath string `json:"html_path"`
220 Created time.Time `json:"created"`
221 Modified time.Time `json:"modified"`
222}
223
224// TaskHistory represents a historical snapshot of a task for undo functionality
225type TaskHistory struct {
226 ID int64 `json:"id"`
227 TaskID int64 `json:"task_id"`
228 Operation string `json:"operation"` // update, delete
229 Snapshot string `json:"snapshot"` // JSON snapshot of task
230 CreatedAt time.Time `json:"created_at"`
231}
232
233// MarshalTags converts tags slice to JSON string for database storage
234func (t *Task) MarshalTags() (string, error) {
235 if len(t.Tags) == 0 {
236 return "", nil
237 }
238 data, err := json.Marshal(t.Tags)
239 return string(data), err
240}
241
242// UnmarshalTags converts JSON string from database to tags slice
243func (t *Task) UnmarshalTags(data string) error {
244 if data == "" {
245 t.Tags = nil
246 return nil
247 }
248 return json.Unmarshal([]byte(data), &t.Tags)
249}
250
251// MarshalAnnotations converts annotations slice to JSON string for database storage
252func (t *Task) MarshalAnnotations() (string, error) {
253 if len(t.Annotations) == 0 {
254 return "", nil
255 }
256 data, err := json.Marshal(t.Annotations)
257 return string(data), err
258}
259
260// UnmarshalAnnotations converts JSON string from database to annotations slice
261func (t *Task) UnmarshalAnnotations(data string) error {
262 if data == "" {
263 t.Annotations = nil
264 return nil
265 }
266 return json.Unmarshal([]byte(data), &t.Annotations)
267}
268
269// IsCompleted returns true if the task is marked as completed
270func (t *Task) IsCompleted() bool { return t.Status == "completed" }
271
272// IsPending returns true if the task is pending
273func (t *Task) IsPending() bool { return t.Status == "pending" }
274
275// IsDeleted returns true if the task is deleted
276func (t *Task) IsDeleted() bool { return t.Status == "deleted" }
277
278// HasPriority returns true if the task has a priority set
279func (t *Task) HasPriority() bool { return t.Priority != "" }
280func (t *Task) IsTodo() bool { return t.Status == StatusTodo }
281func (t *Task) IsInProgress() bool { return t.Status == StatusInProgress }
282func (t *Task) IsBlocked() bool { return t.Status == StatusBlocked }
283func (t *Task) IsDone() bool { return t.Status == StatusDone }
284func (t *Task) IsAbandoned() bool { return t.Status == StatusAbandoned }
285
286// IsValidStatus returns true if the status is one of the defined valid statuses
287func (t *Task) IsValidStatus() bool {
288 validStatuses := []string{
289 StatusTodo, StatusInProgress, StatusBlocked, StatusDone, StatusAbandoned,
290 StatusPending, StatusCompleted, StatusDeleted, // legacy support
291 }
292 return slices.Contains(validStatuses, t.Status)
293}
294
295// IsValidPriority returns true if the priority is valid (text-based or numeric string)
296func (t *Task) IsValidPriority() bool {
297 if t.Priority == "" {
298 return true
299 }
300
301 textPriorities := []string{PriorityHigh, PriorityMedium, PriorityLow}
302 if slices.Contains(textPriorities, t.Priority) {
303 return true
304 }
305
306 if len(t.Priority) == 1 && t.Priority >= "A" && t.Priority <= "Z" {
307 return true
308 }
309
310 switch t.Priority {
311 case "1", "2", "3", "4", "5":
312 return true
313 }
314
315 return false
316}
317
318// GetPriorityWeight returns a numeric weight for sorting priorities. A higher number = higher priority
319func (t *Task) GetPriorityWeight() int {
320 switch t.Priority {
321 case PriorityHigh, "5":
322 return 5
323 case PriorityMedium, "4":
324 return 4
325 case PriorityLow, "3":
326 return 3
327 case "2":
328 return 2
329 case "1":
330 return 1
331 case "A":
332 return 26
333 case "B":
334 return 25
335 case "C":
336 return 24
337 default:
338 if len(t.Priority) == 1 && t.Priority >= "A" && t.Priority <= "Z" {
339 return int('Z' - t.Priority[0] + 1)
340 }
341 return 0
342 }
343}
344
345// IsStarted returns true if the task has a start time set.
346func (t *Task) IsStarted() bool { return t.Start != nil }
347
348// IsOverdue returns true if the task is overdue.
349func (t *Task) IsOverdue(now time.Time) bool {
350 return t.Due != nil && now.After(*t.Due) && !t.IsCompleted()
351}
352
353// HasDueDate returns true if the task has a due date set.
354func (t *Task) HasDueDate() bool { return t.Due != nil }
355
356// IsWaiting returns true if the task has a wait date and it hasn't passed yet.
357func (t *Task) IsWaiting(now time.Time) bool {
358 return t.Wait != nil && now.Before(*t.Wait)
359}
360
361// HasWaitDate returns true if the task has a wait date set.
362func (t *Task) HasWaitDate() bool { return t.Wait != nil }
363
364// IsScheduled returns true if the task has a scheduled date.
365func (t *Task) IsScheduled() bool { return t.Scheduled != nil }
366
367// IsActionable returns true if the task can be worked on now.
368// A task is actionable if it's not waiting, not blocked, and not completed.
369func (t *Task) IsActionable(now time.Time) bool {
370 if t.IsCompleted() || t.IsDone() || t.IsAbandoned() || t.IsBlocked() {
371 return false
372 }
373 if t.IsWaiting(now) {
374 return false
375 }
376 return true
377}
378
379// IsRecurring returns true if the task has recurrence defined.
380func (t *Task) IsRecurring() bool { return t.Recur != "" }
381
382// IsRecurExpired checks if the recurrence has an end (until) date and is past it.
383func (t *Task) IsRecurExpired(now time.Time) bool {
384 return t.Until != nil && now.After(*t.Until)
385}
386
387// HasDependencies returns true if the task depends on other tasks.
388func (t *Task) HasDependencies() bool { return len(t.DependsOn) > 0 }
389
390// Blocks checks if this task blocks another given task.
391func (t *Task) Blocks(other *Task) bool {
392 return slices.Contains(other.DependsOn, t.UUID)
393}
394
395// Urgency computes a comprehensive score based on multiple factors.
396// Higher score means more urgent. Score components:
397// - Priority: 0-10 based on priority weight
398// - Due date: 0-12 based on proximity (overdue gets highest)
399// - Scheduled: 0-4 if scheduled soon
400// - Age: 0-2 for old tasks
401// - Tags: 0.5 per tag (capped at 2.0)
402// - Waiting: -5.0 if not yet actionable
403// - Blocked: -3.0 if has incomplete dependencies
404func (t *Task) Urgency(now time.Time) float64 {
405 if !t.IsActionable(now) {
406 if t.IsWaiting(now) {
407 return -5.0
408 }
409 if t.IsBlocked() {
410 return -3.0
411 }
412 return -10.0
413 }
414
415 score := 0.0
416
417 if t.HasPriority() {
418 weight := t.GetPriorityWeight()
419 if weight >= 20 {
420 score += float64(weight-15) / 2.0
421 } else if weight > 0 {
422 score += float64(weight) * 2.0
423 }
424 }
425
426 if t.HasDueDate() {
427 daysUntilDue := t.Due.Sub(now).Hours() / 24.0
428 if daysUntilDue < 0 {
429 overdueDays := -daysUntilDue
430 score += 12.0 + min(overdueDays*0.5, 3.0)
431 } else if daysUntilDue <= 1 {
432 score += 10.0
433 } else if daysUntilDue <= 3 {
434 score += 8.0
435 } else if daysUntilDue <= 7 {
436 score += 6.0
437 } else if daysUntilDue <= 14 {
438 score += 4.0
439 } else if daysUntilDue <= 30 {
440 score += 2.0
441 }
442 }
443
444 if t.IsScheduled() {
445 daysUntilScheduled := t.Scheduled.Sub(now).Hours() / 24.0
446 if daysUntilScheduled <= 0 {
447 score += 4.0
448 } else if daysUntilScheduled <= 1 {
449 score += 3.0
450 } else if daysUntilScheduled <= 3 {
451 score += 2.0
452 } else if daysUntilScheduled <= 7 {
453 score += 1.0
454 }
455 }
456
457 age := now.Sub(t.Entry).Hours() / 24.0
458 if age > 90 {
459 score += 2.0
460 } else if age > 30 {
461 score += 1.5
462 } else if age > 14 {
463 score += 1.0
464 } else if age > 7 {
465 score += 0.5
466 }
467
468 if len(t.Tags) > 0 {
469 score += min(float64(len(t.Tags))*0.5, 2.0)
470 }
471
472 if t.Project != "" {
473 score += 0.5
474 }
475
476 return score
477}
478
479// GetStatus returns the current status of the task
480func (t *Task) GetStatus() string { return t.Status }
481
482// ValidStatuses returns all valid status values for a task
483func (t *Task) ValidStatuses() []string {
484 return []string{
485 StatusTodo, StatusInProgress, StatusBlocked, StatusDone, StatusAbandoned,
486 StatusPending, StatusCompleted, StatusDeleted,
487 }
488}
489
490// IsWatched returns true if the movie has been watched
491func (m *Movie) IsWatched() bool { return m.Status == "watched" }
492
493// IsQueued returns true if the movie is in the queue
494func (m *Movie) IsQueued() bool { return m.Status == "queued" }
495
496// GetStatus returns the current status of the movie
497func (m *Movie) GetStatus() string { return m.Status }
498
499// ValidStatuses returns all valid status values for a movie
500func (m *Movie) ValidStatuses() []string { return []string{"queued", "watched", "removed"} }
501
502// IsCompleted returns true if the movie has been watched
503func (m *Movie) IsCompleted() bool { return m.Status == "watched" }
504
505// GetCompletionTime returns when the movie was watched
506func (m *Movie) GetCompletionTime() *time.Time { return m.Watched }
507
508// IsWatching returns true if the TV show is currently being watched
509func (tv *TVShow) IsWatching() bool { return tv.Status == "watching" }
510
511// IsWatched returns true if the TV show has been watched
512func (tv *TVShow) IsWatched() bool { return tv.Status == "watched" }
513
514// IsQueued returns true if the TV show is in the queue
515func (tv *TVShow) IsQueued() bool { return tv.Status == "queued" }
516
517// GetStatus returns the current status of the TV show
518func (tv *TVShow) GetStatus() string { return tv.Status }
519
520// ValidStatuses returns all valid status values for a TV show
521func (tv *TVShow) ValidStatuses() []string {
522 return []string{"queued", "watching", "watched", "removed"}
523}
524
525// IsCompleted returns true if the TV show has been watched
526func (tv *TVShow) IsCompleted() bool { return tv.Status == "watched" }
527
528// GetCompletionTime returns when the TV show was last watched
529func (tv *TVShow) GetCompletionTime() *time.Time { return tv.LastWatched }
530
531// IsReading returns true if the book is currently being read
532func (b *Book) IsReading() bool { return b.Status == "reading" }
533
534// IsFinished returns true if the book has been finished
535func (b *Book) IsFinished() bool { return b.Status == "finished" }
536
537// IsQueued returns true if the book is in the queue
538func (b *Book) IsQueued() bool { return b.Status == "queued" }
539
540// ProgressPercent returns the reading progress as a percentage
541func (b *Book) ProgressPercent() int { return b.Progress }
542
543// GetStatus returns the current status of the book
544func (b *Book) GetStatus() string { return b.Status }
545
546// ValidStatuses returns all valid status values for a book
547func (b *Book) ValidStatuses() []string { return []string{"queued", "reading", "finished", "removed"} }
548
549// IsCompleted returns true if the book has been finished
550func (b *Book) IsCompleted() bool { return b.Status == "finished" }
551
552// GetCompletionTime returns when the book was finished
553func (b *Book) GetCompletionTime() *time.Time { return b.Finished }
554
555// GetProgress returns the reading progress percentage (0-100)
556func (b *Book) GetProgress() int { return b.Progress }
557
558// SetProgress sets the reading progress percentage (0-100)
559func (b *Book) SetProgress(progress int) error {
560 if progress < 0 || progress > 100 {
561 return fmt.Errorf("progress must be between 0 and 100, got %d", progress)
562 }
563 b.Progress = progress
564 return nil
565}
566
567func (t *Task) GetID() int64 { return t.ID }
568func (t *Task) SetID(id int64) { t.ID = id }
569func (t *Task) GetTableName() string { return "tasks" }
570func (t *Task) GetCreatedAt() time.Time { return t.Entry }
571func (t *Task) SetCreatedAt(time time.Time) { t.Entry = time }
572func (t *Task) GetUpdatedAt() time.Time { return t.Modified }
573func (t *Task) SetUpdatedAt(time time.Time) { t.Modified = time }
574
575func (m *Movie) GetID() int64 { return m.ID }
576func (m *Movie) SetID(id int64) { m.ID = id }
577func (m *Movie) GetTableName() string { return "movies" }
578func (m *Movie) GetCreatedAt() time.Time { return m.Added }
579func (m *Movie) SetCreatedAt(time time.Time) { m.Added = time }
580func (m *Movie) GetUpdatedAt() time.Time { return m.Added }
581func (m *Movie) SetUpdatedAt(time time.Time) { m.Added = time }
582
583func (tv *TVShow) GetID() int64 { return tv.ID }
584func (tv *TVShow) SetID(id int64) { tv.ID = id }
585func (tv *TVShow) GetTableName() string { return "tv_shows" }
586func (tv *TVShow) GetCreatedAt() time.Time { return tv.Added }
587func (tv *TVShow) SetCreatedAt(time time.Time) { tv.Added = time }
588func (tv *TVShow) GetUpdatedAt() time.Time { return tv.Added }
589func (tv *TVShow) SetUpdatedAt(time time.Time) { tv.Added = time }
590
591func (b *Book) GetID() int64 { return b.ID }
592func (b *Book) SetID(id int64) { b.ID = id }
593func (b *Book) GetTableName() string { return "books" }
594func (b *Book) GetCreatedAt() time.Time { return b.Added }
595func (b *Book) SetCreatedAt(time time.Time) { b.Added = time }
596func (b *Book) GetUpdatedAt() time.Time { return b.Added }
597func (b *Book) SetUpdatedAt(time time.Time) { b.Added = time }
598
599// MarshalTags converts tags slice to JSON string for database storage
600func (n *Note) MarshalTags() (string, error) {
601 if len(n.Tags) == 0 {
602 return "", nil
603 }
604 data, err := json.Marshal(n.Tags)
605 return string(data), err
606}
607
608// UnmarshalTags converts JSON string from database to tags slice
609func (n *Note) UnmarshalTags(data string) error {
610 if data == "" {
611 n.Tags = nil
612 return nil
613 }
614 return json.Unmarshal([]byte(data), &n.Tags)
615}
616
617// IsArchived returns true if the note is archived
618func (n *Note) IsArchived() bool {
619 return n.Archived
620}
621
622// HasLeafletAssociation returns true if the note is associated with a leaflet document
623func (n *Note) HasLeafletAssociation() bool {
624 return n.LeafletRKey != nil
625}
626
627// IsPublished returns true if the note is published on leaflet (not a draft)
628func (n *Note) IsPublished() bool {
629 return n.HasLeafletAssociation() && !n.IsDraft
630}
631
632func (n *Note) GetID() int64 { return n.ID }
633func (n *Note) SetID(id int64) { n.ID = id }
634func (n *Note) GetTableName() string { return "notes" }
635func (n *Note) GetCreatedAt() time.Time { return n.Created }
636func (n *Note) SetCreatedAt(time time.Time) { n.Created = time }
637func (n *Note) GetUpdatedAt() time.Time { return n.Modified }
638func (n *Note) SetUpdatedAt(time time.Time) { n.Modified = time }
639
640// MarshalTracks converts tracks slice to JSON string for database storage
641func (a *Album) MarshalTracks() (string, error) {
642 if len(a.Tracks) == 0 {
643 return "", nil
644 }
645 data, err := json.Marshal(a.Tracks)
646 return string(data), err
647}
648
649// UnmarshalTracks converts JSON string from database to tracks slice
650func (a *Album) UnmarshalTracks(data string) error {
651 if data == "" {
652 a.Tracks = nil
653 return nil
654 }
655 return json.Unmarshal([]byte(data), &a.Tracks)
656}
657
658// HasRating returns true if the album has a rating set
659func (a *Album) HasRating() bool { return a.Rating > 0 }
660
661// IsValidRating returns true if the rating is between 1 and 5
662func (a *Album) IsValidRating() bool { return a.Rating >= 1 && a.Rating <= 5 }
663
664func (a *Album) GetID() int64 { return a.ID }
665func (a *Album) SetID(id int64) { a.ID = id }
666func (a *Album) GetTableName() string { return "albums" }
667func (a *Album) GetCreatedAt() time.Time { return a.Created }
668func (a *Album) SetCreatedAt(time time.Time) { a.Created = time }
669func (a *Album) GetUpdatedAt() time.Time { return a.Modified }
670func (a *Album) SetUpdatedAt(time time.Time) { a.Modified = time }
671
672// IsActive returns true if the time entry is currently active (not stopped)
673func (te *TimeEntry) IsActive() bool {
674 return te.EndTime == nil
675}
676
677// Stop stops the time entry and calculates duration
678func (te *TimeEntry) Stop() {
679 now := time.Now()
680 te.EndTime = &now
681 te.DurationSeconds = int64(now.Sub(te.StartTime).Seconds())
682 te.Modified = now
683}
684
685// GetDuration returns the duration of the time entry
686func (te *TimeEntry) GetDuration() time.Duration {
687 if te.EndTime != nil {
688 return time.Duration(te.DurationSeconds) * time.Second
689 }
690 return time.Since(te.StartTime)
691}
692
693func (te *TimeEntry) GetID() int64 { return te.ID }
694func (te *TimeEntry) SetID(id int64) { te.ID = id }
695func (te *TimeEntry) GetTableName() string { return "time_entries" }
696func (te *TimeEntry) GetCreatedAt() time.Time { return te.Created }
697func (te *TimeEntry) SetCreatedAt(time time.Time) { te.Created = time }
698func (te *TimeEntry) GetUpdatedAt() time.Time { return te.Modified }
699func (te *TimeEntry) SetUpdatedAt(time time.Time) { te.Modified = time }
700
701func (a *Article) GetID() int64 { return a.ID }
702func (a *Article) SetID(id int64) { a.ID = id }
703func (a *Article) GetTableName() string { return "articles" }
704func (a *Article) GetCreatedAt() time.Time { return a.Created }
705func (a *Article) SetCreatedAt(time time.Time) { a.Created = time }
706func (a *Article) GetUpdatedAt() time.Time { return a.Modified }
707func (a *Article) SetUpdatedAt(time time.Time) { a.Modified = time }
708
709// IsValidURL returns true if the article has parseable URL
710func (a *Article) IsValidURL() bool {
711 _, err := url.ParseRequestURI(a.URL)
712 return err == nil
713}
714
715// HasAuthor returns true if the article has an author
716func (a *Article) HasAuthor() bool { return a.Author != "" }
717
718// HasDate returns true if the article has a date
719func (a *Article) HasDate() bool { return a.Date != "" }