EPGStation での録画ルールを一括変更

EPGStation での録画ルール。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/139524/ad20cc7a-c5a4-6aa2-8b54-7c53d47ca026.png)

最初のうちは TS ファイルも残すルールで運用していたが、エンコード処理が問題がないので TS ファイルを自動削除するようにした。
さて、ポリシー変更以前に設定したルールについても、TSファイルを自動削除するように変更したい。GUI でチマチマやるのではなく、どうしたら自動化できるかな?

# 環境

 
- Version of EPGStation: 2.6.20
- Version of Mirakurun: 3.9.0-rc.2
- OS:Ubuntu Linux 20.04
- Architecture: x64

「PLEX PX-Q3PE4 で docker-mirakurun-epgstation を使いたい」
https://qiita.com/nanbuwks/items/640ee4405e1fdd2ca497
こちらで構築したものです。


# WebAPI

こういった処理は EPGStation では WebAPI を使って処理するらしい。
WebAPIのドキュメントはどこかな?

https://github.com/l3tnun/EPGStation/blob/master/doc/webapi.md

こちらによると、


> EPGStation が提供する WebAPI は express-openapi によって提供される 、OpenAPI (Swagger) 準拠の RESTful API です
> 利用可能な全ての API は Swagger UI 上で確認可能です
> http://<hostname>:<port>/api/debug


早速URLをブラウザで見てみると以下のように確認できた。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/139524/dafdd67f-9cd8-456e-1744-db18b21e5b74.png)

# WebAPI が反応するかチェック

まずは WebAPI のアクセスができるかどうかチェック。

「archlinuxを定期的に再起動する（録画中ならやめる）」
https://qiita.com/hirmr/items/9c63bc7672032c5bf050

で使っているテクをそのまま使い、curl で録画中かどうかのチェックを行いました。
```
$ curl http://192.168.42.100:8888/api/recording?isHalfWidth=true
```
返ってきた返事
```
{"records":[{"id":6139,"channelId":400101,"startAt":1674367800000,"endAt":1674370800000,"name":"BS世界のドキュメンタリー「マミー・フット!おばあちゃんはサッカー選手」[二][字][再]","isRecording":true,"isEncoding":false,"isProtected":false,"ruleId":62,"programId":40010127876,"description":"シニア女性たちによるフランス対南アフリカのサッカー特別試合。珍しい取り組みの舞台裏に密着。言葉や文化が異なっても、仲間とともに人生を楽しむ気持ちは変わらない。","extended":"◇番組内容\nシニア女性たちによるフランス対南アフリカのサッカー特別試合。その舞台裏に密着する。フランスのメンバーはほとんどがサッカー経験なしだが新しいことへの挑戦に張り切る。一方の南アフリカチームは女性がショートパンツでサッカーをすることへの偏見をはねのけて練習に励んできた。試合本番。言葉や文化の違いを超えて、仲間とともに人生を楽しむ気持ちを共有する。 原題:MAMIES FOOT(フランス 2020年)\n◇制作\n~2020年 フランス Old Stories Production/PV Senioriales Production制作~","rawExtended":{"番組内容":"シニア女性たちによるフランス対南アフリカのサッカー特別試合。その舞台裏に密着する。フランスのメンバーはほとんどがサッカー経験なしだが新しいことへの挑戦に張り切る。一方の南アフリカチームは女性がショートパンツでサッカーをすることへの偏見をはねのけて練習に励んできた。試合本番。言葉や文化の違いを超えて、仲間とともに人生を楽しむ気持ちを共有する。 原題:MAMIES FOOT(フランス 2020年)","制作":"~2020年 フランス Old Stories Production/PV Senioriales Production制作~"},"genre1":8,"subGenre1":7,"genre2":0,"subGenre2":2,"genre3":0,"subGenre3":5,"videoType":"mpeg2","videoResolution":"1080i","videoStreamContent":1,"videoComponentType":179,"audioSamplingRate":48000,"audioComponentType":3,"thumbnails":[],"videoFiles":[{"id":10632,"name":"TS","filename":"2023年01月22日15時10分00秒-ＢＳ世界のドキュメンタリー「マミー・フット！おばあちゃんはサッカー選手」[二][字][再].m2ts","type":"ts","size":0}]}],"total":1}
```
返事が返ってきたので WebAPI へのアクセスは問題無いようです。

# ルール情報を取得してみる


http://<サーバアドレス>/api/rules でルール情報を取得することができます。


