mirror of
https://github.com/nkanaev/yarr.git
synced 2025-05-25 13:39:22 +00:00
Merge b92ad6b5803d10eab304db016a7cf135043c5b83 into 0e88d4284d96338ded25e0b5acac156b7ea1288a
This commit is contained in:
commit
e49f9910d8
@ -224,6 +224,15 @@
|
||||
<span class="icon mr-1">{% inline "edit.svg" %}</span>
|
||||
Change Link
|
||||
</button>
|
||||
<div class="dropdown-divider"></div>
|
||||
|
||||
<header class="dropdown-header">Expire Unreads</header>
|
||||
<div class="row text-center m-0">
|
||||
<button class="dropdown-item col-4 px-0" :class="{active: feedExpire == 1440}" @click.stop="feedExpireUnreads(current.feed, 1440)">1d</button>
|
||||
<button class="dropdown-item col-4 px-0" :class="{active: feedExpire == 10080}" @click.stop="feedExpireUnreads(current.feed, 10080)">1w</button>
|
||||
<button class="dropdown-item col-4 px-0" :class="{active: feedExpire == 43200}" @click.stop="feedExpireUnreads(current.feed, 43200)">1m</button>
|
||||
</div>
|
||||
|
||||
<div class="dropdown-divider"></div>
|
||||
<header class="dropdown-header">Move to...</header>
|
||||
<button class="dropdown-item"
|
||||
|
@ -52,6 +52,9 @@
|
||||
list_errors: function() {
|
||||
return api('get', './api/feeds/errors').then(json)
|
||||
},
|
||||
get_expire_minutes: function(id) {
|
||||
return api('get', './api/feeds/' + id + '/expire').then(json)
|
||||
},
|
||||
},
|
||||
folders: {
|
||||
list: function() {
|
||||
|
@ -250,6 +250,8 @@ var vm = new Vue({
|
||||
'size': s.theme_size,
|
||||
},
|
||||
'refreshRate': s.refresh_rate,
|
||||
'expireUnreads': s.expiration_rate,
|
||||
'feedExpire': 0,
|
||||
'authenticated': app.authenticated,
|
||||
'feed_errors': {},
|
||||
}
|
||||
@ -333,8 +335,18 @@ var vm = new Vue({
|
||||
},
|
||||
'feedSelected': function(newVal, oldVal) {
|
||||
if (oldVal === undefined) return // do nothing, initial setup
|
||||
|
||||
api.settings.update({feed: newVal}).then(this.refreshItems.bind(this, false))
|
||||
this.itemSelected = null
|
||||
|
||||
var parts = newVal.split(':', 2)
|
||||
if (parts[0] == 'feed') {
|
||||
var feed_id = parts[1]
|
||||
api.feeds.get_expire_minutes(feed_id).then(function(resp) {
|
||||
vm.feedExpire = resp.feedExpire
|
||||
})
|
||||
}
|
||||
|
||||
if (this.$refs.itemlist) this.$refs.itemlist.scrollTop = 0
|
||||
},
|
||||
'itemSelected': function(newVal, oldVal) {
|
||||
@ -496,6 +508,14 @@ var vm = new Vue({
|
||||
}
|
||||
return new Date(datestr).toLocaleDateString(undefined, options)
|
||||
},
|
||||
'feedExpireUnreads': function(feed, newVal) {
|
||||
if (vm.feedExpire == newVal) {
|
||||
newVal = 0
|
||||
}
|
||||
api.feeds.update(feed.id, {expire_minutes: newVal}).then(function() {
|
||||
vm.feedExpire = newVal
|
||||
})
|
||||
},
|
||||
moveFeed: function(feed, folder) {
|
||||
var folder_id = folder ? folder.id : null
|
||||
api.feeds.update(feed.id, {folder_id: folder_id}).then(function() {
|
||||
|
75
src/expirer/expirer.go
Normal file
75
src/expirer/expirer.go
Normal file
@ -0,0 +1,75 @@
|
||||
package expirer
|
||||
|
||||
import (
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/nkanaev/yarr/src/storage"
|
||||
)
|
||||
|
||||
type Expirer struct {
|
||||
db *storage.Storage
|
||||
stop chan bool
|
||||
intervalMinutes uint64
|
||||
}
|
||||
|
||||
func NewExpirer(db *storage.Storage) *Expirer {
|
||||
return &Expirer{
|
||||
db: db,
|
||||
stop: make(chan bool),
|
||||
intervalMinutes: 0,
|
||||
}
|
||||
}
|
||||
|
||||
func expire(db *storage.Storage, rateMinutes uint64, stop chan bool) {
|
||||
tick := time.NewTicker(time.Minute * time.Duration(rateMinutes) / 2)
|
||||
db.ExpireUnreads(rateMinutes)
|
||||
log.Printf("expirer %dm: starting", rateMinutes)
|
||||
for {
|
||||
select {
|
||||
case <-tick.C:
|
||||
log.Printf("expirer %dm: firing", rateMinutes)
|
||||
db.ExpireUnreads(rateMinutes)
|
||||
case <-stop:
|
||||
log.Printf("expirer %dm: stopping", rateMinutes)
|
||||
tick.Stop()
|
||||
stop <- true
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Expirer) getCheckInterval(globalExpirationPeriod uint64) uint64 {
|
||||
minFeedExpirationPeriod, err := e.db.GetMinExpirationPeriod()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
checkInterval := globalExpirationPeriod
|
||||
if *minFeedExpirationPeriod != 0 && *minFeedExpirationPeriod < checkInterval {
|
||||
checkInterval = *minFeedExpirationPeriod
|
||||
}
|
||||
return checkInterval
|
||||
}
|
||||
|
||||
func (e *Expirer) StartUnreadsExpirer(globalExpirationPeriod uint64) {
|
||||
checkInterval := e.getCheckInterval(globalExpirationPeriod)
|
||||
if checkInterval > 0 {
|
||||
e.intervalMinutes = uint64(checkInterval)
|
||||
go expire(e.db, e.intervalMinutes, e.stop)
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Expirer) SetExpirationRate(globalExpirationPeriod uint64) {
|
||||
checkInterval := e.getCheckInterval(globalExpirationPeriod)
|
||||
if checkInterval == e.intervalMinutes {
|
||||
return
|
||||
}
|
||||
e.stop <- true
|
||||
<-e.stop
|
||||
e.intervalMinutes = globalExpirationPeriod
|
||||
if checkInterval == 0 {
|
||||
return
|
||||
}
|
||||
go expire(e.db, e.intervalMinutes, e.stop)
|
||||
}
|
@ -51,6 +51,7 @@ func (s *Server) handler() http.Handler {
|
||||
r.For("/api/feeds/errors", s.handleFeedErrors)
|
||||
r.For("/api/feeds/:id/icon", s.handleFeedIcon)
|
||||
r.For("/api/feeds/:id", s.handleFeed)
|
||||
r.For("/api/feeds/:id/expire", s.handleFeedExpire)
|
||||
r.For("/api/items", s.handleItemList)
|
||||
r.For("/api/items/:id", s.handleItem)
|
||||
r.For("/api/settings", s.handleSettings)
|
||||
@ -263,13 +264,54 @@ func (s *Server) handleFeedList(c *router.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) handleFeedExpire(c *router.Context) {
|
||||
feedId, err := c.VarInt64("id")
|
||||
if err != nil {
|
||||
c.Out.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if c.Req.Method != "GET" {
|
||||
c.Out.WriteHeader(http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
feed := s.db.GetFeed(feedId)
|
||||
if feed == nil {
|
||||
c.Out.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
expire_minutes, err := s.db.GetFeedExpirationRate(feedId)
|
||||
if err != nil {
|
||||
c.Out.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, map[string]interface{}{
|
||||
"feedExpire": expire_minutes,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) handleFeed(c *router.Context) {
|
||||
id, err := c.VarInt64("id")
|
||||
if err != nil {
|
||||
c.Out.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if c.Req.Method == "PUT" {
|
||||
if c.Req.Method == "GET" {
|
||||
feedId, err := c.QueryInt64("feed_id")
|
||||
if err == nil {
|
||||
c.Out.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
feed := s.db.GetFeed(feedId)
|
||||
if feed == nil {
|
||||
c.Out.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
expire_minutes, err := s.db.GetFeedExpirationRate(feedId)
|
||||
c.JSON(http.StatusOK, map[string]interface{}{
|
||||
"expire_minutes": expire_minutes,
|
||||
})
|
||||
} else if c.Req.Method == "PUT" {
|
||||
feed := s.db.GetFeed(id)
|
||||
if feed == nil {
|
||||
c.Out.WriteHeader(http.StatusBadRequest)
|
||||
@ -299,6 +341,15 @@ func (s *Server) handleFeed(c *router.Context) {
|
||||
s.db.UpdateFeedLink(id, link.(string))
|
||||
}
|
||||
}
|
||||
if expire_minutes, ok := body["expire_minutes"]; ok {
|
||||
if reflect.TypeOf(expire_minutes).Kind() == reflect.Float64 {
|
||||
minutes := int64(expire_minutes.(float64))
|
||||
err := s.db.UpdateFeedExpirationRate(id, minutes)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
c.Out.WriteHeader(http.StatusOK)
|
||||
} else if c.Req.Method == "DELETE" {
|
||||
s.db.DeleteFeed(id)
|
||||
@ -417,6 +468,10 @@ func (s *Server) handleSettings(c *router.Context) {
|
||||
if _, ok := settings["refresh_rate"]; ok {
|
||||
s.worker.SetRefreshRate(s.db.GetSettingsValueInt64("refresh_rate"))
|
||||
}
|
||||
if _, ok := settings["expiration_rate"]; ok {
|
||||
expirationRate := s.db.GetSettingsValueInt64("expiration_rate")
|
||||
s.expirer.SetExpirationRate(uint64(expirationRate))
|
||||
}
|
||||
c.Out.WriteHeader(http.StatusOK)
|
||||
} else {
|
||||
c.Out.WriteHeader(http.StatusBadRequest)
|
||||
|
@ -5,6 +5,7 @@ import (
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"github.com/nkanaev/yarr/src/expirer"
|
||||
"github.com/nkanaev/yarr/src/storage"
|
||||
"github.com/nkanaev/yarr/src/worker"
|
||||
)
|
||||
@ -13,6 +14,7 @@ type Server struct {
|
||||
Addr string
|
||||
db *storage.Storage
|
||||
worker *worker.Worker
|
||||
expirer *expirer.Expirer
|
||||
cache map[string]interface{}
|
||||
cache_mutex *sync.Mutex
|
||||
|
||||
@ -31,6 +33,7 @@ func NewServer(db *storage.Storage, addr string) *Server {
|
||||
db: db,
|
||||
Addr: addr,
|
||||
worker: worker.NewWorker(db),
|
||||
expirer: expirer.NewExpirer(db),
|
||||
cache: make(map[string]interface{}),
|
||||
cache_mutex: &sync.Mutex{},
|
||||
}
|
||||
@ -53,6 +56,9 @@ func (s *Server) Start() {
|
||||
s.worker.RefreshFeeds()
|
||||
}
|
||||
|
||||
globalExpirationPeriod := s.db.GetSettingsValueInt64("expiration_rate")
|
||||
s.expirer.StartUnreadsExpirer(uint64(globalExpirationPeriod))
|
||||
|
||||
httpserver := &http.Server{Addr: s.Addr, Handler: s.handler()}
|
||||
|
||||
var err error
|
||||
|
@ -76,6 +76,31 @@ func (s *Storage) UpdateFeedLink(feedId int64, newLink string) bool {
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (s *Storage) GetFeedExpirationRate(feedId int64) (*uint64, error) {
|
||||
var result uint64
|
||||
row := s.db.QueryRow(`select expire_minutes from feeds where id = ?`, feedId)
|
||||
err := row.Scan(&result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func (s *Storage) UpdateFeedExpirationRate(feedId int64, expireMinutes int64) error {
|
||||
_, err := s.db.Exec(`update feeds set expire_minutes = ? where id = ?`, expireMinutes, feedId)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Storage) GetMinExpirationPeriod() (*uint64, error) {
|
||||
var result uint64
|
||||
row := s.db.QueryRow(`select min(expire_minutes) from feeds`)
|
||||
err := row.Scan(&result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func (s *Storage) UpdateFeedIcon(feedId int64, icon *[]byte) bool {
|
||||
_, err := s.db.Exec(`update feeds set icon = ? where id = ?`, icon, feedId)
|
||||
return err == nil
|
||||
|
@ -372,6 +372,51 @@ func (s *Storage) SyncSearch() {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Storage) ExpireUnreads(globalExpireMinutes uint64) {
|
||||
var numExpiredItems int64
|
||||
|
||||
// Collect expiration rates for each feed -- it's either the global default or a per-feed override
|
||||
rows, err := s.db.Query(`select id, iif(expire_minutes = 0, ?, expire_minutes) from feeds`, globalExpireMinutes)
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
return
|
||||
}
|
||||
|
||||
periodFromFeedId := make(map[int64]int64, 0)
|
||||
for rows.Next() {
|
||||
var feedId, period int64
|
||||
if period == 0 {
|
||||
continue
|
||||
}
|
||||
rows.Scan(&feedId, &period, nil)
|
||||
periodFromFeedId[feedId] = period
|
||||
}
|
||||
|
||||
for feedId, period := range periodFromFeedId {
|
||||
result, err := s.db.Exec(
|
||||
`UPDATE items SET status = ? WHERE feed_id = ? AND status = ? AND (julianday('now') - julianday(date_arrived)) * 3600 >= ?`,
|
||||
feedId,
|
||||
READ,
|
||||
UNREAD,
|
||||
period,
|
||||
)
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
return
|
||||
}
|
||||
rowsAffected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
return
|
||||
}
|
||||
numExpiredItems += rowsAffected
|
||||
}
|
||||
|
||||
if numExpiredItems > 0 {
|
||||
log.Printf("Expired %d old unread items", numExpiredItems)
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
itemsKeepSize = 50
|
||||
itemsKeepDays = 90
|
||||
|
@ -17,6 +17,7 @@ var migrations = []func(*sql.Tx) error{
|
||||
m07_add_feed_size,
|
||||
m08_normalize_datetime,
|
||||
m09_change_item_index,
|
||||
m10_per_feed_expiration,
|
||||
}
|
||||
|
||||
var maxVersion = int64(len(migrations))
|
||||
@ -103,7 +104,8 @@ func m01_initial(tx *sql.Tx) error {
|
||||
description text,
|
||||
link text,
|
||||
feed_link text not null,
|
||||
icon blob
|
||||
icon blob,
|
||||
expire_minutes integer not null default 0
|
||||
);
|
||||
|
||||
create index if not exists idx_feed_folder_id on feeds(folder_id);
|
||||
@ -306,3 +308,9 @@ func m09_change_item_index(tx *sql.Tx) error {
|
||||
_, err := tx.Exec(sql)
|
||||
return err
|
||||
}
|
||||
|
||||
func m10_per_feed_expiration(tx *sql.Tx) error {
|
||||
sql := `alter table feeds add column expire_minutes integer not null default 0;`
|
||||
_, err := tx.Exec(sql)
|
||||
return err
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ func settingsDefaults() map[string]interface{} {
|
||||
"theme_font": "",
|
||||
"theme_size": 1,
|
||||
"refresh_rate": 0,
|
||||
"expiration_rate": 0,
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user