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