|パラメータ|||
|:---:|:---:|:---:|
|offset|オフセット|Default:0|
|limit |リミット|Default:24|
|type |予約情報取得タイプ |Available values : all, normal, conflict, skip, overlap|
|keyword |キーワード||

ということで、適当にルール3つをリクエスト

```
$ curl http://192.168.42.100:8888/api/rules?offset=10\&limit=3\&type="all"\&keyword=""
```
とすると、

```
{"rules":[{"id":11,"isTimeSpecification":false,"searchOption":{"keyCS":false,"keyRegExp":false,"name":true,"description":true,"extended":false,"ignoreKeyCS":false,"ignoreKeyRegExp":false,"ignoreName":false,"ignoreDescription":false,"ignoreExtended":false,"GR":false,"BS":false,"CS":false,"SKY":false,"isFree":false,"keyword":"ガリレオ","genres":[{"genre":3,"subGenre":0}],"times":[{"week":127}]},"reserveOption":{"enable":true,"allowEndLack":true,"avoidDuplicate":false},"encodeOption":{"mode1":"H.264","encodeParentDirectoryName1":"recorded","isDeleteOriginalAfterEncode":true},"reservesCnt":0},{"id":12,"isTimeSpecification":false,"searchOption":{"keyCS":false,"keyRegExp":false,"name":true,"description":true,"extended":false,"ignoreKeyCS":false,"ignoreKeyRegExp":false,"ignoreName":false,"ignoreDescription":false,"ignoreExtended":false,"GR":false,"BS":false,"CS":false,"SKY":false,"isFree":false,"keyword":"NHKスペシャル","genres":[{"genre":8,"subGenre":0}],"times":[{"week":127}]},"reserveOption":{"enable":true,"allowEndLack":true,"avoidDuplicate":false},"encodeOption":{"mode1":"H.264","encodeParentDirectoryName1":"recorded","isDeleteOriginalAfterEncode":true},"reservesCnt":6},{"id":13,"isTimeSpecification":false,"searchOption":{"keyCS":false,"keyRegExp":false,"name":true,"description":true,"extended":false,"ignoreKeyCS":false,"ignoreKeyRegExp":false,"ignoreName":false,"ignoreDescription":false,"ignoreExtended":false,"GR":false,"BS":false,"CS":false,"SKY":false,"isFree":false,"keyword":"はたらく魔王さま","genres":[{"genre":7,"subGenre":0}],"times":[{"week":127}]},"reserveOption":{"enable":true,"allowEndLack":true,"avoidDuplicate":false},"encodeOption":{"mode1":"H.264","encodeParentDirectoryName1":"recorded","isDeleteOriginalAfterEncode":true},"reservesCnt":0}],"total":185}
```
取得できました

python で同様に取得してみます。requests モジュールを使いました。

```

import requests
import json
url = "http://192.168.42.100:8888/api/rules?offset=1&limit=1&type=all&keyword="
response = requests.get(url)
jsonData = response.json()
for jsonObj in jsonData["rules"]:
    jsonObj["encodeOption"]={"mode1":"H.264","encodeParentDirectoryName1":"recorded","isDeleteOriginalAfterEncode":"False"}
    print(json.dumps(jsonObj,indent=4))
    print(jsonObj["encodeOption"]["isDeleteOriginalAfterEncode"])


```

この python がうまく動いたので、ルールを修正した JSON に直すようにしたのが以下です。

```
import requests
import json

url = "http://192.168.42.100:8888/api/rules?offset=1&limit=100&type=all&keyword="
response = requests.get(url)
jsonData = response.json()
for jsonObj in jsonData["rules"]:
    if ("encodeOption" not in jsonObj):
      print("no encodeOption:",jsonObj["id"])
      jsonObj["encodeOption"]={"mode1":"H.264","encodeParentDirectoryName1":"recorded","isDeleteOriginalAfterEncode":True}
    if (True != jsonObj["encodeOption"]["isDeleteOriginalAfterEncode"]):
      print("isDeleteOriginalAfterEncode is not True:",jsonObj["id"])
      jsonObj["encodeOption"]["isDeleteOriginalAfterEncode"]=True
    # print(json.dumps(jsonObj,indent=4))
    print(jsonObj["encodeOption"]["isDeleteOriginalAfterEncode"])
```

この修正した JSON を登録するようにすればいいですね。

# 予約修正

