all repos

smutok @ ee96aed409b0f46ac9e2d3f50ed3ee6331ba7bc6

yet another tui rss reader (not abandoned, just paused development)

smutok/internal/store/sqlite_articles.go(view raw)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
package store

import (
	"context"
	"fmt"
	"strings"
)

func (s *Sqlite) UpsertArticle(
	ctx context.Context,
	timestampUsec, feedID, title, content, author, href string,
	publishedAt int,
) error {
	tx, err := s.db.BeginTx(ctx, nil)
	if err != nil {
		return err
	}
	defer tx.Rollback()

	if _, err = tx.ExecContext(ctx,
		`insert or ignore into articles (id, feed_id, title, content, author, href, published_at) values (?, ?, ?, ?, ?, ?, ?)`,
		timestampUsec, feedID, title, content, author, href, publishedAt); err != nil {
		return err
	}

	if _, err = tx.ExecContext(ctx, `insert or ignore into article_statuses (article_id) values (?)`, timestampUsec); err != nil {
		return err
	}

	return tx.Commit()
}

func (s *Sqlite) SyncReadStatus(ctx context.Context, ids []string) error {
	placeholders, args := buildPlaceholdersAndArgs(ids)
	query := fmt.Sprintf(`--sql
	update article_statuses
	set is_read = case when article_id in (%s)
		then false
		else true
	end`, placeholders)

	_, err := s.db.ExecContext(ctx, query, args...)
	return err
}

func (s *Sqlite) SyncStarredStatus(ctx context.Context, ids []string) error {
	placeholders, args := buildPlaceholdersAndArgs(ids)
	query := fmt.Sprintf(`--sql
	update article_statuses
	set is_starred = case when article_id in (%s)
		then true
		else false
	end`, placeholders)

	_, err := s.db.ExecContext(ctx, query, args...)
	return err
}

type ArticleKind int

const (
	ArticleStarred ArticleKind = iota
	ArticleUnread
	ArticleAll
)

type Article struct {
	ID          string
	Title       string
	Href        string
	Content     string
	Author      string
	IsRead      bool
	IsStarred   bool
	FeedID      string
	FeedTitle   string
	PublishedAt int64
}

var getArticlesWhereClause = map[ArticleKind]string{
	ArticleAll:     "",
	ArticleStarred: "where s.is_starred = true",
	ArticleUnread:  "where s.is_read = false",
}

func (s *Sqlite) GetArticles(ctx context.Context, kind ArticleKind) ([]Article, error) {
	query := fmt.Sprintf(`--sql
	select a.id, a.title, a.href, a.content, a.author, s.is_read, s.is_starred, a.feed_id, f.title feed_name, a.published_at
	from articles a
	join article_statuses s on a.id = s.article_id
	join feeds f on f.id = a.feed_id
	%s
	order by a.published_at desc`, getArticlesWhereClause[kind])

	rows, err := s.db.QueryContext(ctx, query)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	var res []Article
	for rows.Next() {
		var a Article
		if serr := rows.Scan(&a.ID, &a.Title, &a.Href, &a.Content, &a.Author, &a.IsRead, &a.IsStarred, &a.FeedID, &a.FeedTitle, &a.PublishedAt); serr != nil {
			return res, serr
		}

		res = append(res, a)
	}

	if err = rows.Err(); err != nil {
		return res, err
	}

	return res, nil
}

func buildPlaceholdersAndArgs(in []string, prefixArgs ...any) (placeholders string, args []any) {
	placeholders = strings.Repeat("?,", len(in))
	placeholders = placeholders[:len(placeholders)-1] // trim trailing comma

	args = make([]any, len(prefixArgs)+len(in))
	copy(args, prefixArgs)
	for i, v := range in {
		args[len(prefixArgs)+i] = v
	}

	return placeholders, args
}