voicepeak邪神ちゃんとpywin32を使って邪神ちゃんがニコ生のコメントを読み上げるプログラムを作った(後編)

# 初めに
前回までにvoicepeakをバックグラウンドで自動操作する関数を実装することはできました。
今回は、ニコニコ生放送のコメントをPythonで取得したり、私なりに使いやすくしようとした細々とした調整を紹介したりします。
前編はこちらです。

https://qiita.com/boxfish_jp/items/08298c724865577f3dde

# pythonでのニコ生のコメント取得
いきなりですが、コードを示したいと思います。
``` nicoCommentGet.py
def nicoCommentGet():
    ### コメントを取得したい放送のURLを指定
    live_id = "co5043209"
    #live_id = "co3000390" # コミュニティIDを指定すると放送中のものを取ってきてくれる
    url = "https://live2.nicovideo.jp/watch/"+live_id

    ### htmlを取ってきてWebSocket接続のための情報を取得
    response = requests.get(url)
    soup = BeautifulSoup(response.text, "html.parser") 
    embedded_data = json.loads(soup.find('script', id='embedded-data')["data-props"])
    urlSystem = embedded_data["site"]["relive"]["webSocketUrl"]

    ### websocketでセッションに送るメッセージ
    message_system_1 = {"type":"startWatching",
                        "data":{"stream":{"quality":"abr",
                                        "protocol":"hls",
                                        "latency":"low",
                                        "chasePlay":False},
                                "room":{"protocol":"webSocket",
                                        "commentable":True},
                                "reconnect":False}}
    message_system_2 ={"type":"getAkashic",
                    "data":{"chasePlay":False}}
    message_system_1 = json.dumps(message_system_1)
    message_system_2 = json.dumps(message_system_2)

    ### コメントセッション用のグローバル変数
    uri_comment = None
    message_comment = None

    ### 視聴セッションとのWebSocket接続関数
    async def connect_WebSocket_system(urlSystem):
        global uri_comment
        global message_comment

        ### 視聴セッションとのWebSocket接続を開始
        async with websockets.connect(urlSystem) as websocket:

            ### 最初のメッセージを送信
            await websocket.send(message_system_1)
            await websocket.send(message_system_2) # これ送らなくても動いてしまう？？
            print("SENT TO THE SYSTEM SERVER: ",message_system_1)
            print("SENT TO THE SYSTEM SERVER: ",message_system_2)

            ### 視聴セッションとのWebSocket接続中ずっと実行
            while True:
                message = await websocket.recv()
                message_dic = json.loads(message)
                print("RESPONSE FROM THE SYSTEM SERVER: ",message_dic)

                ### コメントセッションへ接続するために必要な情報が送られてきたら抽出してグローバル変数へ代入
                if(message_dic["type"]=="room"):
                    uri_comment = message_dic["data"]["messageServer"]["uri"]
                    threadID = message_dic["data"]["threadId"]
                    message_comment = [{"ping": {"content": "rs:0"}},
                                        {"ping": {"content": "ps:0"}},
                                        {"thread": {"thread": threadID,
                                                    "version": "20061206",
                                                    "user_id": "guest",
                                                    "res_from": -150,
                                                    "with_global": 1,
                                                    "scores": 1,
                                                    "nicoru": 0}},
                                        {"ping": {"content": "pf:0"}},
                                        {"ping": {"content": "rf:0"}}]
                    message_comment = json.dumps(message_comment)

                ### pingが送られてきたらpongとkeepseatを送り、視聴権を獲得し続ける
                if(message_dic["type"]=="ping"):
                    pong = json.dumps({"type":"pong"})
                    keepSeat = json.dumps({"type":"keepSeat"})
                    await websocket.send(pong)
                    await websocket.send(keepSeat)

    ### コメントセッションとのWebSocket接続関数
    async def connect_WebSocket_comment():
        loop = asyncio.get_event_loop()

        global uri_comment
        global message_comment

        ### 視聴セッションがグローバル変数に代入するまで1秒待つ
        await loop.run_in_executor(None, time.sleep, 1)

        ### コメントセッションとのWebSocket接続を開始
        async with websockets.connect(uri_comment) as websocket:

                ### 最初のメッセージを送信
            await websocket.send(message_comment)
            print("SENT TO THE COMMENT SERVER: ",message_comment)

            ### コメントセッションとのWebSocket接続中ずっと実行
            while True:
                message = await websocket.recv()
                message_dic = json.loads(message)
                if "chat" in message_dic:
                    print(message_dic["chat"]["content"])
                    if (start):
                        while(speaking):
                            pass
                        sendVoicePeak(message_dic["chat"]["content"])
                    else:
                        print("false")
                else:
                    print("RESPONSE FROM THE COMMENT SERVER: ",message_dic)

    
    
    asyncio.new_event_loop().run_in_executor(None,wait)
    ### asyncioを用いて上で定義した2つのWebSocket実行関数を並列に実行する
    loop = asyncio.get_event_loop()
    gather = asyncio.gather(
    connect_WebSocket_system(urlSystem),
    connect_WebSocket_comment(),
    )
    loop.run_until_complete(gather)
```
https://qiita.com/nouyakko/items/e4b24484c5f7eae0a3d1

このページを参考にさせていただきました。websocketに関する部分はこのページで紹介されているので、省略させて頂いて、元のソースコードから変更した部分を紹介したいと思います。

