Skypeを使ってテレビ番組の録画忘れを防止する

## 何があった
明石家サンタの録画を忘れた。2024年クリスマス、職場で『あっ！！』と叫んでしまう。

## 録画忘れて後悔する番組
さんまさんも69歳でいつ引退されてもおかしくない。もう取り逃したくないけど明石家サンタは1年に1回なのでちょくちょく忘れる。
あと、たまにやってるNHKの将棋特集なんかもよく撮り逃す。
基本的に見たいレギュラー番組だけ録画するスタイルなのでこうなるんよな

## 母親も
明石家さんまが好き。毎日新聞紙に蛍光マーカー引いてた気がする。あれってまだ続けてるんかな。。
ちなみに私が必ず見るのはクリスマスの明石家サンタのみ (←何の補足だ)

## なんでSkypeを使う？
最近、親兄弟との連絡がLINEからSkypeに変わったから。
LINE Notifyは終了しちゃうし、LINE botはプッシュ通知の無料枠が月200件までと制約があるので、Skypeがお手軽かな～と最近思い始めた。

## この記事の内容
前置きが長くなってしまったけど、この記事は
『テレビ番組表をスクレイピングし、録画したい番組が近かったらSkypeで知らせる』という内容です。放送波は地上波で固定

## どのWebサイトを対象にするか
テレビ王国さんが良さそう。一通りサイトを見たけど、スクレイピング禁止の言葉は見当たらなかった。

## スクレイピングするプログラム
いきなりコードです。自分の動作環境はpython3.9

```python:tv_schedule.py
from bs4 import BeautifulSoup
from urllib import request
from datetime import datetime, timedelta
import time
#
import skype_send

def get_tv_info_list(tv_dict):

    # url内容取得
    response = request.urlopen(tv_dict['url'])
    if response.getcode() != 200:
        return False, []

    # 必ずスリープはしましょう！
    time.sleep(1)

    # 解析
    soup = BeautifulSoup(response, 'html.parser')
    utileList = soup.select('div.utileList')
    result = []

    # 取得分で繰り返し ※めんどくさいので２ページ目以降は知らない！ (つまり1ページに収まる20件以内が前提ってこと)
    for tv in utileList:
        h2 = tv.select_one('h2:nth-of-type(1)')
        search_word = tv_dict['番組名必須ワード']
        if search_word and search_word not in h2.a.text:
            # 必須ワードがないものは無視
            continue

        ignore_list = tv_dict['無視リスト']
        if ignore_list and any(ignore in h2.a.text for ignore in ignore_list):
            # 1個でも無視リストに当てはまるものがあれば無視
            continue

        # pタグ全体のテキストを取得（aタグ部分は除外される）
        p = tv.select_one('p:nth-of-type(1)')
        lines = p.get_text(separator='\n', strip=True).splitlines()
        if len(lines) <= 1:
            # 1行しかないときはたぶん '条件に該当する番組はありません。'
            return True, [] 
        tvshow_date = lines[0]
        tv_station = lines[1]

        # お知らせ前日数チェック
        day_cnts = tv_dict['お知らせ前日数']
        if day_cnts >= 0:
            # 現在からN日後のMM/DD を取得
            days_later = datetime.now() + timedelta(days=day_cnts)
            check_day = f'{days_later.month}/{days_later.day}'
            if check_day != tvshow_date.split(' ')[0]:
                continue

        result.append({'放送日時': weekday_en2jp(tvshow_date),
                       '局': tv_station,
                       '番組名': h2.a.text,
                       'リンク': 'https://www.tvkingdom.jp' + h2.a['href']})

    # 無駄な空白を一気に取ってから返す
    return True, [{key: value.replace('      ', ' ').replace("\u3000", " ").strip() for key, value in dictionary.items()}
                  for dictionary in result]

# UTF-8なバイト文字列を返す
def utf8_byte_string(text):
    utf8_bytes = text.encode('utf-8')
    return "%" + "%".join(f"{byte:02x}" for byte in utf8_bytes).upper()

# 英語表記の曜日を日本語にする
def weekday_en2jp(text):
    weekdays = {
        "Mon": "月曜日",
        "Tue": "火曜日",
        "Wed": "水曜日",
        "Thu": "木曜日",
        "Fri": "金曜日",
        "Sat": "土曜日",
        "Sun": "日曜日"
    }

    for eng_day, jp_day in weekdays.items():
        pattern = f"({eng_day})"
        if pattern in text:
            return text.replace(pattern, f"({jp_day})")
    return text

# 対象リストを調べてskypeでお知らせ
def notify_tv_schedule():

    RECORD_LIST = [
                      {
                       '検索ワード': '将棋',
                       '番組名必須ワード':'将棋',
                       '無視リスト': ['[再]', '将棋フォーカス'],
                       'お知らせ前日数': 2,
                       '通知先': ['テレビ番組_自分', 'テレビ番組_母']
                      },
                      {
                       '検索ワード': '明石家サンタ',
                       '番組名必須ワード':'',
                       '無視リスト': [],
                       'お知らせ前日数': 3,
                       '通知先': ['テレビ番組_自分']
                      },
                      {
                       '検索ワード': '明石家さんま',
                       '番組名必須ワード':'さんま',
                       '無視リスト': [],
                       'お知らせ前日数': 2,
                       '通知先': ['テレビ番組_母']
                      },
                  ]

    # リスト分繰り返し
    for tv_dict in RECORD_LIST:
        # 検索ワードからurlを作る
        tv_dict['url'] = f"https://www.tvkingdom.jp/schedulesBySearch.action?condition.genres%5B0%5D.parentId=-1&condition.genres%5B0%5D.childId=-1&stationPlatformId=1&condition.keyword={utf8_byte_string(tv_dict['検索ワード'])}&submit=%E6%A4%9C%E7%B4%A2"
        result, tv_list = get_tv_info_list(tv_dict)
        if not result:
            print('なんか失敗してるよ！？', tv_dict)
            continue
        for tv in tv_list:
            message = "\n".join(f"{value}" for key, value in tv.items())
            for address in tv_dict['通知先']:
                skype_send.message_to_group(address, message)

if __name__ == "__main__":
    notify_tv_schedule()
```

