Python3でradikoと超A&Gと音泉と響を自動検索・録音するソフトを作った

2019-09-14 超A&G+のapi版について書いた
2019-10-30 いいね，ストックしていただけることがあるので記事を最新版にした
2020-01-18 Mysqlとreactで検索・視聴出来るWEBUIを作ってます。そのための更新とリファクタに合わせました

---

夏川椎菜さんの出演するラジオを取り逃がすことなく録音したいのです。


**centOS7を想定して作りましたが，現在はubuntu18.04で動かしてます**


### できること

- radiko, agar, onsen, hibikiの録音
- 設定ファイルから読み込み
- 番組表の取得，キーワードで録音（番組表はradiko, agqrのみ)
- 複数番組の同時録音
- systemctlでの自動起動，再起動
- 録画データをクラウドストレージに保存（id, passは設定ファイルに保存）
- 録画データをファイルに保存
- 録画データをオブジェクトストレージに保存
- 録画情報をMysqlに保存
- 録音完了をLINEで通知する


# 使い方

rtmpdump, ffmpegのインストール
pipenvをいれました

```
git clone https://github.com/sun-yryr/Rec-adio.git
cd Rec-adio
pipenv install
cp ./conf/example_config.json ./conf/config.json
```

config.jsonが設定ファイルになっているので，自分に合わせて設定します。
rec_adio.serviceも変更します

```
sudo mv ./rec_adio.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable rec_adio.service
sudo systemctl start rec_adio.service
```

<br><br><br><br><br><br>

# 以下開発メモです

https://github.com/sun-yryr/Rec-adio

## Radikoの録音
### radikoから番組表を取得，パースする

Xml.etree.ElementTreeを使う。

文字化けがあったので，下記にします。参考[^1]

```python
res = requests.get(url)
#下は遅かった
#res.encoding = res.apprarent_encoding
#決め打ちでいい
res.encoding = "utf-8"
```



`findall(path)`なので `findall("stations/station/progs_prog")`で番組情報まで検索してくれる。

多分この下のデータを使うかなぁ…… 

- 属性ftが，%Y%m%d%H%M%Sで開始時間
- 属性toが，上記で終了時間
- 属性ftlが，%H%Mで開始時間（翌朝4時が27時になるタイプ）
- 属性tolが，上記で終了時間
- 属性durが，番組時間（単位second）
- 子要素title，タイトル
- 子要素desc，番組情報？
- 子要素info，番組情報？
- 子要素pfm，出演者情報



キーワード検索のマッチングはreモジュールを使って正規表現する。~~推してくれ~~

```python
c = re.compile("(水瀬いのり|夏川椎菜)")
if(c.search(検索対象).text is not None):
		    処理
```



検索結果の返り値をこんな感じに決めた

```json
[{
    "station": 放送局id,
    "title": タイトル,
    "ft": 開始時間,
    "to": 終了時間,
    "ftl": あ,
    "tol": あ,
    "dur": 放送時間
}]
```



### 複数同時録音の実装

~~rtmpdumpをsubprocessで動かせばなんとかなりそうなのでそれでやってみる。~~

~~hlsはまた今度やる（多分コマンドを変えるだけなので）~~

嘘です。今後のこと（多分flashが死ぬ20年まではオタクを続ける予定なので）を考えてradikoはhlsで書く。



<details><summary>まず、Centos 7にffmpegを入れる人をする。</summary>
  多分これで通るはず<br><br>
  <code>sudo yum -y install epel-release</code><br>
  <code>sudo rpm -ihv http://awel.domblogger.net/7/media/x86_64/awel-media-release-7-6.noarch.rpm</code><br>
  <code>sudo yum install ffmpeg</code><br>
</details>

radikoのhls録音には認可を通さないといけないらしい[^2]

```python
auth1_url = "https://radiko.jp/v2/api/auth1"
auth2_url = "https://radiko.jp/v2/api/auth2"
headers = {
    "X-Radiko-App": "pc_html5",
    "X-Radiko-App-Version": "0.0.1",
    "X-Radiko-User": "sunyryr",
    "X-Radiko-Device": "pc"
}
res = requests.get(auth1_url, headers=headers)
#res.headers["キー"]でheaderの中身取れるので
AuthToken = res.headers["X-RADIKO-AUTHTOKEN"]
KeyLength = int(res.headers["X-Radiko-KeyLength"])
KeyOffset = int(res.headers["X-Radiko-KeyOffset"])
tmp_authkey = auth_key[KeyOffset:KeyOffset+KeyLength]
#base64エンコードからstr型に変換
AuthKey = base64.b64encode(tmp_authkey.encode('utf-8')).decode('utf-8')
headers = {
　　　　"X-Radiko-AuthToken": AuthToken,
　　　　"X-Radiko-PartialKey": AuthKey,
　　　　"X-Radiko-User": "sunyryr",
　　　　"X-Radiko-Device": "pc"
}
res = requests.get(auth2_url, headers=headers)
print(res.status_code) #200で成功
```



で，このAuthTokenが使えるようになったのでffmpegにぶん投げる。ffmpegの終了には[q]を投げろってあるけど別にctrl+cでも大丈夫だったのでこれで行く
**このトークン、何分有効か知らないので録音直前に取るようにした**



