Sinatraでニコニコ動画の投稿◯周年ちょうどの動画一覧を表示するWebアプリを作った

# 作ったもの

Rubyの勉強の一環として，Nicoversary!というWebアプリを作りました。
(タイトルは Niconico + Anniversary から付けています)

### どんなアプリ?

ニコニコ動画の動画では，「〇〇周年記念にまた見に来た」という内容のコメントを見かけます。
特定の日付に投稿された動画を再生数順で表示できるアプリがあったら，古い動画もまた盛り上がるのかなと思い，作りました。


### Herokuにデプロイしたもの

https://nicoversary.herokuapp.com/
※Heroku側の仕様が変わったようで，今は見られなくなってしまいました…


### GitHubリポジトリ

https://github.com/kg9n3n8y/nicoversary


### 紹介動画(ニコニコ動画)

https://www.nicovideo.jp/watch/sm38788418


## 参考イメージ

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/632954/25e62dee-7601-b35c-2437-391089dd27b8.png" width="640px">


## 機能

"/"(ルートパス)にアクセスすると，投稿日が今日の日付の動画を検索し，再生数順に最大100件表示します。
ただし，重くなりすぎないよう10000再生以上の動画に絞って検索しています。
また，各動画が投稿何周年かがわかるようになっています。


### 日付検索・タグ検索

"1日前"や"1日後"ボタンを押すたびに，検索する日付が変わります。
タグ一覧からタグを選ぶと，特定のタグのついた動画のみに絞って検索できます。
一番上のタイトルロゴをクリックすることで，トップ画面に戻ります。


### さらに高度な検索

検索時のURLを見ると分かるのですが，検索条件を自分でURL欄に入力することもできます。
(入力フォームを入れても良かったのですが，そこまで使わない上にデザインを崩すことになるので，やめておきました)

- 日付検索は "/2021/5/24/"
- タグ検索は "/VOCALOID"
- 日付かつタグ検索は "/2021/5/24/VOCALOID"

リストに存在しないタグを自分で打ち込んで検索することもできます。
ただし，2026年以降の検索になるとクエリが大きくなりすぎて検索できません。

