アニメ一覧サイト「らごインフォ」作ってみた

#うずらインフォって知っていますか？
[うずらインフォ](https://uzurainfo.han-be.com/index.html)

2010年辺りからオタクをやってた人間が今期のアニメをチェックしようとするとTwitterで大体流れてくるアニメの今期まとめ画像を作成してくれるサイトです．
↓こういうの[^2013]
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/263565/5551bcc3-06c2-b7bb-dd32-0a4449cb82cf.png)

一覧性が高く，アニメーション制作・キャスト・スタッフなどがまとめられており，公式のサイトにも飛べるのでそこでざっと確認して気になったものをチェックできるので私は愛用していました．

これが2020年9月長きにわたる画像の生産を終了すると発表しました．

この後，企業系のWebサイトがアニメ一覧を作り始めましたがあんまり個人として気に入りませんでした．
理由は以下です．

- 一覧画像としてTwitter等で拡散できない
- 一覧としての可読性があまりにも悪すぎる
- 画像の大きさが統一されてなかったり，途中で広告挟んでみたりする
- 公式サイトではなくそのサイトの詳細ページに飛ばされるのでイラっとする
- ~~スタッフやアニメーション制作へのフォーカスがうずらインフォもあったからね～みたいなノリで腹が立つ~~

文句言って改善させるよりは，システムエンジニアなんだから自分で作ろうと思います．

**作りました**
うずらから少し大きな雷鳥へという想いで「らごインフォ」と名付けました．

https://lagoinfo.herokuapp.com

# 構成
### Django
今回はDjangoを採用．

ReactでナウでヤングでモダーンなWebサイト構築してやるぜ！って当初は思って本も買ったのですが，よくよく要件を確認してみると大した処理をする必要もないし，SPAにする必要も全く無いねってことに気が付いたので慣れたPythonで楽に作れそうなDjangoを選択しました．

SQL星で生まれ育っているのでORMは苦痛以外なんでもなかったですが，この機会に経験できてよかったと思います．二度と使いません．[^guchi]

アニメの画像が自分持ちではなく，他サイトから引っ張ってくる形なのでストレージをわざわざ用意する必要もないかなとwhitenoiseを使用して静的ファイルを配信しています．

### Heroku
インフラもあまり触りたくないのでHerokuを採用．

AWSだと事故で高額になってしまったりするが，Herokuは一番安い有料プランで$7.00固定なので個人で使うのに安心感があります．
使ってての感想は以下です．

メリット

- 圧倒的な定額の安心感
- GitHubから直接つなげてpushするだけでデプロイが済む
- Workerとか増やすのがとても楽

デメリット

- 非同期処理用にWorkerを作るとDynoが増えることになるので2倍料金がかかる
- アクセスが無いとサーバーが休止するので最初のアクセスでレスポンス性能が悪くなる
- DBが無料だと10,000レコードしか登録できないので，ちゃんと使おうと思ったら増やすことがほぼ必須(最低料金$9.00)
- 他にアプリを作ろうと思ったら別料金
- モニタリングのダッシュボードは最低限[^1]

### データ収集
**データが自動で集まってほしい**

毎日調べてデータベースを更新してみたいなことは私も仕事で忙しいのでやりたくない．
作ったらほぼ作りっぱなしで障害時のみ対応くらいの勢いにしたい．