変更したのは` While True `の中のみです。
``` change.py
### コメントセッションとのWebSocket接続中ずっと実行
while True:
    message = await websocket.recv()
    message_dic = json.loads(message)
    if "chat" in message_dic:
        print(message_dic["chat"]["content"])
        if (start):
            while(speaking):
                pass
            sendVoicePeak(message_dic["chat"]["content"])
        else:
            print("false")
    else:
        print("RESPONSE FROM THE COMMENT SERVER: ",message_dic)
```
` message_dic["chat"]["content"] `にはコメントのみの情報が含まれています。なぜ、コメントのみの情報になるのかや、` if "chat" in message_dic`の処理をかませている理由が分からない方は、以下の記事をご覧ください。

https://qiita.com/boxfish_jp/items/db2ca74f7e38d14021c1

その後、`if (start):`という文がありますが、放送が始まって、時間が経過した後にコメントを取得すると、過去のコメントが一気に流れてきてしまい、邪神ちゃんが全部それを読み上げようとしてしまうので、プログラムを立ち上げてから5秒以内は`start`を`false`にしておくプログラムを別で作りました。
``` wait.py
start = False
def wait():
    global start
    time.sleep(5)
    start = True
```
こうすることでwebsocketが開通し、一気にコメントが流れてくる時間の間は、`start`が`false`なので、邪神ちゃん読み上げプログラムが書かれている`sendVoicePeak()`関数にコメントが流れて、邪神ちゃんに大量のコメントを送って、邪神ちゃんを困らせないようにしています。
そして、
``` waitSpeaking.py
while(speaking):
    pass
sendVoicePeak(message_dic["chat"]["content"])
```
この部分の`while(speaking):`ですが、これは邪神ちゃんがコメントを読み上げている間に次のコメントが入力されることを防止するためのものです。
前回紹介したSendVoicePeak関数に少し手を加えました。
``` sendVoicePeak.py
speaking = False
def sendVoicePeak(message):
    global speaking
    speaking = True
    message = replaceUrls(message)
    #ウインドウハンドルを取得
    VoicePeak = win32gui.FindWindow(None, "VOICEPEAK")  #voicepeakのウインドウハンドルを取得
    JUCE = win32gui.FindWindow(None,"JUCEWindow")       #voicepeak(juce)のウインドウハンドルを取得
    time.sleep(0.05)
    #メッセージを入力
    win32gui.SendMessage(JUCE, win32con.WM_ACTIVATE,2,0)    #ウインドウをアクティブ化
    win32gui.SendMessage(VoicePeak,win32con.WM_SETFOCUS,0,0)    #テキストエリアにフォーカスする
    for s in message:
        print(s);
        win32gui.SendMessage(VoicePeak, win32con.WM_CHAR,ord(s),0)  #読み上げたい文字を送信
        time.sleep(0.05)
    win32gui.SendMessage(VoicePeak,win32con.WM_KILLFOCUS,0,0)   #テキストエリアのフォーカスを外す

    time.sleep(0.5)
    #再生
    win32gui.SendMessage(VoicePeak,win32con.WM_CHAR,32,0)   #スペースを送信して、読み上げを実行
    nowPlaying();

    #入力した文字を削除
    win32gui.SendMessage(VoicePeak,win32con.WM_SETFOCUS,0,0)    #テキストエリアにフォーカスする
    for s in message:
        win32gui.SendMessage(VoicePeak,win32con.WM_KEYDOWN,0x27,0)  #右矢印キーを入力
        win32gui.SendMessage(VoicePeak,win32con.WM_KEYDOWN,8,0)     #バックスペースを入力
        time.sleep(0.05)
    win32gui.SendMessage(VoicePeak,win32con.WM_KILLFOCUS,0,0)
    speaking = False
```
関数の実行中は`speaking`を`True`にすることで、コメント取得プログラムの中にある、`while(speaking):`が関数が実行されているときだけ、無限ループして、SendVoicePeak関数が実行されないようにしています。

# 細かい調整
## voicePeak起動時の調整
voicepeakが起動したとき、一度だけvoicePeakのテキストエリアをクリックしないと、なぜか読み上げプログラムが上手く動きませんでした。毎回起動したあと、クリックするのが面倒くさいので、win32apiの力を使って自動化しました。
``` setupVoicePeak.py
def setupVoicePeak():
    VoicePeak = win32gui.FindWindow(None, "VOICEPEAK")  #voicepeakのウインドウハンドルを取得
    rect = win32gui.GetWindowRect(VoicePeak)
    nowPos = win32api.GetCursorPos()
    win32api.SetCursorPos((rect[0]+161, rect[1]+167))
    win32api.mouse_event(win32con.MOUSEEVENTF_LEFTDOWN, rect[0]+161, rect[1]+167, 0, 0)
    win32api.mouse_event(win32con.MOUSEEVENTF_LEFTUP, rect[0]+161, rect[1]+167, 0, 0)
    win32api.SetCursorPos(nowPos)
    sendVoicePeak("起動しました")
```
関数実行時に邪神ちゃんが「起動しました」と言ってくれたら、立ち上げに成功しています。

# ソースコード全体の紹介
こちらに載せています。ソースコードはかなり読みにくいですが、お許しを。

https://github.com/boxfish-jp/niconamaJyasinnYomiage

# 分からない点はこちらへ
毎日配信しているので、生放送のコメントで教えてもらえると対応がしやすいです。

https://www.nicovideo.jp/user/98746932/

https://www.twitch.tv/boxfish_jp/videos

(同時配信をしているので、どちらかお好きな方でコメントを）