all repos

smutok @ d02187624dcf414f4928eb63fde2952352bf16f9

yet another tui rss reader (not abandoned, just paused development)
5 files changed, 113 insertions(+), 72 deletions(-)
improve the api
Author: Oleksandr Smirnov olexsmir@gmail.com
Committed at: 2025-12-24 01:05:17 +0200
Change ID: tpqyrxomorzlpnwkoqqooswuzyqlznul
Parent: 0ff9ce8
M cmd_main.go

@@ -3,7 +3,6 @@

import ( "context" "errors" - "fmt" "log/slog" "github.com/urfave/cli/v3"

@@ -50,7 +49,5 @@

gr.SetAuthToken(token) gs := sync.NewFreshRSS(db, gr) - fmt.Println(gs.Sync(ctx, true)) - - return nil + return gs.Sync(ctx) }
M go.mod

@@ -7,6 +7,7 @@ ariga.io/atlas v0.38.0

github.com/adrg/xdg v0.5.3 github.com/charmbracelet/bubbletea v1.3.10 github.com/pelletier/go-toml/v2 v2.2.4 + github.com/tidwall/gjson v1.18.0 github.com/urfave/cli/v3 v3.6.1 modernc.org/sqlite v1.40.1 olexsmir.xyz/x v0.1.1

@@ -40,6 +41,8 @@ github.com/muesli/termenv v0.16.0 // indirect

github.com/ncruces/go-strftime v0.1.9 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/rivo/uniseg v0.4.7 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.0 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/zclconf/go-cty v1.14.4 // indirect github.com/zclconf/go-cty-yaml v1.1.0 // indirect
M go.sum

@@ -81,6 +81,12 @@ github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=

github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/urfave/cli/v3 v3.6.1 h1:j8Qq8NyUawj/7rTYdBGrxcH7A/j7/G8Q5LhWEW4G3Mo= github.com/urfave/cli/v3 v3.6.1/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
M internal/provider/freshrss.go

@@ -12,6 +12,8 @@ "net/url"

