Skip to content

Commit

Permalink
feat: add search command
Browse files Browse the repository at this point in the history
  • Loading branch information
radulucut committed Sep 3, 2024
1 parent 05fe206 commit 9ece8a6
Show file tree
Hide file tree
Showing 6 changed files with 271 additions and 26 deletions.
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,11 @@ cleed --since "1d"
# Display feeds since the last run
cleed --since last

# Display feeds from a specific list and limit the number of feeds
# Display feeds from a specific list and limit the number of items
cleed --list my-list --limit 10

# Search for items
cleed --search "keyword" --limit 10
```

#### Unfollow a feed
Expand Down
11 changes: 9 additions & 2 deletions cmd/cleed/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,11 @@ Examples:
# Display feeds since the last run
cleed --since last
# Display feeds from a specific list and limit the number of feeds
# Display feeds from a specific list and limit the number of items
cleed --list my-list --limit 10
# Search for items
cleed --search "keyword" --limit 10
`,
Version: version,
RunE: root.RunRoot,
Expand All @@ -102,8 +105,9 @@ Examples:

flags := root.Cmd.Flags()
flags.StringP("list", "L", "", "list to display feeds from")
flags.Uint("limit", 50, "limit the number of feeds to display")
flags.Uint("limit", 50, "limit the number of items to display")
flags.String("since", "", "display feeds since the last run (last), a specific date (e.g. 2024-01-01 12:03:04) or duration (e.g. 1d)")
flags.String("search", "", "search for items (title, categories)")
flags.Bool("config-path", false, "show the path to the config directory")
flags.Bool("cache-path", false, "show the path to the cache directory")
flags.Bool("cache-info", false, "show the cache information")
Expand Down Expand Up @@ -140,6 +144,9 @@ func (r *Root) RunRoot(cmd *cobra.Command, args []string) error {
Limit: int(limit),
Since: since,
}
if cmd.Flag("search").Changed {
return r.feed.Search(cmd.Flag("search").Value.String(), opts)
}
return r.feed.Feed(opts)
}

Expand Down
85 changes: 85 additions & 0 deletions cmd/cleed/root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,91 @@ RSS Feed • Item 1
assert.Equal(t, atom, string(b))
}

func Test_Feed_Search(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()

timeMock := mocks.NewMockTime(ctrl)
timeMock.EXPECT().Now().Return(defaultCurrentTime).AnyTimes()

out := new(bytes.Buffer)
printer := internal.NewPrinter(nil, out, out)
storage := _storage.NewLocalStorage("cleed_test", timeMock)
defer localStorageCleanup(t, storage)

configDir, err := os.UserConfigDir()
if err != nil {
t.Fatal(err)
}
listsDir := path.Join(configDir, "cleed_test", "lists")
err = os.MkdirAll(listsDir, 0700)
if err != nil {
t.Fatal(err)
}

items := []*FeedItem{
{
Title: "Keyword 1",
Link: "https://rss-feed.com/item-1/",
Published: "Wed, 31 Dec 2023 23:45:00 GMT",
Categories: []string{"category1", "category2"},
},
{
Title: "keywords",
Link: "https://rss-feed.com/item-1/",
Published: "Wed, 31 Dec 2023 23:45:00 GMT",
Categories: []string{"category1", "category2"},
},
{
Title: "keywordxaxa",
Link: "https://rss-feed.com/item-1/",
Published: "Wed, 31 Dec 2023 23:45:00 GMT",
Categories: []string{"category1", "category2"},
},
{
Title: "zzzz",
Link: "https://rss-feed.com/item-1/",
Published: "Wed, 31 Dec 2023 23:45:00 GMT",
Categories: []string{"keywordzz", "category2"},
},
}
rss := createRSS(items)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(rss))
}))
defer server.Close()

err = os.WriteFile(path.Join(listsDir, "default"),
[]byte(fmt.Sprintf("%d %s\n",
defaultCurrentTime.Unix(), server.URL+"/rss",
),
), 0600)
if err != nil {
t.Fatal(err)
}