先程は GET メソッドで JSON を取得しましたが、それを修正した JSON は PUT メソッドを使って登録する必要があります。ルール修正のAPIが少し難しかったので、先に予約修正のAPIを curl でテストしてみました。以下の curl は、先の `http://<hostname>:<port>/api/debug` 画面にて `Try it out` を選ぶと生成することができます。

予約を取得
```
$ curl -X 'GET'   'http://192.168.42.100:8888/api/reserves/7444?isHalfWidth=false'   -H 'accept: application/json'
{"id":7444,"isSkip":false,"isConflict":false,"isOverlap":false,"allowEndLack":true,"isTimeSpecified":false,"isDeleteOriginalAfterEncode":false,"channelId":3273601024,"startAt":1674986400000,"endAt":1674988200000,"name":"ＮＨＫニュース７[二][字]","programId":327360102426083,"description":"「確かなニュース、いつでも　どこでも」　きょうの日本、世界の今　一歩先へ、一歩深く　テレビでもスマホでも　このニュースを届けたい","extended":"◇番組内容\n「確かなニュース、いつでも　どこでも」　きょうの日本、世界の今　一歩先へ、一歩深く　テレビでもスマホでも　このニュースを届けたい\n◇出演者\n【キャスター】高井正智，川﨑理加，深川仁志，【気象キャスター】向笠康二郎","rawExtended":{"番組内容":"「確かなニュース、いつでも　どこでも」　きょうの日本、世界の今　一歩先へ、一歩深く　テレビでもスマホでも　このニュースを届けたい","出演者":"【キャスター】高井正智，川﨑理加，深川仁志，【気象キャスター】向笠康二郎"},"genre1":0,"subGenre1":0,"genre2":0,"subGenre2":1,"genre3":1,"subGenre3":0,"videoType":"mpeg2","videoResolution":"1080i","videoStreamContent":1,"videoComponentType....

```
これのうち、allowEndLack さえ含まれていれば、変更データは一部分だけでOKでした。以下は元々の録画予約に含まれていない encodeOption を追加したコマンドです。

```
$ curl -X 'PUT'   'http://192.168.42.100:8888/api/reserves/7444'   -H 'accept: application/json'   -H 'Content-Type: application/json'   -d '{  "allowEndLack": true,  "encodeOption": {"mode1": "H.264","encodeParentDirectoryName1": "recorded","isDeleteOriginalAfterEncode": true}}'
```
返事は以下のように返ってきました。
```
{"code":201,"message":"ok"}
```

# ルール修正


ルール修正については、修正する一部だけで試したらエラーとなり、けっこうしっかりデータを作っておかないといけない感じです。基本的に取得したルールのJSONデータを書き換えて戻すようにしています。

## curl でテスト

ルール取得
```
$ curl -X 'GET'   'http://192.168.42.100:8888/api/rules/16'   -H 'accept: application/json'
{"id":16,"isTimeSpecification":false,"searchOption":{"keyCS":false,"keyRegExp":false,"name":true,"description":true,"extended":false,"ignoreKeyCS":false,"ignoreKeyRegExp":false,"ignoreName":false,"ignoreDescription":false,"ignoreExtended":false,"GR":false,"BS":false,"CS":false,"SKY":false,"isFree":false,"keyword":"ベストヒットUSA ","channelIds":[400151],"genres":[{"genre":4,"subGenre":1}],"times":[{"week":127}]},"reserveOption":{"enable":true,"allowEndLack":true,"avoidDuplicate":false}}n
```
ルールに encodeOption を加えて書き換え
```
$ curl -X 'PUT'   'http://192.168.42.100:8888/api/rules/16'   -H 'accept: application/json'   -H 'Content-Type: application/json'   -d '{"id":16,"isTimeSpecification":false,"searchOption":{"keyCS":false,"keyRegExp":false,"name":true,"description":true,"extended":false,"ignoreKeyCS":false,"ignoreKeyRegExp":false,"ignoreName":false,"ignoreDescription":false,"ignoreExtended":false,"GR":false,"BS":false,"CS":false,"SKY":false,"isFree":false,"keyword":"ベストヒットUSA ","channelIds":[400151],"genres":[{"genre":4,"subGenre":1}],"times":[{"week":127}]},"reserveOption":{"enable":true,"allowEndLack":true,"avoidDuplicate":false},"encodeOption":{"mode1":"H.264","encodeParentDirectoryName1":"recorded","isDeleteOriginalAfterEncode":true}}'
```
返事
```
{"code":200}
```

## Pythonプログラム

