cli + tui to publish to leaflet (wip) & manage tasks, notes & watch/read lists 馃崈
charm leaflet readability golang
29
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 719 lines 25 kB view raw
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 != "" }