OBSで開始したYoutube Liveの配信URLをLINE公式アカウントで通知する(M1 Mac)

# はじめに
Youtube Liveを限定公開で配信する場合、チャンネルのSubscriberに配信URLが通知されない。
また、Youtube LiveとTwitterの連携が2019年で終了したようなので、Twitterでの通知も不可である。
Lumia Streamは配信開始を検知して好きなスクリプトを走らせたりAPIを叩いたりできるが、配信URLを収集していない(とDiscordで言われた)。従って配信URLを取得するスクリプトを書いた。
LINE通知については、Subscriberと主にLINEで繋がっているため選ばれただけで、おまけ程度である。

# システム概要

![image832.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/38265/3938b2e8-5326-632b-9a82-ea7e21dcf6bf.png)

1. OBSにて配信を開始するとOBS scriptがそれを検知
1. Python scriptを呼び出して配信URLをスクレイピングしOBS scriptに返す
1. URLをShell scriptに渡してLINE通知

# 開発環境
- M1 MacBook Air Ventura 13.4
- OBS Studio 29.1.1 (64bit)
- Python 3.9.6 64-bit

# スクリプト説明

## OBS script(Lua) で配信開始をトリガ
配信開始時にスクレイピングとLINE通知を行うためのスクリプトを作成するために、下記を参考にした。
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">OBSで配信開始時にツイート画面を表示するだけのスクリプトを書いてみました。初Luaなので動かなかったらすみません。<a href="https://t.co/C02zO7iKjx">https://t.co/C02zO7iKjx</a><br>記入例 <a href="https://t.co/CfvKDOV5ap">pic.twitter.com/CfvKDOV5ap</a></p>&mdash; レモングラス (@Iemonglass) <a href="https://twitter.com/Iemonglass/status/1335923488624820225?ref_src=twsrc%5Etfw">December 7, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

"script_xxx"系の名前のfunctionについては下記に説明がある。

https://docs.obsproject.com/scripting

OBSのイベントに関する定数は下記に説明がある。

https://docs.obsproject.com/reference-frontend-api

作成したスクリプト:
```lua:obsexec.lua
obs = obslua
settings = {}

-- Script hook for defining the script description
function script_description()
   return [[
配信開始と終了時にその旨を配信URLと一緒にLINEに通知するスクリプト。
ただし、LINEへの通知スクリプト(postMsgToLINE_start.command)と、配信URLを取得するスクリプト(getStreamingURL.py)は別ファイル

by maple0705
   ]]
end

-- Script hook for defining the settings that can be configured for the script
function script_properties()
   local props = obs.obs_properties_create()

   obs.obs_properties_add_text(props, "targetprofile", "対象プロファイル", obs.OBS_TEXT_DEFAULT)
   obs.source_list_release(scenes)
   
   return props
end

-- Script hook that is called whenver the script settings change
function script_update(_settings)	
   settings = _settings
end

-- Script hook that is called when the script is loaded
function script_load(settings)
   obs.obs_frontend_add_event_callback(handle_event)
end

function handle_event(event)
   if event == obs.OBS_FRONTEND_EVENT_STREAMING_STARTED then
      obs.script_log(obs.LOG_INFO, "配信を開始しました。")
      local currentprofile = obs.obs_frontend_get_current_profile()
      local savedprofile = obs.obs_data_get_string(settings, "targetprofile")

      if currentprofile == savedprofile then
         local streaming_url = get_streaming_url()
         obs.script_log(obs.LOG_INFO, streaming_url)

         post_MsgToLINE_start(streaming_url)
      end

   elseif event == obs.OBS_FRONTEND_EVENT_STREAMING_STOPPED then
      obs.script_log(obs.LOG_INFO, "配信を終了しました。")
      local currentprofile = obs.obs_frontend_get_current_profile()
      local savedprofile = obs.obs_data_get_string(settings, "targetprofile")

      if currentprofile == savedprofile then
         post_MsgToLINE_stop()
      end

   end
end

function get_streaming_url()
   local streaming_url = ""
   local python_path = "./getStreamingURL.py"
   local command = "python3 " .. python_path

   local handle = io.popen(command)
   streaming_url = handle:read("*a")
   handle:close()

   return streaming_url
end

function post_MsgToLINE_start(streaming_url)
   local command = "./postMsgToLINE_start.command " .. streaming_url
   local handle = io.popen(command)
   rep = handle:read("*a")
   handle:close()
end

function post_MsgToLINE_stop()
   local command = "./postMsgToLINE_stop.command "
   local handle = io.popen(command)
   rep = handle:read("*a")
   handle:close()
end
```