```python
import subprocess
from signal import SIGINT
cwd = "ffmpeg なんか"
p1 = subprocess.Popen(cwd, shell=True)
time.sleep(録音時間)
# ctrl+cを送る
p1.send_signal(SIGINT)
```

複数同時もできた。




### radikoのstreaming urlについて

翌日に鯖で実行したら404とかその辺のエラーが出たので調整。

https://gist.github.com/ji6czd/f86440200ba286f1f7af2e103dd430ff

参考になりました，ありがとうございます。



つまるところ，urlのlsidの値が変わるので，上の人のコードを流用させていただき動的に取得しました。



###録音のスケジューリング

~~えーどうやってやろう~~ mainの無限ループとdatetimeの比較で開始を判定する。

multiprocessingで関数を別のプロセスに投げればいい（返り値はいらないので）

強制終了が入った時用にスケジューリングは別ファイルに保存する形をとる。

> 予約時間から現在時間を引いて，負の値なら過ぎ去った日々。正の値ならあと何時間何分になる。



**今回の場合，multiprocessingのjoin()は使わない（投げっぱなしジャーマンなので）**








## 超A&G+（joqr）のやつ

~~こやつはapiがないので（あったら教えてください、agさん作って（泣））スクレイピングする。~~


**api勝手に作っちゃいました。** ぜひ使ってね
https://qiita.com/taittide/items/c5404b4684f342db2dde

radikoの場合と大体同じで，受け取ったjsonを正規表現で見て録音番組を検索します。設定ファイルの記述がちょっと楽になったかな。



### rtmpdumpでの録音

超A&G+は~~時代遅れ~~古きよきrtmpで配信しているので、rtmpdumpを使ってダウンロードしていきます。

<details><summary>まず、Centos 7にrtmpdumpを入れる人をする。</summary>
  ちょっと編集する場所がある<br><br>
  shell   <code>git clone git://git.ffmpeg.org/rtmpdump</code><br>
  shell   <code>cd rtmpdump/</code><br>
  <code>./Makefile</code>の中身、3行目くらいを<code>prefix=/usr</code>に変更する。<br>
  <code>./librtmp/Makefile</code>の中身、3行目くらいを<code>prefix=/usr</code>に変えるのと、7行目くらいを<code>libdir=$(prefix)/lib64</code>に変える<br>
  shell   <code>make</code><br>
  shell   <code>make install</code><br>
</details>

~~説明がめんどくさくなってしまった~~ そのうち書こうと思います



作業順

1. rtmpdumpで録音（録音時間を指定できる！）
2. ffmpegでaac形式に変換[^3]
3. 1で録音した.flvを削除








## 保存先の設定

`savedir/title/title_YYYY…m4a`の形式で保存したいので，

1. titleのフォルダがなければ作成
1. 絶対パスにしてshellの実行

という方法にします。フォルダの確認は`os.path.isdir(path)`でいける。









## systemctlの自動起動、再起動

このプログラムは常時起動が基本ですが、今のままだといちいちタスクキルをしないといけないので（しかもmultiprocessのせいで数が多い）systemdに任せる。めっちゃ楽



今回のプログラムは無限ループにしてるから，type=simpleでいいしpidfileを作らなくていいので…...

```
[Unit]
Description=Radiko & AGQR record script
After=network.target

[install]
WantedBy=multi-user.target

[Service]
Type=simple
ExecStart=pythonプログラムのパス
Restart=on-failure
User=ユーザー
```



初めて知ったんですが，stopなどを実装しなくてもsystemdさんがよしなにしてくれる（タスクキル？）神



```shell
sudo nano /etc/systemd/system/hogehoge.service
# 上のを書く
sudo systemctl daemon-reload
sudo systemctl start hogehoge.service
# 確認する。エラー出てないことを確認
sudo systemctl enable hogehoge.service　# 自動起動オン
```

`config.json `変えたら毎度 `systemctl restart hogehoge.service` しないといけない







## 音泉の録音

録音というよりはダウンロードなんだよなというお気持ちです。

簡単にいうと、作品が直mp3で配信されているのでurlを作成し、ダウンロードしてきます。

ダウンロードに関しては問題ないですが、ダウンロード済み作品をどう持つかが悩みどころ。

```python
# ダウンロード
res = requests.get("http://www.onsen.ag/api/shownMovie/shownMovie.json")
programs = json.loads(res.text)
for program in programs["result"]:
    url = "http://www.onsen.ag/data/api/getMovieInfo/%s" % program
    res2 = requests.get(url)
    prog = json.loads(res2.text[9:len(res2.text)-3])
    title = prog.get("title")
    personality = prog.get("personality")
    update_DT = prog.get("update")
    # 配信してない作品が紛れている
    if (title !="" and personality != "" and update_DT !=""):
        if (c.search(personality)):
            print(prog["update"], prog["title"], prog["personality"])
            movie_url = prog["moviePath"]["pc"]
            res3 = requests.get(movie_url)
            f = open(path+title+".mp3", "wb")
            f.write(res3.content)
            f.close()
```