"strconv" "strings" "time" + + "github.com/tidwall/gjson" ) var (

@@ -67,22 +69,29 @@

type subscriptionList struct { Subscriptions []Subscriptions `json:"subscriptions"` } + type Subscriptions struct { - Categories struct { - ID string `json:"id"` - Label string `json:"label"` - } `json:"categories"` - ID string `json:"id"` - HTMLURL string `json:"htmlUrl"` - IconURL string `json:"iconUrl"` - Title string `json:"title"` - URL string `json:"url"` + Categories []SubscriptionCategory `json:"categories"` + ID string `json:"id"` + HTMLURL string `json:"htmlUrl"` + Title string `json:"title"` + URL string `json:"url"` + + // IconURL string `json:"iconUrl"` +} + +type SubscriptionCategory struct { + ID string `json:"id"` + Label string `json:"label"` } func (g FreshRSS) SubscriptionList(ctx context.Context) ([]Subscriptions, error) { - var resp subscriptionList - err := g.request(ctx, "/reader/api/0/subscription/list?output=json", nil, &resp) - return resp.Subscriptions, err + params := url.Values{} + params.Set("output", "json") + + var jsonResp subscriptionList + err := g.request(ctx, "/reader/api/0/subscription/list", params, &jsonResp) + return jsonResp.Subscriptions, err } type tagList struct {

@@ -95,76 +104,95 @@ Type string `json:"type,omitempty"`

} func (g FreshRSS) TagList(ctx context.Context) ([]Tag, error) { + params := url.Values{} + params.Set("output", "json") + var resp tagList - err := g.request(ctx, "/reader/api/0/tag/list?output=json", nil, &resp) + err := g.request(ctx, "/reader/api/0/tag/list", params, &resp) return resp.Tags, err } -type StreamContents struct { - Continuation string `json:"continuation"` - ID string `json:"id"` - Items []struct { - Alternate []struct { - Href string `json:"href"` - } `json:"alternate"` - Author string `json:"author"` - Canonical []struct { - Href string `json:"href"` - } `json:"canonical"` - Categories []string `json:"categories"` - CrawlTimeMsec string `json:"crawlTimeMsec"` - ID string `json:"id"` - Origin struct { - HTMLURL string `json:"htmlUrl"` - StreamID string `json:"streamId"` - Title string `json:"title"` - } `json:"origin"` - Published int `json:"published"` - Summary struct { - Content string `json:"content"` - } `json:"summary"` - TimestampUsec string `json:"timestampUsec"` - Title string `json:"title"` - } `json:"items"` - Updated int `json:"updated"` +type ContentItem struct { + ID string + Published int64 + Title string + Author string + Canonical []string + Content string + Categories []string + Origin struct { + HTMLURL string + StreamID string + Title string + } + + // CrawlTimeMsec string `json:"crawlTimeMsec"` + // TimestampUsec string `json:"timestampUsec"` } -func (g FreshRSS) GetItems(ctx context.Context, excludeTarget string, lastModified, n int) (StreamContents, error) { +func (g FreshRSS) StreamContents(ctx context.Context, steamID, excludeTarget string, lastModified, n int) ([]ContentItem, error) { params := url.Values{} setOption(&params, "xt", excludeTarget) setOptionInt(&params, "ot", lastModified) setOptionInt(&params, "n", n) + params.Set("r", "n") - var resp StreamContents - err := g.request(ctx, "/reader/api/0/stream/contents/user/-/state/com.google/reading-list", params, &resp) - return resp, err -} + var jsonResp string + if err := g.request(ctx, "/reader/api/0/stream/contents/"+steamID, params, &jsonResp); err != nil { + return nil, err + } -func (g FreshRSS) GetStaredItems(ctx context.Context, n int) (StreamContents, error) { - params := url.Values{} - setOptionInt(&params, "n", n) + items := gjson.GetBytes([]byte(jsonResp), "items").Array() + if len(items) == 0 { + return []ContentItem{}, nil + } + + res := make([]ContentItem, len(items)) + for i, item := range items { + var ci ContentItem + ci.ID = item.Get("id").String() + ci.Title = item.Get("title").String() + ci.Published = item.Get("published").Int() + ci.Author = item.Get("author").String() + ci.Content = item.Get("summary.content").String() + ci.Origin.StreamID = item.Get("origin.streamId").String() + ci.Origin.HTMLURL = item.Get("origin.htmlUrl").String() + ci.Origin.Title = item.Get("origin.title").String() + + for _, href := range item.Get("canonical.#.href").Array() { + if h := href.String(); h != "" { + ci.Canonical = append(ci.Canonical, h) + } + } + for _, cat := range item.Get("categories").Array() { + ci.Categories = append(ci.Categories, cat.String()) + } - var resp StreamContents - err := g.request(ctx, "/reader/api/0/stream/contents/user/-/state/com.google/starred", params, &resp) - return resp, err -} + res[i] = ci + } -type StreamItemsIDs struct { - Continuation string `json:"continuation"` - ItemRefs []struct { - ID string `json:"id"` - } `json:"itemRefs"` + return res, nil } -func (g FreshRSS) GetItemsIDs(ctx context.Context, excludeTarget, includeTarget string, n int) (StreamItemsIDs, error) { +func (g FreshRSS) StreamIDs(ctx context.Context, excludeTarget, includeTarget string, n int) ([]string, error) { params := url.Values{} setOption(&params, "xt", excludeTarget) setOption(&params, "s", includeTarget) setOptionInt(&params, "n", n) + params.Set("r", "n") - var resp StreamItemsIDs - err := g.request(ctx, "/reader/api/0/stream/items/ids", params, &resp) - return resp, err + var jsonResp string + if err := g.request(ctx, "/reader/api/0/stream/items/ids", params, &jsonResp); err != nil { + return nil, err + } + + ids := gjson.Get(jsonResp, "itemRefs.#.id").Array() + resp := make([]string, len(ids)) + for i, v := range ids { + resp[i] = v.String() + } + + return resp, nil } func (g FreshRSS) SetItemsState(ctx context.Context, token, itemID string, addAction, removeAction string) error {
M internal/sync/freshrss.go

@@ -19,13 +19,20 @@ api: api,

} } -func (g *FreshRSS) Sync(ctx context.Context, initial bool) error { - writeToken, err := g.api.GetWriteToken(ctx) - if err != nil { - return err - } - - _ = writeToken +func (g *FreshRSS) Sync(ctx context.Context) error { + // tags, err := g.api.TagList(ctx) + // subscriptions, err := g.api.SubscriptionList(ctx) + // unreadItems, err := g.api.StreamContents( + // ctx, + // "user/-/state/com.google/reading-list", + // "user/-/state/com.google/read", + // 0, + // 1000) + // ids, err := g.api.GetItemsIDs(ctx, + // "user/-/state/com.google/read", + // "user/-/state/com.google/reading-list", + // 1000, + // ) return nil }