## Python script で配信URLを取得
スクリプトの骨格として下記を参考にした。

https://qiita.com/yuto-naga/items/c039efdfb465055f1a71

私の場合はSeleniumコンテナは使用せず、Youtubeにログイン済の自分のChromeを使用するので、そのChromeの起動方法は下記を参考にした("ソースコード"節)。

https://qiita.com/yaju/items/4a7c300670d69037f5bb#%E3%82%BD%E3%83%BC%E3%82%B9%E3%82%B3%E3%83%BC%E3%83%89

ただし、上記のコードでは、既にChromeが起動中の場合はスクリプトが落ちる。その対策は下記の通り:

https://teratail.com/questions/318077

作成したスクリプト:
```python:getStreamingURL.py
# execute below on terminal first:
# "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" --remote-debugging-port=9222
# and "lsof -i:9222" on terminal if you want to check

from selenium import webdriver
from selenium.webdriver.chrome import service as ChromeService
from selenium.webdriver.chrome.options import Options

from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait

from bs4 import BeautifulSoup

try:
    options = Options()
    options.add_argument("--user-data-dir=/Users/maple0705/Library/Application Support/Google/Chrome")
    options.add_argument("--profile-directory=Default")
    options.add_experimental_option("debuggerAddress", "127.0.0.1:9222")
    #options.add_argument("--headless")

    # chromeを起動
    chrome_service = ChromeService.Service(executable_path="/usr/local/bin/chromedriver")
    driver = webdriver.Chrome(service=chrome_service, options=options)
    # open new window
    driver.execute_script("window.open()")
    new_window = driver.window_handles[-1]
    driver.switch_to.window(new_window)

    # 自分の配信の管理画面のURLを開く
    driver.get("https://studio.youtube.com/channel/xxxxx/livestreaming/manage")

    # 定義済みの条件で待機する(最大20秒)
    WebDriverWait(driver, 20).until(
        # 指定したページ上の要素が読み込まれるまで待機
        EC.presence_of_element_located((By.TAG_NAME, "ytcp-video-row"))
    )

    # HTMLを文字コードをUTF-8に変換してから取得
    html = driver.page_source.encode('utf-8')

    # 取得したhtmlをBeautifulSoupで解析
    soup = BeautifulSoup(html, "html.parser")

    # 配信一覧を表示している要素だけに絞る
    section = soup.find("div", id="scroll-container", class_="style-scope ytls-broadcast-list-content")

    # 配信中ライブすべてを抽出する
    rowlist = section.find_all("div", id="row-container")
    # 配信中ライブのURL
    # for row in rowlist:
    #     elem = row.find(id="video-title")
    #     url = elem.get("href")
    #     print(url)

    # 最新ライブのurl
    elem = rowlist[0].find(id="video-title")
    url = "https://youtube.com" + elem.get("href")
    url = url.replace("/video", "/watch")
    url = url.replace("/livestreaming", "")
    print(url)

except Exception as e:
    print("**error**")
    print(e)
finally:
    # 終了
    driver.close()  # close tab of chrome
    driver.quit()
```

## Shell script でLINE公式アカウントからURLを通知
LINE Developers のAPIリファレンスにスクリプト例があるのでそれを拝借した。スクレイピングで取得したURLを受け取るので一部改変している。

https://developers.line.biz/ja/reference/messaging-api/#send-broadcast-message

```shell:postMsgToLINE_start.command
curl -v -X POST https://api.line.me/v2/bot/message/broadcast \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer {channel access token}' \
-d @- <<EOS
{
    "messages":[
        {
            "type":"text",
            "text":"配信を開始しました！見てね！"
        }, 
        {
            "type":"text",
            "text":"${1}"
        }
    ]
}
EOS
```