mirror of
https://github.com/nkanaev/yarr.git
synced 2025-05-24 00:33:14 +00:00
337 lines
7.1 KiB
Go
337 lines
7.1 KiB
Go
package storage
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/nkanaev/yarr/src/content/htmlutil"
|
|
)
|
|
|
|
type ItemStatus int
|
|
|
|
const (
|
|
UNREAD ItemStatus = 0
|
|
READ ItemStatus = 1
|
|
STARRED ItemStatus = 2
|
|
)
|
|
|
|
var StatusRepresentations = map[ItemStatus]string{
|
|
UNREAD: "unread",
|
|
READ: "read",
|
|
STARRED: "starred",
|
|
}
|
|
|
|
var StatusValues = map[string]ItemStatus{
|
|
"unread": UNREAD,
|
|
"read": READ,
|
|
"starred": STARRED,
|
|
}
|
|
|
|
func (s ItemStatus) MarshalJSON() ([]byte, error) {
|
|
return json.Marshal(StatusRepresentations[s])
|
|
}
|
|
|
|
func (s *ItemStatus) UnmarshalJSON(b []byte) error {
|
|
var str string
|
|
if err := json.Unmarshal(b, &str); err != nil {
|
|
return err
|
|
}
|
|
*s = StatusValues[str]
|
|
return nil
|
|
}
|
|
|
|
type Item struct {
|
|
Id int64 `json:"id"`
|
|
GUID string `json:"guid"`
|
|
FeedId int64 `json:"feed_id"`
|
|
Title string `json:"title"`
|
|
Link string `json:"link"`
|
|
Content string `json:"content,omitempty"`
|
|
Date time.Time `json:"date"`
|
|
Status ItemStatus `json:"status"`
|
|
ImageURL *string `json:"image"`
|
|
AudioURL *string `json:"podcast_url"`
|
|
}
|
|
|
|
type ItemFilter struct {
|
|
FolderID *int64
|
|
FeedID *int64
|
|
Status *ItemStatus
|
|
Search *string
|
|
After *int64
|
|
}
|
|
|
|
type MarkFilter struct {
|
|
FolderID *int64
|
|
FeedID *int64
|
|
}
|
|
|
|
func (s *Storage) CreateItems(items []Item) bool {
|
|
tx, err := s.db.Begin()
|
|
if err != nil {
|
|
log.Print(err)
|
|
return false
|
|
}
|
|
|
|
now := time.Now()
|
|
|
|
for _, item := range items {
|
|
_, err = tx.Exec(`
|
|
insert into items (
|
|
guid, feed_id, title, link, date,
|
|
content, image, podcast_url,
|
|
date_arrived, status
|
|
)
|
|
values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
on conflict (feed_id, guid) do nothing`,
|
|
item.GUID, item.FeedId, item.Title, item.Link, item.Date,
|
|
item.Content, item.ImageURL, item.AudioURL,
|
|
now, UNREAD,
|
|
)
|
|
if err != nil {
|
|
log.Print(err)
|
|
if err = tx.Rollback(); err != nil {
|
|
log.Print(err)
|
|
return false
|
|
}
|
|
return false
|
|
}
|
|
}
|
|
if err = tx.Commit(); err != nil {
|
|
log.Print(err)
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func listQueryPredicate(filter ItemFilter, newestFirst bool) (string, []interface{}) {
|
|
cond := make([]string, 0)
|
|
args := make([]interface{}, 0)
|
|
if filter.FolderID != nil {
|
|
cond = append(cond, "i.feed_id in (select id from feeds where folder_id = ?)")
|
|
args = append(args, *filter.FolderID)
|
|
}
|
|
if filter.FeedID != nil {
|
|
cond = append(cond, "i.feed_id = ?")
|
|
args = append(args, *filter.FeedID)
|
|
}
|
|
if filter.Status != nil {
|
|
cond = append(cond, "i.status = ?")
|
|
args = append(args, *filter.Status)
|
|
}
|
|
if filter.Search != nil {
|
|
words := strings.Fields(*filter.Search)
|
|
terms := make([]string, len(words))
|
|
for idx, word := range words {
|
|
terms[idx] = word + "*"
|
|
}
|
|
|
|
cond = append(cond, "i.search_rowid in (select rowid from search where search match ?)")
|
|
args = append(args, strings.Join(terms, " "))
|
|
}
|
|
if filter.After != nil {
|
|
compare := ">"
|
|
if newestFirst {
|
|
compare = "<"
|
|
}
|
|
cond = append(cond, fmt.Sprintf("(i.date, i.id) %s (select date, id from items where id = ?)", compare))
|
|
args = append(args, *filter.After)
|
|
}
|
|
|
|
predicate := "1"
|
|
if len(cond) > 0 {
|
|
predicate = strings.Join(cond, " and ")
|
|
}
|
|
|
|
return predicate, args
|
|
}
|
|
|
|
func (s *Storage) ListItems(filter ItemFilter, limit int, newestFirst bool) []Item {
|
|
predicate, args := listQueryPredicate(filter, newestFirst)
|
|
result := make([]Item, 0, 0)
|
|
|
|
order := "date desc, id desc"
|
|
if !newestFirst {
|
|
order = "date asc, id asc"
|
|
}
|
|
|
|
query := fmt.Sprintf(`
|
|
select
|
|
i.id, i.guid, i.feed_id,
|
|
i.title, i.link, i.date,
|
|
i.status, i.image, i.podcast_url
|
|
from items i
|
|
where %s
|
|
order by %s
|
|
limit %d
|
|
`, predicate, order, limit)
|
|
rows, err := s.db.Query(query, args...)
|
|
if err != nil {
|
|
log.Print(err)
|
|
return result
|
|
}
|
|
for rows.Next() {
|
|
var x Item
|
|
err = rows.Scan(
|
|
&x.Id, &x.GUID, &x.FeedId,
|
|
&x.Title, &x.Link, &x.Date,
|
|
&x.Status, &x.ImageURL, &x.AudioURL,
|
|
)
|
|
if err != nil {
|
|
log.Print(err)
|
|
return result
|
|
}
|
|
result = append(result, x)
|
|
}
|
|
return result
|
|
}
|
|
|
|
func (s *Storage) GetItem(id int64) *Item {
|
|
i := &Item{}
|
|
err := s.db.QueryRow(`
|
|
select
|
|
i.id, i.guid, i.feed_id, i.title, i.link, i.content,
|
|
i.date, i.status, i.image, i.podcast_url
|
|
from items i
|
|
where i.id = ?
|
|
`, id).Scan(
|
|
&i.Id, &i.GUID, &i.FeedId, &i.Title, &i.Link, &i.Content,
|
|
&i.Date, &i.Status, &i.ImageURL, &i.AudioURL,
|
|
)
|
|
if err != nil {
|
|
log.Print(err)
|
|
return nil
|
|
}
|
|
return i
|
|
}
|
|
|
|
func (s *Storage) UpdateItemStatus(item_id int64, status ItemStatus) bool {
|
|
_, err := s.db.Exec(`update items set status = ? where id = ?`, status, item_id)
|
|
return err == nil
|
|
}
|
|
|
|
func (s *Storage) MarkItemsRead(filter MarkFilter) bool {
|
|
predicate, args := listQueryPredicate(ItemFilter{FolderID: filter.FolderID, FeedID: filter.FeedID}, false)
|
|
query := fmt.Sprintf(`
|
|
update items as i set status = %d
|
|
where %s and i.status != %d
|
|
`, READ, predicate, STARRED)
|
|
_, err := s.db.Exec(query, args...)
|
|
if err != nil {
|
|
log.Print(err)
|
|
}
|
|
return err == nil
|
|
}
|
|
|
|
type FeedStat struct {
|
|
FeedId int64 `json:"feed_id"`
|
|
UnreadCount int64 `json:"unread"`
|
|
StarredCount int64 `json:"starred"`
|
|
}
|
|
|
|
func (s *Storage) FeedStats() []FeedStat {
|
|
result := make([]FeedStat, 0)
|
|
rows, err := s.db.Query(fmt.Sprintf(`
|
|
select
|
|
feed_id,
|
|
sum(case status when %d then 1 else 0 end),
|
|
sum(case status when %d then 1 else 0 end)
|
|
from items
|
|
group by feed_id
|
|
`, UNREAD, STARRED))
|
|
if err != nil {
|
|
log.Print(err)
|
|
return result
|
|
}
|
|
for rows.Next() {
|
|
stat := FeedStat{}
|
|
rows.Scan(&stat.FeedId, &stat.UnreadCount, &stat.StarredCount)
|
|
result = append(result, stat)
|
|
}
|
|
return result
|
|
}
|
|
|
|
func (s *Storage) SyncSearch() {
|
|
rows, err := s.db.Query(`
|
|
select id, title, content
|
|
from items
|
|
where search_rowid is null;
|
|
`)
|
|
if err != nil {
|
|
log.Print(err)
|
|
return
|
|
}
|
|
|
|
items := make([]Item, 0)
|
|
for rows.Next() {
|
|
var item Item
|
|
rows.Scan(&item.Id, &item.Title, &item.Content)
|
|
items = append(items, item)
|
|
}
|
|
|
|
for _, item := range items {
|
|
result, err := s.db.Exec(`
|
|
insert into search (title, description, content) values (?, "", ?)`,
|
|
item.Title, htmlutil.ExtractText(item.Content),
|
|
)
|
|
if err != nil {
|
|
log.Print(err)
|
|
return
|
|
}
|
|
if numrows, err := result.RowsAffected(); err == nil && numrows == 1 {
|
|
if rowId, err := result.LastInsertId(); err == nil {
|
|
s.db.Exec(
|
|
`update items set search_rowid = ? where id = ?`,
|
|
rowId, item.Id,
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *Storage) DeleteOldItems() {
|
|
rows, err := s.db.Query(fmt.Sprintf(`
|
|
select feed_id, count(*) as num_items
|
|
from items
|
|
where status != %d
|
|
group by feed_id
|
|
having num_items > 50
|
|
`, STARRED))
|
|
|
|
if err != nil {
|
|
log.Print(err)
|
|
return
|
|
}
|
|
|
|
feedIds := make([]int64, 0)
|
|
for rows.Next() {
|
|
var id int64
|
|
rows.Scan(&id, nil)
|
|
feedIds = append(feedIds, id)
|
|
}
|
|
|
|
for _, feedId := range feedIds {
|
|
result, err := s.db.Exec(`
|
|
delete from items where feed_id = ? and status != ? and date_arrived < ?`,
|
|
feedId,
|
|
STARRED,
|
|
time.Now().Add(-time.Hour*24*90), // 90 days
|
|
)
|
|
if err != nil {
|
|
log.Print(err)
|
|
return
|
|
}
|
|
num, err := result.RowsAffected()
|
|
if err != nil {
|
|
log.Print(err)
|
|
return
|
|
}
|
|
if num > 0 {
|
|
log.Printf("Deleted %d old items (%d)", num, feedId)
|
|
}
|
|
}
|
|
}
|