【構想編】discord.pyでニコニコ生放送の放送開始通知をDiscordに投稿するbot

1/30追記
完成しました
[【完成編】discord.pyでニコニコ生放送の放送開始通知をDiscordに投稿するbot](https://qiita.com/fctokyo1016/items/973f531c526fc65012b6)

# 初めに
Qiita初投稿になりますfctokyo1016ことリョウ（凌）と申します。 Twitter→[@fctokyo1016](https://twitter.com/fctokyo1016)
普段は主にPHPでの開発なんかを行ってます。
エンジニア歴数年のほぼ素人がなんとか作ったコードなので大目にみていただければ幸いです。

# 作ろうとした経緯
- ニコニコ生放送[10名ほどが所属する企画チーム](https://ch.nicovideo.jp/boxtv)に在籍していて、各々が自由に同じコミュニティで生放送が行える状態
- ニコ生のアラートが仕様変更で使えなくなってしまったので、他のメンバーの放送を見る際にURLを探すのが煩わしい
- メンバー全員が入っているDiscordのサーバーに通知が来れば楽なのでは？
- TwitchやYoutubeLiveなどのサイトは既に既存のbotがあるがニコニコには未対応
- なんとかインチキ（自作）できんのかね？

# 苦労した点
- 上記でも記載したがニコ生のアラート仕様変更でRSSもAPIもユーザー生放送には対応していない

# アイデア
- 過去に少しだけ触ったスクレイピングを使えないか
    - ちょうどPython勉強したかったし

# 参考にした記事
- [Pythonで実用Discord Bot(discordpy解説)](https://qiita.com/1ntegrale9/items/9d570ef8175cf178468f)

# 実行結果
実際にウチのメンバーの放送が通知されました
![Discord_2020-01-15_01-02-13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126452/8925bda5-87f6-3a3a-168e-d6b4f21f66f4.png)

# コード
```python:discordbot.py
import requests
import os.path
import re
import discord
import asyncio
from bs4 import BeautifulSoup

# 通知管理用ファイルパス指定
listFilePath = 'list.txt'

# 自分のBotのアクセストークンに置き換えてください
TOKEN = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXX'

# 任意のチャンネルID(int)
CHANNEL_ID = 0000000000000000

# 確認したいコミュニティを設定
targetCommunitys = ['co1520016']

# リスト内検索 あればTrue 無ければFalseを返す
def searchList(liveURL):
    # ファイル存在チェック
    if not os.path.exists(listFilePath):
        return False

    liveLV = liveIdExtraction(liveURL)

    #ファイル内チェック
    with open(listFilePath) as f:
        for i, line in enumerate(f):
            if line == liveLV + '\n':
                return True
    print(line)
    return False

# リストに放送ID追記
def addList(liveURL):
    liveLV = liveIdExtraction(liveURL)
    with open(listFilePath, 'a') as f:
        print(liveLV, file=f)

# 放送URLから放送ID(lvXXXXXXX)抽出
def liveIdExtraction(liveURL):
    repatter = re.compile('lv[0-9]+')
    return repatter.search(liveURL).group()

# 放送URLから放送タイトル取得
def getLiveTitle(liveURL):
    r = requests.get(liveURL)
    soup = BeautifulSoup(r.content, "html.parser")
    for meta_tag in soup.find_all('meta', attrs={'property': 'og:title'}):
        return meta_tag.get('content')

# 放送URLから放送者名取得
def getLiveName(liveURL):
    r = requests.get(liveURL)
    soup = BeautifulSoup(r.content, "html.parser")
    return soup.find("span",{"class":"name"}).text



# 接続に必要なオブジェクトを生成
client = discord.Client()

# 起動時に動作する処理
@client.event
async def on_ready():
    while(True):
        # ターゲットコミュニティの数だけ繰り返す
        for targetCommunity in targetCommunitys:
            # URLを設定
            r = requests.get("https://com.nicovideo.jp/community/" + targetCommunity)

            # コミュニティTOPページを確認
            soup = BeautifulSoup(r.content, "html.parser")
            result = soup.find("section", "now_live")

            # もし放送が始まっていれば
            if result is not None:
                # 放送URL取得
                liveURL = result.find("a", "now_live_inner").get("href")

                # リスト内を検索してすでに処理済みの放送IDであれば処理しない
                if searchList(liveURL) is False:
                    # 放送タイトル取得
                    liveTitle = getLiveTitle(liveURL)
                    # 放送者名取得
                    liveName = getLiveName(liveURL)

                    # Discordへ送信
                    channel = client.get_channel(CHANNEL_ID)
                    await channel.send('@everyone ' + liveName + 'さんが配信を開始しました\n\n' + liveTitle + '\n' + liveURL)

                    # 放送ID追記
                    addList(liveURL)

        # 1分待つ
        await asyncio.sleep(60)

# Discordに接続
client.run(TOKEN)
```

# やってること
- ニコニコ生放送のコミュニティページをスクレイピングして放送URLを取得
- Discordに通知
- 通知した放送は再通知しないようにテキストファイルに残す


一度処理した放送を残すテキストファイルのファイル名を指定してます。お好みでどうぞ

```python:discordbot.py
# 通知管理用ファイルパス指定
listFilePath = 'list.txt'
```

Discordの設定系です

```python:discordbot.py
# 自分のBotのアクセストークンに置き換えてください
TOKEN = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXX'

# 任意のチャンネルID(int)
CHANNEL_ID = 0000000000000000
```

処理対象のコミュニティを配列で指定します。複数コミュニティ対応です

```python:discordbot.py
# 確認したいコミュニティを設定
targetCommunitys = ['co1520016']
```

放送URLから既に通知済みか否かを判定します
同じ放送を何度も通知してしまうことを防ぐためのチェック用です

```python:discordbot.py
# リスト内検索 あればTrue 無ければFalseを返す
def searchList(liveURL):
    # ファイル存在チェック
    if not os.path.exists(listFilePath):
        return False

    liveLV = liveIdExtraction(liveURL)

    #ファイル内チェック
    with open(listFilePath) as f:
        for i, line in enumerate(f):
            if line == liveLV + '\n':
                return True
    print(line)
    return False
```

通知を行った放送をリストに追加します

```python:discordbot.py
# リストに放送ID追記
def addList(liveURL):
    liveLV = liveIdExtraction(liveURL)
    with open(listFilePath, 'a') as f:
        print(liveLV, file=f)
```

放送URLからスクレイピングで各種データを取得しています

```python:discordbot.py
# 放送URLから放送ID(lvXXXXXXX)抽出
def liveIdExtraction(liveURL):
    repatter = re.compile('lv[0-9]+')
    return repatter.search(liveURL).group()

# 放送URLから放送タイトル取得
def getLiveTitle(liveURL):
    r = requests.get(liveURL)
    soup = BeautifulSoup(r.content, "html.parser")
    for meta_tag in soup.find_all('meta', attrs={'property': 'og:title'}):
        return meta_tag.get('content')

# 放送URLから放送者名取得
def getLiveName(liveURL):
    r = requests.get(liveURL)
    soup = BeautifulSoup(r.content, "html.parser")
    return soup.find("span",{"class":"name"}).text
```

# 課題
- Herokuで実際に稼働させようとしたが、再起動される毎にローカルファイルが削除されてしまう仕様を後から知る
- 通知管理用ファイル(list.txt)が毎回消されてしまうので同じ放送を何度も通知してしまう可能性がある
- ~~Googleのspreadsheetなど外部でそのデータを持つ必要がある~~
    - Herokuは無料でPostgreSQLのデータベースを持てるらしい
    - ？？？「知らなかったそんなの・・・」