feed := internal.NewTerminalFeed(timeMock, printer, storage)
feed.SetAgent("cleed/test")

root, err := NewRoot("0.1.0", timeMock, printer, storage, feed)
assert.NoError(t, err)

os.Args = []string{"cleed", "--search", "keyword"}

err = root.Cmd.Execute()
assert.NoError(t, err)
assert.Equal(t, `RSS Feed • zzzz
15 minutes ago https://rss-feed.com/item-1/
RSS Feed • keywords
15 minutes ago https://rss-feed.com/item-1/
RSS Feed • Keyword 1
15 minutes ago https://rss-feed.com/item-1/
`, out.String())
}

func Test_Feed_With_Summary(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
Expand Down
25 changes: 25 additions & 0 deletions cmd/cleed/utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,31 @@ func localStorageCleanup(t *testing.T, storage *storage.LocalStorage) {
}
}

type FeedItem struct {
Title string
Link string
Published string
Categories []string
}

func createRSS(items []*FeedItem) string {
var itemsStr = ""
for _, item := range items {
itemsStr += "<item><title>" +
item.Title + "</title><link>" +
item.Link + "</link><pubDate>" +
item.Published + "</pubDate>"
for i := range item.Categories {
itemsStr += "<category>" + item.Categories[i] + "</category>"
}
itemsStr += "</item>"
}
return `<rss version="2.0"><channel>
<title>RSS Feed</title>
<description>Feed description</description>
<link>https://rss-feed.com/</link>` + itemsStr + "</channel></rss>"
}