#### 上記プログラムの番組通知
1. 将棋番組(将棋フォーカスと再放送を除く)を自分と母親へ
2. 明石家サンタを自分へ
3. 明石家さんまの番組を母親へ


## 番組リストの部分だけ簡単な説明

・検索ワード
　テレビ王国で検索するワード
・番組名必須ワード
　番組名に含まれるワードを指定する。不要なときは''
・無視リスト
　番組名に含まれてたら対象外にするリスト
・お知らせ前日数
　何日前にお知らせするか。常に知らせるには-1
・通知先
　Skypeのグループ名

繰り返し録画設定してる番組とか再放送は通知を受けたくないかもだから無視リストも作っておいた。
もし参考にする人がいれば、この辺をカスタマイズしてください！

## Skypeにメッセージ送信するプログラム
いつものようにChatGPTに手伝ってもらって書いた。この記事では不使用だけど、画像などのファイル送信も実装してる。
参考にする人は、メールアドレスとパスワードのとこだけ変えれば動くと思います。

```python:skype_send.py
import os
from skpy import Skype

# skypeにメッセージ送信
def message_to_group(group_topic, message, username='あなたのメール@gmail.com', password='あなたのパスワード', image_file=None):
    try:
        # Skypeにログイン
        skype_obj = Skype(username, password)

        # 指定したトピックのグループチャットを探す
        group_chat = None
        for chat_id in skype_obj.chats.recent():  # recent()でチャットIDのリストを取得
            chat = skype_obj.chats[chat_id]       # 各チャットIDに対応するチャットオブジェクトを取得
            if hasattr(chat, 'topic') and chat.topic == group_topic:  # トピックで一致するか確認
                group_chat = chat
                break

        if not group_chat:
            return

        # メッセージを送信
        group_chat.sendMsg(message)

        # ファイルを送信（オプション）
        if image_file:
            with open(image_file, "rb") as file:
                file_name = os.path.basename(image_file)
                group_chat.sendFile(file, file_name, f"{file_name} を送信します！")
                print(f"ファイル '{file_name}' を送信しました。")

    except Exception as e:
        print(f"エラーが発生しました: {e}")

def main():
    text = 'test'
    message_to_group('テレビ番組_自分',  text)

if __name__ == '__main__':
    main()
```

## Skypeの設定内容
順番前後してますが、Skypeで通知を受け取りたいグループをWindowsから作成していきます。

###
ヘルプとフィードバックから確認すると、バージョンは8.126.0.208でした。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3625818/fb0cd5ea-f54c-cbce-6c96-e090f559aa0a.png)

青い丸をクリックして、「新しいグループチャット」を選びます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3625818/b5a06dee-fab8-de0f-9155-55bccb610984.png)

グループ名を入力して次へ
※このグループ名は、最初のプログラムの「通知先」と同じ名称になるようにします。
<img width="300" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3625818/92e9a183-d073-ab3e-d326-8927a97b682c.png">

通知させたい人が居れば選択します。ここでは母親を選びます。
<img width="300" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3625818/88fa5eb8-e581-b274-1d4f-511e236129d0.png">

チャットの所に追加されたことを確認します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3625818/2e531f40-4f7c-85d6-dfb7-a6cd62293736.png)

ちなみにSkypeのデフォルト設定だと他人からの検索結果に表示される設定になっています。
『投資詐欺グループに勝手に追加されたんやけど！？』と親から連絡を受けることになるので、
設定->連絡先->プライバシー と選んで「検索結果に表示する」 をOFFにしておきましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3625818/a77e8adc-de1d-72f5-36bd-64394431256c.png)

## 実行してみる
プログラムを実行してみて、Skypeに通知が行くことを確認しましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3625818/e1358869-15ac-f1b8-8dea-8c95187026eb.png)
天彦先生 vs ダニーじゃん これは胸熱

## よく分からない警告
実行時に何か警告が出ますがよく分かりません(笑)
```
# python3 tv_schedule.py
/usr/local/lib/python3.9/site-packages/skpy/msg.py:239: MarkupResemblesLocatorWarning: The input looks more like a filename than markup. You may want to open this file and pass the filehandle into Beautiful Soup.
  fields.update(cls.contentToFields(BeautifulSoup(fields["content"], "html.parser")))
```

ちょっとだけ調べたけど、Skypeに送るメッセージに「:」が入っているとこの警告が出る模様？？
でもURLリンクは送りたいので無視する！

## 自動で動かす
AWSのEC2インスタンスを利用しているので、cronで1日1回実行させるようにしておいた。

## 終わりに
たぶん意図通りに動かないケースは出てくると思うので、チューニングは必要そう。
例えば番組名必須ワードに将棋を入れたのは、囲碁将棋という芸人さんが出演している番組を除外するためなんだけど、この芸人さんの名前を含む番組が出来たら録画対象になってしまう。他には将棋人気がもっと高くなって、番組名に将棋無くても大丈夫やろ！とテレビ局が判断して番組名に将棋が含まれなくなったら、今度は逆に録画対象外になってしまう。(ここの日本語、読んでる人に伝わるだろうか・・)

キーワード予約可能なレコーダーを持っている人からすれば何の意味もない記事でした～