うーん簡単そうなので、「n回目をファイル名に入れておいて、逐一確認する」で行こうかな。単推しなので

```python
# ファイル重複チェック
file_name = title +"#"+ count + ".mp3"
file_path = dir_path +"/"+ file_name
if not file_name in os.listdir(dir_path):
    保存とアップロード
```







## ついでに… 響-HIbiki Radio Stationの録音

こちらも録音というよりはダウンロードなんだよなというお気持ち

url探るの大変でしたがgoogle先生の力でできた。

*手順*

1. 番組一覧を出す
2. 1のidから再生用urlを生成する
3. ffmpegに投げる



~~こっちもダウンロード済み作品は「n回目をファイル名に」で行きます~~

クソか？第n回の欄に`毎週木曜日21:30より「FRESH! 響チャンネル」で配信開始！`ってマジで何考えてるんだ



更新日をファイル名にします。

id多すぎてまじで意味がわからん。episode>video>idが本体

こんな感じにすると取れる（詳しくはgithub）

```python
headers = {"X-Requested-With": "XMLHttpRequest"}
# 番組の取得
res = requests.get("https://vcms-api.hibiki-radio.jp/api/v1/programs", headers=headers)
tmpjson = json.loads(res.text)
url = "https://vcms-api.hibiki-radio.jp/api/v1/programs/" + tmpjson[0]["access_id"]
res2 = requests.get(url, headers=headers)
tmpjson = json.loads(res2.text)
video_url = "https://vcms-api.hibiki-radio.jp/api/v1/videos/play_check?video_id=" + str(tmpjson["episode"]["video"]["id"])
tmpjson = json.loads(res2.text)
cwd = 'ffmpeg -i "%s" -acodec copy "%s"' % (tmpjson["playlist_url"], "保存ファイル名")
p1 = subprocess.run(cwd, stdout=subprocess.DEVNULL, shell=True)
```







## 各種保存方法
2020-01-18改定

DropBoxの無料プランがオーバーしそうなので，オブジェクトストレージや外部ストレージなしを選択できるようにした。

処理順としては
1. Mysqlが存在かつオブジェクトストレージの設定ファイルがある → オブジェクトストレージに保存
2. DropBoxの設定ファイルがある → ファイルとDropBoxに保存
3. 何もない → ファイルのみ


### オブジェクトストレージ

conohaのやつ。Pythonライブラリを探したんですが，スクラッチで書いた方が楽だった。
`lib/functions.py` にあります。
トークンの更新

```python
data = {
    "auth": {
    "passwordCredentials": {
        "username": self.username,
        "password": self.password
    },
    "tenantId": self.tenantid
    }
}
res = requests.post(self.identityUrl + "/tokens",
    headers={"Content-Type" : "application/json"},
    data=json.dumps(data))
```

ファイルのアップロードも，httpでできるのでrequestsに全任せしました。ただし，content-typeをしっかりしないと読み込んでくれないことがあるので注意。

```python
f = open(filePath, "rb")
res = requests.put(Path,
    headers={
        "Content-Type" : "audio/mpeg",  # ここで送信するデータ形式を決める
        "X-Auth-Token": self.token
    },
    data=f.read())
```



### DropBox

アクセストークンの取得をする（[dropbox](https://www.dropbox.com/lp/developers) <-ここからAppの作成）

モジュールのインストールをする

```shell
sudo pip install dropbox
```

書く

```python
import dropbox
dbx = dropbox.Dropbox(アクセストークン)
dbx.users_get_current_account()
```

フォルダの存在確認とフォルダの作成が少しめんどくさい

```python
# フォルダリスト
res = dbx.files_list_folder('')
db_list = [d.name for d in res.entries]
# リストにradioってフォルダがないなら作成
if not "radio" in db_list:
    dbx.files_create_folder("radio")
```

dropboxのルートは`"/"`じゃなくて `"" ` なので気をつけよう

なお階層潜る時は`/hogehoge/hagehage`で



アップロードは割と簡単で、

```python
fs = open(file_path, "rb")
dbx.files_upload(fs.read(), dbx_path)
fs.close()
```

バイナリで開いて渡してあげるだけ。









## 録音完了の通知（line notify）

やっぱりLINEでしょ？

developerモードで通知するのは面白みに欠けるけど，これユーザー認証ないしいいか〜〜



1. [LINE Notify](https://notify-bot.line.me/ja/) ここからログインして個人用アクセストークンを生成する。

2. トークンをヘッダーに含めてhttp postリクエストを送る

   ```python
   # pythonならこれでok
   headers = {"Authorization": "Bearer ここにトークン"}
   payload = {"message": "送信するよ！"}
   requests.post("https://notify-api.line.me/api/notify", headers=headers, data=payload)
   ```

3. 終わり



# 感想
バグ等あったら更新します。
これで推しを推すことができる...... 

以下の記事を参考にさせていただきました。ありがとうございます！

[^1]: https://blog.aoshiman.org/entry/118/
[^2]: https://blog.bluedeer.net/archives/216
[^3]: https://qiita.com/cha84rakanal/items/e84fe4eb6fbe2ae13fd8