そんなわがままを叶えるAPIありました，**[Annict API](https://developers.annict.jp/graphql-api/reference/)**

https://developers.annict.jp/graphql-api/reference/

Annictというサイトを運営しているDBをそのまま外にAPIで提供してくれているとてもありがたい存在です．

ただ，画像は提供していないのでこれについてはAnnictと一部紐づけられている海外サイト[MyAnimeList](https://myanimelist.net/)のREST APIを叩きます．
こちらは残念ながらOAuth認証だったので画面からの入力を必要とします．私の気が向いたら更新します．

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/263565/76068be6-58f7-9cf3-9553-7895fe83cbfa.png)

これにて基本的にはノータッチで情報が更新されていくように構築できた．

# 苦労ポイント
### OAuth認証
本来は実装するつもりはなかったのでかなり苦戦しました．

PKCEという方式を採用しており，技術仕様は[こちら](https://tools.ietf.org/html/rfc7636)

1.　コード検証機とコードチャレンジを作成する
2.　OAuthの認証リクエストを送る

```python
def get_auth_url(self):
    url, headers, body = oauth.prepare_authorization_request(
        MAL_CONSTS['authorize_url'],
        state=state,
        code_challenge=code_challenge)
    return url
```
3.　認証先で認証して，コードを取得する
4.　取得したコードでアクセストークンを取得

```python
url, headers, body = oauth.prepare_token_request(
            MAL_CONSTS['request_token_url'],
            client_secret=MAL_CONSTS['client_secret'],
            code=cd,
            code_verifier=code_challenge) #今回はplain認証
```

5.　アクセストークンを使用してapiを実行

```python
url = MAL_CONSTS['api_base_url'] + 'anime/' + str(mal_anime_id) + '?fields=main_picture'
url, headers, body = oauth.add_token(url)
req = urllib.request.Request(url, headers=headers)
try:
    res = urllib.request.urlopen(req)
    data = json.load(res)
```


### Debug=falseにすると出るバグ
🙀📞「どうしてDebugをfalseにした瞬間に動かなくなるんですか……？」

DjangoでDebugモードを解除すると動かなくなりました．
しかもDebugトレース画面もDebugモードのときにしか出ないのでなんで怒られているかがさっぱりわかりません．

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/263565/df8faf5a-8925-3f2c-f128-93bb960a393e.png)
親の顔より見たServer Error(500)

これをデバッグのときと同じ画面を出すようにしました．

```python:views.py
@requires_csrf_token
def my_customized_server_error(request, template_name='500.html'):
    import sys
    from django.views import debug
    error_html = debug.technical_500_response(request, *sys.exc_info()).content
    return HttpResponseServerError(error_html)
```

```python:urls.py
from django.urls import include,path
from muta import views

handler500 = views.my_customized_server_error

urlpatterns = [
    path('', include('muta.urls'), name='index'),
]
```
本番環境では消すことをお忘れなきよう．
こちらが参考になりました．

https://qiita.com/yuu-eguci/items/a1e4b0a2f238d5ccc985


### canvasの制限
画像を出力するのにcanvasを用いてスクリーンショットを撮って出力する方針を取っています．
実際のコードは以下です．

```javascript
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/0.4.1/html2canvas.js"></script>
<script>
  window.onload = function(){
      html2canvas(document.getElementById("animelist"),{
        proxy: true,
        useCORS: true,
        onrendered: function(canvas){
          var imgData = canvas.toDataURL('image/png');
          document.getElementById("screan_shot").href = imgData;
        }
      });
  }
</script>

```

今回，画像を外部のサイトから拾ってきているため，canvasの汚染を受けます．
そのため外部の画像をそのまま出力するためにはCORS（Cross-Origin Resource Sharing）を行う必要があります．

そこで便利なライブラリが[django-cors-headers](https://github.com/adamchainz/django-cors-headers)です．

```python:settings.py
INSTALLED_APPS = [
    'whitenoise.runserver_nostatic',
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'corsheaders',　#追加
    'django_rq',
    'muta.apps.MutaConfig',
]

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'whitenoise.middleware.WhiteNoiseMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.locale.LocaleMiddleware',
    "corsheaders.middleware.CorsMiddleware", #追加
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

CORS_ORIGIN_WHITELIST = [
    'https://myanimelist.net',
]

```

settings.pyを上のように追加すると自動でヘッダーを追加してくれるようになります．

参考にしました．

https://techblog.kayac.com/html2canvas

### 非同期処理
非同期処理も何故か引くほど詰まったので別で書いておきました．

https://qiita.com/penguinz222/items/e1b198d2ab174a522a81

# 謝辞
よっしゃ作るぞって言って，そのことを周りの人にも言い続けたのが完成させることができた大きな要因だと思っています．
友人，同僚，家族，美容師の兄ちゃんすべてに感謝の意を表します．

### 気が向いたら実装したい機能
最後に私の忘備録に近いですが，今回見送った・これあったら良いんじゃないかという機能を挙げておきます．

- Annictアカウントを連携し，個人の視聴情報を追加した画像を生成する機能
- Twitterに画像を投稿する機能
    - Tweeetボタンが画像を投稿するようになっていなくてめんどくさい

その他，これあったら面白いんじゃないかというアイデアあればお待ちしています．

[^2013]: 一部の人にぶっ刺さりそうな2013年冬アニメをチョイス
[^1]: 個人で使用する分には十分ですが
[^guchi]: 複合キーが使えない，最適化が面倒，結局発行されているSQLを確認するハメになる等々……