プログラミングのお題としてCrystalで簡単なコマンドラインRSSリーダーを作る

::: note info

これは [Crystal Advent Calendar 2023](https://qiita.com/advent-calendar/2023/crystal) の21日目の記事です。一つ前の記事は [Crsytalで実行が終了したらnotifyするコマンドラインツールを作る - その１](https://qiita.com/kojix2/items/5972178c46057fde0103)、次の記事は [挨拶を表示するツールを作りながら Crystalにおけるコマンドラインツールの作成 を考える](https://qiita.com/kojix2/items/71dfeaffa9b7004d75d6) です。

:::

世の中にはさまざまな「プログラミングのお題」があります。たとえばウェブアプリケーションだったらTODOアプリがよく作られていますね。

そのような「プログラミングのお題」のラインナップにRSSリーダーが加わって、「多くの人が一度は簡単なRSSリーダーを書いたことある」という世界になったらといいのになと考えることがあります。




# 最低限の例

まずは隗より始めよということで、私の Crystal の学習も兼ねて最低限のものを書いてみました。


```crystal
# my_rss_reader.cr

require "http/client"
require "xml"

def get_content(node : XML::Node, name : String) : String | Nil
  child = node.children.find { |child| child.name == name }
  child ? child.content : nil
end

url = "https://b.hatena.ne.jp/q/crystal?mode=rss&target=tag&sort=recent&date_range=5y&safe=on&users=1"

response = HTTP::Client.get(url)
doc = XML.parse(response.body)
node_rdf = doc.children[0]

node_rdf.children
  .select(&.element?)
  .select { |rdf_child| rdf_child.name == "item" }
  .each do |node_item|
    puts ""
    puts "--------"

    title = get_content(node_item, "title") || "title: ?"
    puts "● #{title}"
    puts ""

    url = get_content(node_item, "link") || "url: ?"
    puts url

    date = get_content(node_item, "date") || "date: ?"
    puts date

    puts ""

    desc = get_content(node_item, "description") || "description: ?"
    puts desc
  end
```

「RSSリーダー」というともっと立派なものをイメージするかもしれませんが（記事タイトルから想像したものと違っていたらすみません）、最低限ならこんなのでいいと思うんですよ。

フィードの例として、はてなブックマークで crystal タグが付けられた新着記事のフィードを使ってみました。

ターミナルで実行すると次のように Crystal に関する新着記事の一覧が表示されます。

```
  $ crystal my_rss_reader.cr

--------
● GitHub - elbywan/zap: Another [insert blazing fast synonyms] JavaScript package manager

https://github.com/elbywan/zap
2023-10-15T07:53:27Z

You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session. You switched accounts on another tab or window. Reload to refresh your session.

--------
● Crystal 1.10.0 is released!

https://crystal-lang.org/2023/10/09/1.10.0-released/
2023-10-10T14:35:27Z

We are delivering a new Crystal release with several bugfixes and improvements.
Pre-built packages are available on GitHub Releases and our official distribution channels.
See crystal-lang.org/install for installation instructions. Stats This release includes 82 changes since 1.9.2 by 21 contribu...

--------
● Crystal の小さな本 - Qiita

https://qiita.com/tadnakam/items/b1c2e32e86135db63c30
2023-10-08T12:52:24Z

続・Crystal の小さな本 もあります。
Crystal で Hello World! Crystal という Ruby ライクな言語は、C や Go の代わりとして使えます。
つまり、Ruby と異なりインタプリタでなく実行ファイルを生成するコンパイラ言語です。
インストール方法は Ubuntu の場合こちらのページになります。 2023/9 段階ではバージョン 1....

--------
● CrystalDoc.info - Crystal Shards API Documentation

https://crystaldoc.info/
2023-09-21T12:25:09Z

Recently Updated devnote-dev/xmlt (github) kereal/feedread (github) colinkey/crss (github) xeals/cryss (github) wyhaines/ParseDate.cr (github) fukaoi/crystal-ethereum (github) spider-gazelle/tflite_image (github) Globoplox/case_aware_json (github) Globoplox/blah (github) Globoplox/bmp (github) Ad...

--------
● GitHub - elbywan/crystalline: A Language Server Protocol implementation for Crystal. 🔮

https://github.com/elbywan/crystalline
2023-08-21T09:39:16Z

You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session. You switched accounts on another tab or window. Reload to refresh your session.

--------
● Rustの関数をRubyやCrystalから呼び出す：方法と課題 - 1日目 - Qiita

https://qiita.com/kojix2/items/302169c326bf3ce69130
2023-08-06T02:50:02Z

はじめに 最近では、新しいライブラリがRustで書かれるようになってきました。
これに伴い、RubyやCrystalなどの好みの言語からRustの関数を呼び出したくなることが増えます。
Python言語に対しては、バインディングが最初から提供されることも多いのですが、
それ以外の言語からRustの関数を呼び出したい場合、自身で作成...

--------
（以下略）
```

※ 記事の見た目の都合のため、一部手で改行を追加しています。ターミナルで見た場合は普通にターミナルの幅で折り返されます。



# 今回使ったライブラリ

標準ライブラリだけで済みました。今回も shards の使い方を覚えるチャンスを逃してしまった……。


## HTTP::Client

<!-- HTTP::Client - Crystal 1.10.1 -->
https://crystal-lang.org/api/1.10.1/HTTP/Client.html

ドキュメントに載っているサンプルコードの通りにやればOK。



## XML

<!-- XML - Crystal 1.10.1 -->
https://crystal-lang.org/api/1.10.1/XML.html

XML は若干めんどいですが、主役は XML::Node クラスで、今回の範囲であれば以下だけでなんとかなりました。

```
XML::Node#children
XML::Node#element? ... Returns true if this is an Element node.
XML::Node#name ... タグ名
XML::Node#content
```


# 改造してみよう

プログラミング学習のためにやることといえば改造ですよね！ 1に改造、2に改造です。

以下、適当に思いついた改造案を挙げてみました。




## 別のフィードに変えてみる

たとえば

```crystal
url = "https://b.hatena.ne.jp/q/crystal?mode=rss&target=tag&sort=recent&date_range=5y&safe=on&users=1"
```

の部分を

```crystal
url = "https://b.hatena.ne.jp/q/ruby?mode=rss&target=tag&sort=recent&date_range=5y&safe=on&users=1"
```

に書き換えると、 Crystal ではなく Ruby の新着記事が表示できるようになります。

他の適当な RSSフィードの URL に書き換えてうまく動くか試してみてもよいでしょう。




## 新しい順に番号を振る

昇順で表示されているのか降順で表示されているのか、パッと見で分かりにくいので番号を降ってみましょう。これは簡単そう。




## 日時の昇順で表示させる

コマンドラインで使う都合上、新しいものが下になっている方が便利かもしれません。




## 表示する量を減らす

- 固定件数で打ち切る
  - たとえば最新の10件だけ表示する
- 期間で制限する
  - たとえば直近1ヶ月のものだけ表示する




## 日時の表示を改善する

改造前は `2023-10-08T12:52:24Z` のような表示になっていて、以下の点が改善できそうです。

- `T` が邪魔なのでなくす
- UTC ではなく日本の日時に変換して表示する
- 「〜時間前」「〜日前」のように表示する



## その他、比較的簡単そうなもの

疲れてきたので以下箇条書きで……。

- 複数のフィードに対応する
  - 複数のフィードの内容を1つに混ぜ、最新のもの n 件を表示する
  - 複数のフィードのそれぞれについて最新のもの n 件を表示する
- Atom にも対応する
  - 今回のは RSS だけなので
  - ちなみに Qiita のフィードは RSS ではなく Atom（例: https://qiita.com/tags/crystal/feed ）
- フィードの URL を設定ファイルで管理する
- 取得したフィードをローカルにキャッシュする


## 立派な UI を作る

- ウェブUI
- デスクトップアプリ

参考:

<!-- 【Crystal】webviewでウェブサイトをアプリにする #Crystal - Qiita -->
https://qiita.com/kojix2/items/6fd4490ad9c1cc25c3a9

これを使えばウェブUI作って後からデスクトップアプリ化できそうですね。



## ターミナルに出力するのではなく、整形したテキストをメールで自分宛に送る

私は実際に昔 Ruby で作ったものを使っていて、大きなメンテナンスなしで10年以上使い続けています。

メールで自分に送って、スマホのメールアプリを自作 RSSリーダーのフロンドエンドとして使う形です。立派な UI 作るとメンテが大変だしこれで十分なのです。記事タイトルと本文の冒頭をざっと眺めて、気になるものがあったらリンクを開いてブラウザで読み、眺め終わったらメールごと削除する、という使い方。

- 取得した中からランダムに抽出して固定件数表示する
  - 最新の情報を1分1秒でも速く知りたい、というわけではない
  - 一度見たものが何度も流れてきても「あ、これはもう見たな」とスルーすればOK
- 既読管理しない
  - 「すべてに目を通さなきゃ」となるとしんどいので、そうならないように
- フィルタし放題
  - 見る必要がないとあらかじめ分かっているものはURLや件名などで好きなだけフィルタすればよい



# おわりに

簡単なコマンドラインRSSリーダーの最低限の例と改造案を書いてみました。

初心者に限らず、作ったことがない方は一度自作してみてはいかがでしょうか。または、新しい言語を覚える際に何か作りたいんだけどいつものやつは飽きたな、なんて時に思い出してみてください。


# バージョン

```terminal
  $ crystal version                                                                                                                                  
Crystal 1.10.1 [c6f3552f5] (2023-10-13)                                                                                                              
                                                                                                                                                     
LLVM: 15.0.7                                                                                                                                         
Default target: x86_64-unknown-linux-gnu  
```

# この記事を読んだ人は（ひょっとしたら）こちらも読んでいます

<!-- プログラミングのお題としてCrystalで簡単なbeepコマンドを作る #Crystal - Qiita -->
https://qiita.com/sonota88/items/fffe2588f8da15ac264d

<!-- 素朴な自作言語のコンパイラをCrystalに移植した #Crystal - Qiita -->
https://qiita.com/sonota88/items/275c2b5407986b100cc8