requests モジュールでは JSON の PUT がうまくいかなかったので、 urllib.request を使っています。
urllib.request が標準モジュールであることと、エラーメッセージがわかりやすかったので今後もこっちのほうがいいかな。

```
#import requests
import urllib.request
import json
# example json data={"id":16,"isTimeSpecification":False,"searchOption":{"keyCS":False,"keyRegExp":False,"name":True,"description":True,"extended":False,"ignoreKeyCS":False,"ignoreKeyRegExp":False,"ignoreName":False,"ignoreDescription":False,"ignoreExtended":False,"GR":False,"BS":False,"CS":False,"SKY":False,"isFree":False,"keyword":"ベストヒットUSA ","channelIds":[400151],"genres":[{"genre":4,"subGenre":1}],"times":[{"week":127}]},"reserveOption":{"enable":True,"allowEndLack":True,"avoidDuplicate":False},"encodeOption":{"mode1":"H.264","encodeParentDirectoryName1":"recorded","isDeleteOriginalAfterEncode":True}}

# limit を適宜変更のこと
urlget = "http://192.168.42.100:8888/api/rules?offset=0&limit=1000&type=all&keyword="
urlput = "http://192.168.42.100:8888/api/rules/"
# ルール一覧を得る
response = urllib.request.urlopen(urlget)
jsonData = json.load(response)
# ルールごとにループ
for jsonObj in jsonData["rules"]:
    # encodeOption が指定されていなかった場合
    if ("encodeOption" not in jsonObj):
      print("no encodeOption:",jsonObj["id"])
      # encodeOption を子要素と共に追加
      jsonObj["encodeOption"]={"mode1":"H.264","encodeParentDirectoryName1":"recorded","isDeleteOriginalAfterEncode":True}
      # 変更したルールを変更登録
      url=urlput+str(jsonObj["id"])
      headers = {"Content-Type": "application/json"}
      # print (json.dumps(s))
      res=urllib.request.Request(url,json.dumps(jsonObj).encode(),headers,method="PUT")
      with urllib.request.urlopen(res) as f:
          pass
      print(f.status,end="")
      if (200==f.status):
          print(" OK")
      else:
          print(" Error")
          print(f.reason)
          print(json.dumps(res.json()))
    # encodeOption が指定されているがisDeleteOriginarlAfterEncode が True でなかった場合
    if (True != jsonObj["encodeOption"]["isDeleteOriginalAfterEncode"]):
      print("isDeleteOriginalAfterEncode is not True:",jsonObj["id"])
      # True に変更
      jsonObj["encodeOption"]["isDeleteOriginalAfterEncode"]=True
      # 変更したルールを変更登録
      url=urlput+str(jsonObj["id"])
      headers = {"Content-Type": "application/json"}
      # print (json.dumps(s))
      res=urllib.request.Request(url,json.dumps(jsonObj).encode(),headers,method="PUT")
      with urllib.request.urlopen(res) as f:
          pass
      print(f.status,end="")
      if (200==f.status):
          print(" OK")
      else:
          print(" Error")
          print(f.reason)
          print(json.dumps(res.json()))


```

ちょっと手間取ったところとして、jsonObj は Python 内部では辞書型で持っているのですが、それをjson.dumps()で文字列に変えて更に encode() でバイト文字列にする必要がありました。

そのうえ、辞書型での Bool 値と json の Bool 値の表記が異なって ( True/true , False/false ) いるのをそらなかったので、最初戸惑ってしまいました。


### 実行
```
$ python3 apitest.py 
no encodeOption: 35
200 OK
no encodeOption: 40
200 OK
no encodeOption: 41
200 OK
no encodeOption: 42
200 OK
isDeleteOriginalAfterEncode is not True: 44
200 OK
isDeleteOriginalAfterEncode is not True: 45
200 OK
no encodeOption: 46
.
.
.
isDeleteOriginalAfterEncode is not True: 139
200 OK
no encodeOption: 143
200 OK
isDeleteOriginalAfterEncode is not True: 173
200 OK
```

うまくいきました。

# 録画ルール変更後の予約の扱い

録画ルールを変更しても、録画ルールから生成された予約が約一週間先まであります。録画ルールを変更する前に生成されたこの予約について、録画ルールを変更したら予約の内容はどうなるのでしょうか?

GUIで確かめたら、録画ルールを変更する以前に生成された予約の内容は、変更が反映されてました。予約も変更が必要かな? と思ってましたがそれは必要ないということになります。