また，[現存する最古の動画](https://www.nicovideo.jp/watch/sm9)が2007年の動画であるため，2008年よりも前の西暦では検索できなくしてあります。


# 開発環境

主に，以下のような環境で作りました。

- ruby 2.6.3
- bundler 2.2.17
- sinatra 2.1.0
- slim 4.1.0

また，以下のAPIを使わせていただきました。

https://site.nicovideo.jp/search-api-docs/snapshot

ツールとかデプロイ先は，初心者なのでこんな組み合わせでやってます。

- エディタは VS Code
- Git のプッシュは Sourcetree を使用
- デプロイ先は無料の Heroku


# 中身の紹介

SinatraはRubyのフレームワークの中でも軽量なことが特徴です。
今回はAPIを叩くだけのDBを使わないアプリなので，主なファイルが3つで済みました。

- app.rb (ルーティング 兼 Controller)
- views/index.slim (View)
- public/main.css (CSS)


## ルーティング

Railsとはちょっと違う書き方です。

```ruby:app.rb
require 'sinatra'
...(略)...

def nico_search(year, month, day ,tag)
...(略)...
end

# "/2021/5/24/VOCALOID" みたいなURLをそれぞれ変数に入れてビューを表示
get '/*/*/*/*' do |year, month, day, tag|
	@year = year.to_i
	redirect to('/') if @year < 2008 || 2025 < @year
	@month = month.to_i
	@day = day.to_i
	@tag = tag
	today = Date.new(@year, @month, @day)
	year = today.strftime("%Y")
	month = today.strftime("%m")
	day = today.strftime("%d")
	@movies = nico_search(year, month, day ,tag)
	slim :index
end

# "/VOCALOID" みたいなURLを変数に入れてビューを表示
# "/" であっても変数を空にして検索を行い，ビューを表示
get '/*' do |tag|
	today = Date.today
	year = today.strftime("%Y")
	month = today.strftime("%m")
	day = today.strftime("%d")
	@year = year.to_i
	@month = month.to_i
	@day = day.to_i
	@tag = tag
	@movies = nico_search(year, month, day ,tag)
	slim :index
end
```

ルーティングには`*`のようなワイルドカードを使うことができます。
また，上から順に照らし合わせて最初にマッチしたものを実行するので，`get '/*/*/*/*' do`を先に`get '/*' do`より先に書いています。
(逆にすると"/2021/5/24/VOCALOID"に対して`tag = "2021/5/24/VOCALOID"`で実行されてしまう)

## コントローラー(というか検索する関数)

ルーティングの上に`nico_search`関数を用意し，APIを叩いています。

```ruby:app.rb
def nico_search(year, month, day ,tag)
	# APIのエンドポイント
	api = "https://api.search.nicovideo.jp/api/v2/snapshot/video/contents/search"
	# タグ名(クエリ用にエンコードする必要がある)
	keyword = URI.encode_www_form_component(tag)
	# 西暦何年まで遡るか
	start_year = 2007
	# タグの完全一致のみ検索
	targets = "tagsExact"
	# 叩いたAPIから帰ってくる値の種類を設定
	fields = "contentId, title, startTime, thumbnailUrl, viewCounter, commentCounter"
	# 再生数これ以下は検索しない
	viewcounts = "10000"
	# 再生数の降順で並び替え
	sort = "-viewCounter"
	# 取得する動画数(最大100)
	limit = "100"
	# アプリ名
	appname = "Nicoversary"

	# それぞれの年の特定の日付だけの検索条件フィルター(JSON形式にしてエンコード)
	date_filter = []
	(start_year..year.to_i).each do |y|
		date_filter << {
			"type": "range",
			"field": "startTime",
			"from": "#{y}-#{month}-#{day}T00:00:00+09:00",
			"to": "#{y}-#{month}-#{day}T23:59:59+09:00",
			"include_lower": true
		}
	end

	json_filter = { "type": "or", "filters": date_filter}.to_json

	json_filter = URI.encode_www_form_component(json_filter)

	# APIを叩いて，検索結果を受け取る
	url = "#{api}?q=#{keyword}&targets=#{targets}&fields=#{fields}&filters[viewCounter][gte]=#{viewcounts}&jsonFilter=#{json_filter}&_sort=#{sort}&_limit=#{limit}&_context=#{appname}"
	uri = URI.parse(url)
	res = Net::HTTP.get(uri)
	res_json = JSON.parse(res)
	return res_json["data"]
end
```

詳しい使い方はニコニコ動画APIのガイドに書いてあります。

https://site.nicovideo.jp/search-api-docs/snapshot


## ビュー(一覧表示部分)

ビューは erb, haml, slim など，Railsと同じようにかけます。
今回は1ファイルに全て書きましたが，部分テンプレートのように一部を呼び出してレンダリングすることもできるようです。

受け取った動画情報(インスタンス変数`@movies`)は，以下のように処理しています。

```slim:index.slim
- i = 1
- @movies.each do |movie|
  a href="https://nico.ms/#{movie["contentId"]}"
    section
      img src="#{movie["thumbnailUrl"]}"
      p
        - anniversary = @year - movie["startTime"].split('-')[0].to_i
        = "＼ #{anniversary}周年 ／"
      p
        = movie["title"]
      p
        img src="/icon_play.svg" focusable="false"
        = movie["viewCounter"]
        img src="/icon_comment.svg" focusable="false" 
        = movie["commentCounter"]
      span
        = i
        - i += 1
```

実際にはそれぞれにクラスをつけ，レスポンシブになるようにCSSで`display: flex;`など調整することで見やすくしています。
(Flexboxでレスポンシブ対応するためのいいトレーニングになりました)


## CSS・画像

CSSファイルや画像は，アプリのルート直下にpublicフォルダを作ってそこに置くと，以下の例のような書き方で参照できるようです。

(例) `/public/picture.jpg`は，view側では`/picture.jpg`で参照できる。

CSSやファビコン画像も以下のように参照しています。

```slim:index.slim
link href="/favicon.ico" rel="icon"
link href="/main.css" rel="stylesheet" type="text/css"
```


# アプリのデプロイ

GitからHerokuにPushするだけで，特別なことはしていないです。
以下のようなファイルをアプリのルート直下に作りました。

```text:Procfile
web: bundle exec rackup config.ru -p $PORT
```

```ruby:config.ru
require './app.rb'
run Sinatra::Application
```

### 参考記事

https://qiita.com/yukihigasi/items/3b2307061e15996a84ca


私の場合はGitからHerokuへのPush時にエラーが出て，以下の記事にお世話になりました。

https://qiita.com/daiki457110/items/cfbd78a4267dbfa5d3f1

```zsh
$ bundle lock　--add-platform x86_64-linux
```


# 感想

きっかけは思いつきで「こういうのあったらいいな」→「Sinatraで手軽に作れないかな」と考えたことからでした。
せっかくなので，RailsでWebアプリ開発する息抜きに，Sinartaも使ってみようということで作りました。

制作期間は，Sinatraの使い方を勉強し始めてから3日。
ファイル数も少なく，小さめなWebアプリならあっという間にできました。

勝手に色んなものを生成しない分，Railsよりも軽くて小さいものなら手軽に作れそうです。
またSinatraで作りたいアイデアができたら，触ってみたいと思います。