···5 "io"
6)
78-// MediaHandler defines common operations for media handlers
9-//
10-// This interface captures the shared behavior across media handlers for polymorphic handling of different media types.
11type MediaHandler interface {
12- // SearchAndAdd searches for media and allows user to select and add to queue
13- SearchAndAdd(ctx context.Context, query string, interactive bool) error
14- // List lists all media items with optional status filtering
15- List(ctx context.Context, status string) error
16- // UpdateStatus changes the status of a media item
17- UpdateStatus(ctx context.Context, id, status string) error
18- // Remove removes a media item from the queue
19- Remove(ctx context.Context, id string) error
20- // SetInputReader sets the input reader for interactive prompts
21- SetInputReader(reader io.Reader)
22- // Close cleans up resources
23- Close() error
24}
2526// Searchable defines search behavior for media handlers
···5 "io"
6)
78+// MediaHandler defines common operations for media handlers and captures the shared behavior across media handlers for polymorphic handling of different media types.
009type MediaHandler interface {
10+ SearchAndAdd(ctx context.Context, query string, interactive bool) error // SearchAndAdd searches for media and allows user to select and add to queue
11+ List(ctx context.Context, status string) error // List lists all media items with optional status filtering
12+ UpdateStatus(ctx context.Context, id, status string) error // UpdateStatus changes the status of a media item
13+ Remove(ctx context.Context, id string) error // Remove removes a media item from the queue
14+ SetInputReader(reader io.Reader) // SetInputReader sets the input reader for interactive prompts
15+ Close() error // Close cleans up resources
00000016}
1718// Searchable defines search behavior for media handlers
···4344// Model defines the common interface that all domain models must implement
45type Model interface {
46- // GetID returns the primary key identifier
47- GetID() int64
48- // SetID sets the primary key identifier
49- SetID(id int64)
50- // GetTableName returns the database table name for this model
51- GetTableName() string
52- // GetCreatedAt returns when the model was created
53- GetCreatedAt() time.Time
54- // SetCreatedAt sets when the model was created
55- SetCreatedAt(t time.Time)
56- // GetUpdatedAt returns when the model was last updated
57- GetUpdatedAt() time.Time
58- // SetUpdatedAt sets when the model was last updated
59- SetUpdatedAt(t time.Time)
060}
610000000000000000000000000000000000000000062// Task represents a task item with TaskWarrior-inspired fields
63type Task struct {
64 ID int64 `json:"id"`
···210}
211212// IsCompleted returns true if the task is marked as completed
213-func (t *Task) IsCompleted() bool {
214- return t.Status == "completed"
215-}
216217// IsPending returns true if the task is pending
218-func (t *Task) IsPending() bool {
219- return t.Status == "pending"
220-}
221222// IsDeleted returns true if the task is deleted
223-func (t *Task) IsDeleted() bool {
224- return t.Status == "deleted"
225-}
226227// HasPriority returns true if the task has a priority set
228-func (t *Task) HasPriority() bool {
229- return t.Priority != ""
230-}
231232-// New status tracking methods
233-func (t *Task) IsTodo() bool {
234- return t.Status == StatusTodo
235-}
236-237-func (t *Task) IsInProgress() bool {
238- return t.Status == StatusInProgress
239-}
240-241-func (t *Task) IsBlocked() bool {
242- return t.Status == StatusBlocked
243-}
244-245-func (t *Task) IsDone() bool {
246- return t.Status == StatusDone
247-}
248-249-func (t *Task) IsAbandoned() bool {
250- return t.Status == StatusAbandoned
251-}
252253// IsValidStatus returns true if the status is one of the defined valid statuses
254func (t *Task) IsValidStatus() bool {
···282 return false
283}
284285-// GetPriorityWeight returns a numeric weight for sorting priorities
286-//
287-// Higher numbers = higher priority
288func (t *Task) GetPriorityWeight() int {
289 switch t.Priority {
290 case PriorityHigh, "5":
···524}
525526// HasRating returns true if the album has a rating set
527-func (a *Album) HasRating() bool {
528- return a.Rating > 0
529-}
530531// IsValidRating returns true if the rating is between 1 and 5
532-func (a *Album) IsValidRating() bool {
533- return a.Rating >= 1 && a.Rating <= 5
534-}
535536func (a *Album) GetID() int64 { return a.ID }
537func (a *Album) SetID(id int64) { a.ID = id }
···585}
586587// HasAuthor returns true if the article has an author
588-func (a *Article) HasAuthor() bool {
589- return a.Author != ""
590-}
591592// HasDate returns true if the article has a date
593-func (a *Article) HasDate() bool {
594- return a.Date != ""
595-}
···4344// 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]
58+type Stateful interface {
59+ GetStatus() string
60+ ValidStatuses() []string
61}
6263+// Queueable represents media that can be queued for later consumption
64+//
65+// Implemented by: [Book], [Movie], [TVShow]
66+type 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)
74+type 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)
83+type Progressable interface {
84+ Completable
85+ GetProgress() int
86+ SetProgress(progress int) error
87+}
88+89+// Compile-time interface checks
90+var (
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"`
···252}
253254// IsCompleted returns true if the task is marked as completed
255+func (t *Task) IsCompleted() bool { return t.Status == "completed" }
00256257// IsPending returns true if the task is pending
258+func (t *Task) IsPending() bool { return t.Status == "pending" }
00259260// IsDeleted returns true if the task is deleted
261+func (t *Task) IsDeleted() bool { return t.Status == "deleted" }
00262263// HasPriority returns true if the task has a priority set
264+func (t *Task) HasPriority() bool { return t.Priority != "" }
00265266+func (t *Task) IsTodo() bool { return t.Status == StatusTodo }
267+func (t *Task) IsInProgress() bool { return t.Status == StatusInProgress }
268+func (t *Task) IsBlocked() bool { return t.Status == StatusBlocked }
269+func (t *Task) IsDone() bool { return t.Status == StatusDone }
270+func (t *Task) IsAbandoned() bool { return t.Status == StatusAbandoned }
000000000000000271272// IsValidStatus returns true if the status is one of the defined valid statuses
273func (t *Task) IsValidStatus() bool {
···301 return false
302}
303304+// GetPriorityWeight returns a numeric weight for sorting priorities. A higher number = higher priority
00305func (t *Task) GetPriorityWeight() int {
306 switch t.Priority {
307 case PriorityHigh, "5":
···541}
542543// HasRating returns true if the album has a rating set
544+func (a *Album) HasRating() bool { return a.Rating > 0 }
00545546// IsValidRating returns true if the rating is between 1 and 5
547+func (a *Album) IsValidRating() bool { return a.Rating >= 1 && a.Rating <= 5 }
00548549func (a *Album) GetID() int64 { return a.ID }
550func (a *Album) SetID(id int64) { a.ID = id }
···598}
599600// HasAuthor returns true if the article has an author
601+func (a *Article) HasAuthor() bool { return a.Author != "" }
00602603// HasDate returns true if the article has a date
604+func (a *Article) HasDate() bool { return a.Date != "" }
00
+139-134
internal/repo/article_repository.go
···11 "github.com/stormlightlabs/noteleaf/internal/services"
12)
13000014// ArticleRepository provides database operations for articles
15type ArticleRepository struct {
16 db *sql.DB
···32 Offset int
33}
3435-// Create stores a new article and returns its assigned ID
36-func (r *ArticleRepository) Create(ctx context.Context, article *models.Article) (int64, error) {
37- if err := r.Validate(article); err != nil {
38- return 0, err
39- }
40-41- now := time.Now()
42- article.Created = now
43- article.Modified = now
44-45- query := `
46- INSERT INTO articles (url, title, author, date, markdown_path, html_path, created, modified)
47- VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
48-49- result, err := r.db.ExecContext(ctx, query,
50- article.URL, article.Title, article.Author, article.Date,
51- article.MarkdownPath, article.HTMLPath, article.Created, article.Modified)
52- if err != nil {
53- return 0, fmt.Errorf("failed to insert article: %w", err)
54- }
55-56- id, err := result.LastInsertId()
57- if err != nil {
58- return 0, fmt.Errorf("failed to get last insert id: %w", err)
59- }
60-61- article.ID = id
62- return id, nil
63-}
64-65-// Get retrieves an article by its ID
66-func (r *ArticleRepository) Get(ctx context.Context, id int64) (*models.Article, error) {
67- query := `
68- SELECT id, url, title, author, date, markdown_path, html_path, created, modified
69- FROM articles WHERE id = ?`
70-71- row := r.db.QueryRowContext(ctx, query, id)
72-73 var article models.Article
74- err := row.Scan(&article.ID, &article.URL, &article.Title, &article.Author, &article.Date,
75 &article.MarkdownPath, &article.HTMLPath, &article.Created, &article.Modified)
76 if err != nil {
77- if err == sql.ErrNoRows {
78- return nil, fmt.Errorf("article with id %d not found", id)
79- }
80- return nil, fmt.Errorf("failed to scan article: %w", err)
81 }
82-83 return &article, nil
84}
8586-// GetByURL retrieves an article by its URL
87-func (r *ArticleRepository) GetByURL(ctx context.Context, url string) (*models.Article, error) {
88- query := `
89- SELECT id, url, title, author, date, markdown_path, html_path, created, modified
90- FROM articles WHERE url = ?`
91-92- row := r.db.QueryRowContext(ctx, query, url)
93-94- var article models.Article
95- err := row.Scan(&article.ID, &article.URL, &article.Title, &article.Author, &article.Date,
96- &article.MarkdownPath, &article.HTMLPath, &article.Created, &article.Modified)
97 if err != nil {
98 if err == sql.ErrNoRows {
99- return nil, fmt.Errorf("article with url %s not found", url)
100 }
101 return nil, fmt.Errorf("failed to scan article: %w", err)
102 }
103-104- return &article, nil
105-}
106-107-// Update modifies an existing article
108-func (r *ArticleRepository) Update(ctx context.Context, article *models.Article) error {
109- if err := r.Validate(article); err != nil {
110- return err
111- }
112-113- article.Modified = time.Now()
114-115- query := `
116- UPDATE articles
117- SET title = ?, author = ?, date = ?, markdown_path = ?, html_path = ?, modified = ?
118- WHERE id = ?`
119-120- result, err := r.db.ExecContext(ctx, query,
121- article.Title, article.Author, article.Date, article.MarkdownPath,
122- article.HTMLPath, article.Modified, article.ID)
123- if err != nil {
124- return fmt.Errorf("failed to update article: %w", err)
125- }
126-127- rowsAffected, err := result.RowsAffected()
128- if err != nil {
129- return fmt.Errorf("failed to get rows affected: %w", err)
130- }
131-132- if rowsAffected == 0 {
133- return fmt.Errorf("article with id %d not found", article.ID)
134- }
135-136- return nil
137}
138139-// Delete removes an article from the database
140-func (r *ArticleRepository) Delete(ctx context.Context, id int64) error {
141- query := "DELETE FROM articles WHERE id = ?"
142-143- result, err := r.db.ExecContext(ctx, query, id)
144 if err != nil {
145- return fmt.Errorf("failed to delete article: %w", err)
146 }
0147148- rowsAffected, err := result.RowsAffected()
149- if err != nil {
150- return fmt.Errorf("failed to get rows affected: %w", err)
0000151 }
152153- if rowsAffected == 0 {
154- return fmt.Errorf("article with id %d not found", id)
155 }
156157- return nil
158}
159160-// List retrieves articles with optional filtering
161-func (r *ArticleRepository) List(ctx context.Context, opts *ArticleListOptions) ([]*models.Article, error) {
162- query := `
163- SELECT id, url, title, author, date, markdown_path, html_path, created, modified
164- FROM articles`
165-166 var conditions []string
167 var args []any
168···204 }
205 }
206207- rows, err := r.db.QueryContext(ctx, query, args...)
208- if err != nil {
209- return nil, fmt.Errorf("failed to query articles: %w", err)
210- }
211- defer rows.Close()
212-213- var articles []*models.Article
214- for rows.Next() {
215- var article models.Article
216- err := rows.Scan(&article.ID, &article.URL, &article.Title, &article.Author, &article.Date,
217- &article.MarkdownPath, &article.HTMLPath, &article.Created, &article.Modified)
218- if err != nil {
219- return nil, fmt.Errorf("failed to scan article: %w", err)
220- }
221- articles = append(articles, &article)
222- }
223-224- if err = rows.Err(); err != nil {
225- return nil, fmt.Errorf("error iterating over articles: %w", err)
226- }
227-228- return articles, nil
229}
230231-// Count returns the total number of articles matching the given options
232-func (r *ArticleRepository) Count(ctx context.Context, opts *ArticleListOptions) (int64, error) {
233- query := "SELECT COUNT(*) FROM articles"
234-235 var conditions []string
236 var args []any
237···261 if len(conditions) > 0 {
262 query += " WHERE " + strings.Join(conditions, " AND ")
263 }
0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000264265 var count int64
266 err := r.db.QueryRowContext(ctx, query, args...).Scan(&count)
···11 "github.com/stormlightlabs/noteleaf/internal/services"
12)
1314+func ArticleNotFoundError(id int64) error {
15+ return fmt.Errorf("article with id %d not found", id)
16+}
17+18// ArticleRepository provides database operations for articles
19type ArticleRepository struct {
20 db *sql.DB
···36 Offset int
37}
3839+// scanArticle scans a database row into an Article model
40+func (r *ArticleRepository) scanArticle(s scanner) (*models.Article, error) {
00000000000000000000000000000000000041 var article models.Article
42+ err := s.Scan(&article.ID, &article.URL, &article.Title, &article.Author, &article.Date,
43 &article.MarkdownPath, &article.HTMLPath, &article.Created, &article.Modified)
44 if err != nil {
45+ return nil, err
00046 }
047 return &article, nil
48}
4950+// queryOne executes a query that returns a single article
51+func (r *ArticleRepository) queryOne(ctx context.Context, query string, args ...any) (*models.Article, error) {
52+ row := r.db.QueryRowContext(ctx, query, args...)
53+ article, err := r.scanArticle(row)
000000054 if err != nil {
55 if err == sql.ErrNoRows {
56+ return nil, fmt.Errorf("article not found")
57 }
58 return nil, fmt.Errorf("failed to scan article: %w", err)
59 }
60+ return article, nil
00000000000000000000000000000000061}
6263+// queryMany executes a query that returns multiple articles
64+func (r *ArticleRepository) queryMany(ctx context.Context, query string, args ...any) ([]*models.Article, error) {
65+ rows, err := r.db.QueryContext(ctx, query, args...)
0066 if err != nil {
67+ return nil, fmt.Errorf("failed to query articles: %w", err)
68 }
69+ defer rows.Close()
7071+ var articles []*models.Article
72+ for rows.Next() {
73+ article, err := r.scanArticle(rows)
74+ if err != nil {
75+ return nil, fmt.Errorf("failed to scan article: %w", err)
76+ }
77+ articles = append(articles, article)
78 }
7980+ if err := rows.Err(); err != nil {
81+ return nil, fmt.Errorf("error iterating over articles: %w", err)
82 }
8384+ return articles, nil
85}
8687+// buildListQuery constructs a query and arguments for the List method
88+func (r *ArticleRepository) buildListQuery(opts *ArticleListOptions) (string, []any) {
89+ query := queryArticlesList
00090 var conditions []string
91 var args []any
92···128 }
129 }
130131+ return query, args
000000000000000000000132}
133134+// buildCountQuery constructs a count query and arguments
135+func (r *ArticleRepository) buildCountQuery(opts *ArticleListOptions) (string, []any) {
136+ query := queryArticlesCount
0137 var conditions []string
138 var args []any
139···163 if len(conditions) > 0 {
164 query += " WHERE " + strings.Join(conditions, " AND ")
165 }
166+167+ return query, args
168+}
169+170+// Create stores a new article and returns its assigned ID
171+func (r *ArticleRepository) Create(ctx context.Context, article *models.Article) (int64, error) {
172+ if err := r.Validate(article); err != nil {
173+ return 0, err
174+ }
175+176+ now := time.Now()
177+ article.Created = now
178+ article.Modified = now
179+180+ result, err := r.db.ExecContext(ctx, queryArticleInsert,
181+ article.URL, article.Title, article.Author, article.Date,
182+ article.MarkdownPath, article.HTMLPath, article.Created, article.Modified)
183+ if err != nil {
184+ return 0, fmt.Errorf("failed to insert article: %w", err)
185+ }
186+187+ id, err := result.LastInsertId()
188+ if err != nil {
189+ return 0, fmt.Errorf("failed to get last insert id: %w", err)
190+ }
191+192+ article.ID = id
193+ return id, nil
194+}
195+196+// Get retrieves an article by its ID
197+func (r *ArticleRepository) Get(ctx context.Context, id int64) (*models.Article, error) {
198+ article, err := r.queryOne(ctx, queryArticleByID, id)
199+ if err != nil {
200+ return nil, ArticleNotFoundError(id)
201+ }
202+ return article, nil
203+}
204+205+// GetByURL retrieves an article by its URL
206+func (r *ArticleRepository) GetByURL(ctx context.Context, url string) (*models.Article, error) {
207+ article, err := r.queryOne(ctx, queryArticleByURL, url)
208+ if err != nil {
209+ return nil, fmt.Errorf("article with url %s not found", url)
210+ }
211+ return article, nil
212+}
213+214+// Update modifies an existing article
215+func (r *ArticleRepository) Update(ctx context.Context, article *models.Article) error {
216+ if err := r.Validate(article); err != nil {
217+ return err
218+ }
219+220+ article.Modified = time.Now()
221+222+ result, err := r.db.ExecContext(ctx, queryArticleUpdate,
223+ article.Title, article.Author, article.Date, article.MarkdownPath,
224+ article.HTMLPath, article.Modified, article.ID)
225+ if err != nil {
226+ return fmt.Errorf("failed to update article: %w", err)
227+ }
228+229+ rowsAffected, err := result.RowsAffected()
230+ if err != nil {
231+ return fmt.Errorf("failed to get rows affected: %w", err)
232+ }
233+234+ if rowsAffected == 0 {
235+ return ArticleNotFoundError(article.ID)
236+ }
237+238+ return nil
239+}
240+241+// Delete removes an article from the database
242+func (r *ArticleRepository) Delete(ctx context.Context, id int64) error {
243+ result, err := r.db.ExecContext(ctx, queryArticleDelete, id)
244+ if err != nil {
245+ return fmt.Errorf("failed to delete article: %w", err)
246+ }
247+248+ rowsAffected, err := result.RowsAffected()
249+ if err != nil {
250+ return fmt.Errorf("failed to get rows affected: %w", err)
251+ }
252+253+ if rowsAffected == 0 {
254+ return ArticleNotFoundError(id)
255+ }
256+257+ return nil
258+}
259+260+// List retrieves articles with optional filtering
261+func (r *ArticleRepository) List(ctx context.Context, opts *ArticleListOptions) ([]*models.Article, error) {
262+ query, args := r.buildListQuery(opts)
263+ return r.queryMany(ctx, query, args...)
264+}
265+266+// Count returns the total number of articles matching the given options
267+func (r *ArticleRepository) Count(ctx context.Context, opts *ArticleListOptions) (int64, error) {
268+ query, args := r.buildCountQuery(opts)
269270 var count int64
271 err := r.db.QueryRowContext(ctx, query, args...).Scan(&count)
···9)
1011// MediaConfig defines configuration for a media repository
12-//
13-// T should be a pointer type (*models.Book, *models.Movie, *models.TVShow)
14type MediaConfig[T models.Model] struct {
15 TableName string // TableName is the database table name (e.g., "books", "movies", "tv_shows")
16 New func() T // New creates a new zero-value instance of T
···23}
2425// BaseMediaRepository provides shared CRUD operations for media types
26-//
27-// This generic implementation eliminates duplicate code across Book, Movie, and TV repositories.
28-// Type-specific behavior is configured via MediaConfig.
29-//
30-// T should be a pointer type (*models.Book, *models.Movie, *models.TVShow)
31type BaseMediaRepository[T models.Model] struct {
32 db *sql.DB
33 config MediaConfig[T]
···64}
6566// Get retrieves a media item by ID
67-//
68-// Returns T directly (which is already a pointer type like *models.Book)
69func (r *BaseMediaRepository[T]) Get(ctx context.Context, id int64) (T, error) {
70 query := fmt.Sprintf("SELECT * FROM %s WHERE id = ?", r.config.TableName)
71 row := r.db.QueryRowContext(ctx, query, id)
···109}
110111// ListQuery executes a custom query and scans results
112-//
113-// Returns []T where T is a pointer type (e.g., []*models.Book)
114func (r *BaseMediaRepository[T]) ListQuery(ctx context.Context, query string, args ...any) ([]T, error) {
115 rows, err := r.db.QueryContext(ctx, query, args...)
116 if err != nil {
···140 return count, nil
141}
142143-// buildPlaceholders generates "?,?,?" for SQL placeholders
144func buildPlaceholders(values []any) string {
145 if len(values) == 0 {
146 return ""
···9)
1011// MediaConfig defines configuration for a media repository
0012type MediaConfig[T models.Model] struct {
13 TableName string // TableName is the database table name (e.g., "books", "movies", "tv_shows")
14 New func() T // New creates a new zero-value instance of T
···21}
2223// BaseMediaRepository provides shared CRUD operations for media types
0000024type BaseMediaRepository[T models.Model] struct {
25 db *sql.DB
26 config MediaConfig[T]
···57}
5859// Get retrieves a media item by ID
0060func (r *BaseMediaRepository[T]) Get(ctx context.Context, id int64) (T, error) {
61 query := fmt.Sprintf("SELECT * FROM %s WHERE id = ?", r.config.TableName)
62 row := r.db.QueryRowContext(ctx, query, id)
···100}
101102// ListQuery executes a custom query and scans results
00103func (r *BaseMediaRepository[T]) ListQuery(ctx context.Context, query string, args ...any) ([]T, error) {
104 rows, err := r.db.QueryContext(ctx, query, args...)
105 if err != nil {
···129 return count, nil
130}
1310132func buildPlaceholders(values []any) string {
133 if len(values) == 0 {
134 return ""
···7)
89// MediaRepository defines CRUD operations for media types (Books, Movies, TV)
10-//
11-// This interface captures the shared behavior across media repositories
12type MediaRepository[T models.Model] interface {
13- // Create stores a new media item and returns its assigned ID
14- Create(ctx context.Context, item *T) (int64, error)
15-16- // Get retrieves a media item by ID
17- Get(ctx context.Context, id int64) (*T, error)
18-19- // Update modifies an existing media item
20- Update(ctx context.Context, item *T) error
21-22- // Delete removes a media item by ID
23- Delete(ctx context.Context, id int64) error
24-25- // List retrieves media items with optional filtering and sorting
26- List(ctx context.Context, opts any) ([]*T, error)
27-28- // Count returns the number of media items matching conditions
29- Count(ctx context.Context, opts any) (int64, error)
30}
3132-// StatusFilterable extends MediaRepository with status-based filtering
33-//
34-// Media types (Books, Movies, TV) support status-based queries like "queued", "reading", "watching", "watched", "finished"
35type StatusFilterable[T models.Model] interface {
36 MediaRepository[T]
37-38 // GetByStatus retrieves all items with the given status
39 GetByStatus(ctx context.Context, status string) ([]*T, error)
40}
···7)
89// MediaRepository defines CRUD operations for media types (Books, Movies, TV)
0010type MediaRepository[T models.Model] interface {
11+ Create(ctx context.Context, item *T) (int64, error) // Create stores a new media item and returns its assigned ID
12+ Get(ctx context.Context, id int64) (*T, error) // Get retrieves a media item by ID
13+ Update(ctx context.Context, item *T) error // Update modifies an existing media item
14+ Delete(ctx context.Context, id int64) error // Delete removes a media item by ID
15+ List(ctx context.Context, opts any) ([]*T, error) // List retrieves media items with optional filtering and sorting
16+ Count(ctx context.Context, opts any) (int64, error) // Count returns the number of media items matching conditions
0000000000017}
1819+// StatusFilterable extends MediaRepository with status-based filtering for queries like "queued", "reading", "watching", "watched", "finished"
0020type StatusFilterable[T models.Model] interface {
21 MediaRepository[T]
022 // GetByStatus retrieves all items with the given status
23 GetByStatus(ctx context.Context, status string) ([]*T, error)
24}
+13-16
internal/repo/movie_repository.go
···10 "github.com/stormlightlabs/noteleaf/internal/models"
11)
1200000000000013// MovieRepository provides database operations for movies
14type MovieRepository struct {
15 *BaseMediaRepository[*models.Movie]
···37 },
38 }
3940- return &MovieRepository{
41- BaseMediaRepository: NewBaseMediaRepository(db, config),
42- db: db,
43- }
44}
4546// Create stores a new movie and returns its assigned ID
···211 movie.Watched = &now
212 return r.Update(ctx, movie)
213}
214-215-// MovieListOptions defines options for listing movies
216-type MovieListOptions struct {
217- Status string
218- Year int
219- MinRating float64
220- Search string
221- SortBy string
222- SortOrder string
223- Limit int
224- Offset int
225-}
···10 "github.com/stormlightlabs/noteleaf/internal/models"
11)
1213+// MovieListOptions defines options for listing movies
14+type MovieListOptions struct {
15+ Status string
16+ Year int
17+ MinRating float64
18+ Search string
19+ SortBy string
20+ SortOrder string
21+ Limit int
22+ Offset int
23+}
24+25// MovieRepository provides database operations for movies
26type MovieRepository struct {
27 *BaseMediaRepository[*models.Movie]
···49 },
50 }
5152+ return &MovieRepository{BaseMediaRepository: NewBaseMediaRepository(db, config), db: db}
00053}
5455// Create stores a new movie and returns its assigned ID
···220 movie.Watched = &now
221 return r.Update(ctx, movie)
222}
000000000000