[mirror] yet another tui rss reader github.com/olexsmir/smutok

improve the api

olexsmir.xyz d0218762 0ff9ce85

verified
Changed files
+113 -72
internal
provider
sync
+1 -4
cmd_main.go
··· 3 3 import ( 4 4 "context" 5 5 "errors" 6 - "fmt" 7 6 "log/slog" 8 7 9 8 "github.com/urfave/cli/v3" ··· 50 49 gr.SetAuthToken(token) 51 50 52 51 gs := sync.NewFreshRSS(db, gr) 53 - fmt.Println(gs.Sync(ctx, true)) 54 - 55 - return nil 52 + return gs.Sync(ctx) 56 53 }
+3
go.mod
··· 7 7 github.com/adrg/xdg v0.5.3 8 8 github.com/charmbracelet/bubbletea v1.3.10 9 9 github.com/pelletier/go-toml/v2 v2.2.4 10 + github.com/tidwall/gjson v1.18.0 10 11 github.com/urfave/cli/v3 v3.6.1 11 12 modernc.org/sqlite v1.40.1 12 13 olexsmir.xyz/x v0.1.1 ··· 40 41 github.com/ncruces/go-strftime v0.1.9 // indirect 41 42 github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect 42 43 github.com/rivo/uniseg v0.4.7 // indirect 44 + github.com/tidwall/match v1.1.1 // indirect 45 + github.com/tidwall/pretty v1.2.0 // indirect 43 46 github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect 44 47 github.com/zclconf/go-cty v1.14.4 // indirect 45 48 github.com/zclconf/go-cty-yaml v1.1.0 // indirect
+6
go.sum
··· 81 81 github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= 82 82 github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= 83 83 github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 84 + github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= 85 + github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= 86 + github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= 87 + github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= 88 + github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= 89 + github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= 84 90 github.com/urfave/cli/v3 v3.6.1 h1:j8Qq8NyUawj/7rTYdBGrxcH7A/j7/G8Q5LhWEW4G3Mo= 85 91 github.com/urfave/cli/v3 v3.6.1/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso= 86 92 github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
+89 -61
internal/provider/freshrss.go
··· 12 12 "strconv" 13 13 "strings" 14 14 "time" 15 + 16 + "github.com/tidwall/gjson" 15 17 ) 16 18 17 19 var ( ··· 67 69 type subscriptionList struct { 68 70 Subscriptions []Subscriptions `json:"subscriptions"` 69 71 } 72 + 70 73 type Subscriptions struct { 71 - Categories struct { 72 - ID string `json:"id"` 73 - Label string `json:"label"` 74 - } `json:"categories"` 75 - ID string `json:"id"` 76 - HTMLURL string `json:"htmlUrl"` 77 - IconURL string `json:"iconUrl"` 78 - Title string `json:"title"` 79 - URL string `json:"url"` 74 + Categories []SubscriptionCategory `json:"categories"` 75 + ID string `json:"id"` 76 + HTMLURL string `json:"htmlUrl"` 77 + Title string `json:"title"` 78 + URL string `json:"url"` 79 + 80 + // IconURL string `json:"iconUrl"` 81 + } 82 + 83 + type SubscriptionCategory struct { 84 + ID string `json:"id"` 85 + Label string `json:"label"` 80 86 } 81 87 82 88 func (g FreshRSS) SubscriptionList(ctx context.Context) ([]Subscriptions, error) { 83 - var resp subscriptionList 84 - err := g.request(ctx, "/reader/api/0/subscription/list?output=json", nil, &resp) 85 - return resp.Subscriptions, err 89 + params := url.Values{} 90 + params.Set("output", "json") 91 + 92 + var jsonResp subscriptionList 93 + err := g.request(ctx, "/reader/api/0/subscription/list", params, &jsonResp) 94 + return jsonResp.Subscriptions, err 86 95 } 87 96 88 97 type tagList struct { ··· 95 104 } 96 105 97 106 func (g FreshRSS) TagList(ctx context.Context) ([]Tag, error) { 107 + params := url.Values{} 108 + params.Set("output", "json") 109 + 98 110 var resp tagList 99 - err := g.request(ctx, "/reader/api/0/tag/list?output=json", nil, &resp) 111 + err := g.request(ctx, "/reader/api/0/tag/list", params, &resp) 100 112 return resp.Tags, err 101 113 } 102 114 103 - type StreamContents struct { 104 - Continuation string `json:"continuation"` 105 - ID string `json:"id"` 106 - Items []struct { 107 - Alternate []struct { 108 - Href string `json:"href"` 109 - } `json:"alternate"` 110 - Author string `json:"author"` 111 - Canonical []struct { 112 - Href string `json:"href"` 113 - } `json:"canonical"` 114 - Categories []string `json:"categories"` 115 - CrawlTimeMsec string `json:"crawlTimeMsec"` 116 - ID string `json:"id"` 117 - Origin struct { 118 - HTMLURL string `json:"htmlUrl"` 119 - StreamID string `json:"streamId"` 120 - Title string `json:"title"` 121 - } `json:"origin"` 122 - Published int `json:"published"` 123 - Summary struct { 124 - Content string `json:"content"` 125 - } `json:"summary"` 126 - TimestampUsec string `json:"timestampUsec"` 127 - Title string `json:"title"` 128 - } `json:"items"` 129 - Updated int `json:"updated"` 115 + type ContentItem struct { 116 + ID string 117 + Published int64 118 + Title string 119 + Author string 120 + Canonical []string 121 + Content string 122 + Categories []string 123 + Origin struct { 124 + HTMLURL string 125 + StreamID string 126 + Title string 127 + } 128 + 129 + // CrawlTimeMsec string `json:"crawlTimeMsec"` 130 + // TimestampUsec string `json:"timestampUsec"` 130 131 } 131 132 132 - func (g FreshRSS) GetItems(ctx context.Context, excludeTarget string, lastModified, n int) (StreamContents, error) { 133 + func (g FreshRSS) StreamContents(ctx context.Context, steamID, excludeTarget string, lastModified, n int) ([]ContentItem, error) { 133 134 params := url.Values{} 134 135 setOption(&params, "xt", excludeTarget) 135 136 setOptionInt(&params, "ot", lastModified) 136 137 setOptionInt(&params, "n", n) 138 + params.Set("r", "n") 137 139 138 - var resp StreamContents 139 - err := g.request(ctx, "/reader/api/0/stream/contents/user/-/state/com.google/reading-list", params, &resp) 140 - return resp, err 141 - } 140 + var jsonResp string 141 + if err := g.request(ctx, "/reader/api/0/stream/contents/"+steamID, params, &jsonResp); err != nil { 142 + return nil, err 143 + } 142 144 143 - func (g FreshRSS) GetStaredItems(ctx context.Context, n int) (StreamContents, error) { 144 - params := url.Values{} 145 - setOptionInt(&params, "n", n) 145 + items := gjson.GetBytes([]byte(jsonResp), "items").Array() 146 + if len(items) == 0 { 147 + return []ContentItem{}, nil 148 + } 149 + 150 + res := make([]ContentItem, len(items)) 151 + for i, item := range items { 152 + var ci ContentItem 153 + ci.ID = item.Get("id").String() 154 + ci.Title = item.Get("title").String() 155 + ci.Published = item.Get("published").Int() 156 + ci.Author = item.Get("author").String() 157 + ci.Content = item.Get("summary.content").String() 158 + ci.Origin.StreamID = item.Get("origin.streamId").String() 159 + ci.Origin.HTMLURL = item.Get("origin.htmlUrl").String() 160 + ci.Origin.Title = item.Get("origin.title").String() 161 + 162 + for _, href := range item.Get("canonical.#.href").Array() { 163 + if h := href.String(); h != "" { 164 + ci.Canonical = append(ci.Canonical, h) 165 + } 166 + } 167 + for _, cat := range item.Get("categories").Array() { 168 + ci.Categories = append(ci.Categories, cat.String()) 169 + } 146 170 147 - var resp StreamContents 148 - err := g.request(ctx, "/reader/api/0/stream/contents/user/-/state/com.google/starred", params, &resp) 149 - return resp, err 150 - } 171 + res[i] = ci 172 + } 151 173 152 - type StreamItemsIDs struct { 153 - Continuation string `json:"continuation"` 154 - ItemRefs []struct { 155 - ID string `json:"id"` 156 - } `json:"itemRefs"` 174 + return res, nil 157 175 } 158 176 159 - func (g FreshRSS) GetItemsIDs(ctx context.Context, excludeTarget, includeTarget string, n int) (StreamItemsIDs, error) { 177 + func (g FreshRSS) StreamIDs(ctx context.Context, excludeTarget, includeTarget string, n int) ([]string, error) { 160 178 params := url.Values{} 161 179 setOption(&params, "xt", excludeTarget) 162 180 setOption(&params, "s", includeTarget) 163 181 setOptionInt(&params, "n", n) 182 + params.Set("r", "n") 164 183 165 - var resp StreamItemsIDs 166 - err := g.request(ctx, "/reader/api/0/stream/items/ids", params, &resp) 167 - return resp, err 184 + var jsonResp string 185 + if err := g.request(ctx, "/reader/api/0/stream/items/ids", params, &jsonResp); err != nil { 186 + return nil, err 187 + } 188 + 189 + ids := gjson.Get(jsonResp, "itemRefs.#.id").Array() 190 + resp := make([]string, len(ids)) 191 + for i, v := range ids { 192 + resp[i] = v.String() 193 + } 194 + 195 + return resp, nil 168 196 } 169 197 170 198 func (g FreshRSS) SetItemsState(ctx context.Context, token, itemID string, addAction, removeAction string) error {
+14 -7
internal/sync/freshrss.go
··· 19 19 } 20 20 } 21 21 22 - func (g *FreshRSS) Sync(ctx context.Context, initial bool) error { 23 - writeToken, err := g.api.GetWriteToken(ctx) 24 - if err != nil { 25 - return err 26 - } 27 - 28 - _ = writeToken 22 + func (g *FreshRSS) Sync(ctx context.Context) error { 23 + // tags, err := g.api.TagList(ctx) 24 + // subscriptions, err := g.api.SubscriptionList(ctx) 25 + // unreadItems, err := g.api.StreamContents( 26 + // ctx, 27 + // "user/-/state/com.google/reading-list", 28 + // "user/-/state/com.google/read", 29 + // 0, 30 + // 1000) 31 + // ids, err := g.api.GetItemsIDs(ctx, 32 + // "user/-/state/com.google/read", 33 + // "user/-/state/com.google/reading-list", 34 + // 1000, 35 + // ) 29 36 30 37 return nil 31 38 }