func createDefaultRSS() string {
return `<rss version="2.0">
<channel>
Expand Down
100 changes: 78 additions & 22 deletions internal/feed.go
Original file line number Diff line number Diff line change
Expand Up @@ -355,26 +355,26 @@ func (f *TerminalFeed) ExportToOPML(path, list string) error {
return nil
}

func (t *TerminalFeed) ShowConfigPath() error {
path, err := t.storage.JoinConfigDir("")
func (f *TerminalFeed) ShowConfigPath() error {
path, err := f.storage.JoinConfigDir("")
if err != nil {
return utils.NewInternalError("failed to get config path: " + err.Error())
}
t.printer.Println(path)
f.printer.Println(path)
return nil
}

func (t *TerminalFeed) ShowCachePath() error {
path, err := t.storage.JoinCacheDir("")
func (f *TerminalFeed) ShowCachePath() error {
path, err := f.storage.JoinCacheDir("")
if err != nil {
return utils.NewInternalError("failed to get cache path: " + err.Error())
}
t.printer.Println(path)
f.printer.Println(path)
return nil
}

func (t *TerminalFeed) ShowCacheInfo() error {
cacheInfo, err := t.storage.LoadCacheInfo()
func (f *TerminalFeed) ShowCacheInfo() error {
cacheInfo, err := f.storage.LoadCacheInfo()
if err != nil {
return utils.NewInternalError("failed to load cache info: " + err.Error())
}
Expand All @@ -384,8 +384,8 @@ func (t *TerminalFeed) ShowCacheInfo() error {
cellMax[0] = max(cellMax[0], len(k))
items = append(items, v)
}
t.printer.Print(runewidth.FillRight("URL", cellMax[0]))
t.printer.Println(" Last fetch Fetch after")
f.printer.Print(runewidth.FillRight("URL", cellMax[0]))
f.printer.Println(" Last fetch Fetch after")
slices.SortFunc(items, func(a, b *storage.CacheInfoItem) int {
if a.URL < b.URL {
return -1
Expand All @@ -396,9 +396,45 @@ func (t *TerminalFeed) ShowCacheInfo() error {
return 0
})
for i := range items {
t.printer.Print(runewidth.FillRight(items[i].URL, cellMax[0]))
t.printer.Printf(" %s %s\n", items[i].LastFetch.Format("2006-01-02 15:04:05"), items[i].FetchAfter.Format("2006-01-02 15:04:05"))
f.printer.Print(runewidth.FillRight(items[i].URL, cellMax[0]))
f.printer.Printf(" %s %s\n", items[i].LastFetch.Format("2006-01-02 15:04:05"), items[i].FetchAfter.Format("2006-01-02 15:04:05"))
}
return nil
}

type FeedOptions struct {
List string
Query [][]rune
Limit int
Since time.Time
}

func (f *TerminalFeed) Search(query string, opts *FeedOptions) error {
summary := &RunSummary{
Start: f.time.Now(),
}
config, err := f.storage.LoadConfig()
if err != nil {
return utils.NewInternalError("failed to load config: " + err.Error())
}
opts.Query = utils.Tokenize(query, nil)
if len(opts.Query) == 0 {
return utils.NewInternalError("query is empty")
}
items, err := f.processFeeds(opts, config, summary)
if err != nil {
return err
}
slices.SortFunc(items, func(a, b *FeedItem) int {
if a.Score > b.Score {
return 1
}
if a.Score < b.Score {
return -1
}
return 0
})
f.outputItems(items, config, summary, opts)
return nil
}

Expand All @@ -408,12 +444,7 @@ type FeedItem struct {
PublishedRelative string
FeedColor uint8
IsNew bool
}

type FeedOptions struct {
List string
Limit int
Since time.Time
Score int
}

type RunSummary struct {
Expand Down Expand Up @@ -449,10 +480,22 @@ func (f *TerminalFeed) Feed(opts *FeedOptions) error {
}
return 0
})
config.LastRun = f.time.Now()
f.storage.SaveConfig()
f.outputItems(items, config, summary, opts)
return nil
}

func (f *TerminalFeed) outputItems(
items []*FeedItem,
config *storage.Config,
summary *RunSummary,
opts *FeedOptions,
) {
l := len(items)
if l == 0 {
f.printer.ErrPrintln("no items to display")
return nil
return
}
if opts.Limit > 0 {
l = min(len(items), opts.Limit)
Expand Down Expand Up @@ -483,13 +526,10 @@ func (f *TerminalFeed) Feed(opts *FeedOptions) error {
"\n\n",
)
}
config.LastRun = f.time.Now()
f.storage.SaveConfig()
if config.Summary == 1 {
summary.ItemsShown = l
f.printSummary(summary)
}
return nil
}

func (f *TerminalFeed) printSummary(s *RunSummary) {
Expand Down Expand Up @@ -568,11 +608,19 @@ func (f *TerminalFeed) processFeeds(opts *FeedOptions, config *storage.Config, s
if !opts.Since.IsZero() && feedItem.PublishedParsed.Before(opts.Since) {
continue
}
score := 0
if len(opts.Query) > 0 {
score = utils.Score(opts.Query, f.tokenizeItem(feedItem))
}
if score == -1 {
continue
}
items = append(items, &FeedItem{
Feed: feed,
Item: feedItem,
FeedColor: color,
IsNew: feedItem.PublishedParsed.After(ci.LastFetch),
Score: score,
})
}
if res.Changed {
Expand All @@ -595,6 +643,14 @@ func (f *TerminalFeed) processFeeds(opts *FeedOptions, config *storage.Config, s
return items, nil
}

func (f *TerminalFeed) tokenizeItem(item *gofeed.Item) [][]rune {
tokens := utils.Tokenize(item.Title, nil)
for i := range item.Categories {
tokens = utils.Tokenize(item.Categories[i], tokens)
}
return tokens
}

func (f *TerminalFeed) parseFeed(url string) (*gofeed.Feed, error) {
fc, err := f.storage.OpenFeedCache(url)
if err != nil {
Expand Down
Loading

0 comments on commit 9ece8a6

Please sign in to comment.