iTunesで再生中の曲の歌詞をPythonで自動表示させよう（改良版）

#はじめに
以前作った「[iTunesで再生中の曲の歌詞をPythonで自動表示させよう](https://qiita.com/saba/items/4d3884d475245720a7a8)」に大幅に機能を追加しました。

#改良箇所

- 曲のスキップに対応
- 表示の高速化
- アプリの制御
- 自動スクロール
- 歌詞検索精度の向上


#曲のスキップに対応
一度曲の歌詞ページを開いたらその曲が終わるまでそのページを表示し続ける仕様だったのを、次の曲にスキップしたら歌詞も次の曲を表示させるようにしました。

```python:変更前
while True:
    start = time.time()  # delete

    # ~~~~~~~~

    time.sleep(music_time - (time.time() - start))  # delete
```
```python:変更後
prev_track_name = ""  # add
while True:
    # 再生中の曲名、アルバム名、アーティスト名、曲の長さ
    name = music.current_track.name.get()
    artist = music.current_track.artist.get()
    album = music.current_track.album.get()
    music_time = music.current_track.time.get()  # m:s の str

    # add-start
    if name == prev_track_name:
        time.sleep(1)
        continue

    prev_track_name = name
    # add-end

    # ~~~~~~~~

    time.sleep(1)  # add
```


#表示の高速化
歌詞ページの表示が完了するまでに時間がかかっていたため、タイムアウトを設定しました。

```python:変更後
driver = webdriver.Chrome(executable_path="chromedriver")
driver.get("https://www.google.com/")  # 初期タブ
driver.set_page_load_timeout(5)  # 読み込みの最大待ち時間（この場合 5 秒間）  # add
```

歌詞自体は 5 秒もあれば読み込みができますが、それ以外の画像等に時間がかかってページ読み込み時間が長くなってしまうため、5 秒を過ぎたら強制的に終了させます。ついでに「歌詞の位置までスクロール」も、歌詞の位置を探してその位置までスクロールするように変更しました。

```python:変更前
while True:
    # ~~~~~~~~

    try:
        # ~~~~~~~~

        # 新しいタブで開く
        driver.execute_script("window.open()")
        driver.switch_to.window(driver.window_handles[1])
        driver.get("https://www.uta-net.com/" + name_url)
        driver.execute_script("window.scrollTo(0, 380);")  # 歌詞の位置までスクロール
        # 前のタブを閉じる
        driver.switch_to.window(driver.window_handles[0])
        driver.close()
        driver.switch_to.window(driver.window_handles[0])
```

```python:変更後
while True:
    # ~~~~~~~~

    try:
        # ~~~~~~~~

        # 新しいタブで開く
        driver.execute_script("window.open()")
        driver.switch_to.window(driver.window_handles[1])

        # add-start
        try:
            driver.get("https://www.uta-net.com/" + name_url)
        except:
            pass
        # 歌詞の位置までスクロール
        driver.execute_script("arguments[0].scrollIntoView();", driver.find_element_by_id("view_kashi"))
        # add-end

        # 前のタブを閉じる
        driver.switch_to.window(driver.window_handles[0])
        driver.close()
        driver.switch_to.window(driver.window_handles[0])

```


#アプリの制御
ほぼおまけですが、ターミナル上でも「再生」「次の曲へ」などを制御できるようにしました。
まず基本的な操作です。

```python:基本操作
music = appscript.app("Music")

# 再生
music.play()

# 一時停止
music.pause()

# 再生・一時停止
music.playpause()

# 前のトラックへ
music.previous_track()

# 次のトラックへ
music.next_track()
```
これをそのまま使います。
また、通常の `input()` だと入力が行われるまで待機して他の処理を行うことができないため、タイムアウト付き入力を利用しました。

```python:タイムアウト付き入力
from select import select
import sys

# 引数は順に「読み込み」「書き込み」「例外」「待機時間」で、今回は読み込みのみ使用
# 待機時間は 0.5 秒に指定
# 0.5 秒経って入力がなければ空のリストが返される
read, _, _ = select([sys.stdin], [], [], 0.5)

if read:
    command = sys.stdin.readline().strip()
else:
    command = ""
```
以上をそのままコードの中に入れていきます。

```python:変更後
while True:
    # Music アプリにアクセスする
    music = appscript.app("Music")

    # add-start
    # ---- アプリ制御 ----

    # タイムアウト付き入力
    read, _, _ = select([sys.stdin], [], [], 0.5)

    if read:
        command = sys.stdin.readline().strip()
    else:
        command = ""

    if command == "b":  # 前のトラックへ
        music.previous_track()
    elif command == "p":  # 再生・一時停止
        music.playpause()
    elif command == "n":  # 次のトラックへ
        music.next_track()
    # add-end

    # ~~~~~~~~
```


#自動スクロール
歌詞全体が表示できないこともあるため、歌詞が自動でスクロールされるようにしました。

```python:変更後
while True:
    # ~~~~~~~~

    if name == prev_track_name:
        time.sleep(1)

        # add-start
        count += 1
        if count % 5 == 0:
            driver.execute_script(
                "window.scrollTo(window.pageXOffset, window.pageYOffset + " + str(kashi_length // music_time * 5) + ");"
            )
        # add-end

        continue

        # ~~~~~~~~

        # 新しいタブで開く
        driver.execute_script("window.open()")
        driver.switch_to.window(driver.window_handles[1])
        try:
            driver.get("https://www.uta-net.com/" + name_url)
        except:
            pass
        # 歌詞の位置までスクロール
        driver.execute_script("arguments[0].scrollIntoView();", driver.find_element_by_id("view_kashi"))

        # add-start
        # ページ上の歌詞部分の長さ
        kashi_length = driver.find_element_by_id("view_mylink").location["y"] - \
                       driver.find_element_by_id("view_kashi").location["y"]
        # add-end

        # 前のタブを閉じる
        driver.switch_to.window(driver.window_handles[0])
        driver.close()
        driver.switch_to.window(driver.window_handles[0])

```
ページ内の歌詞部分の長さを取得し、 (歌詞の長さ / 曲の長さ) * 5 下にスクロール を 5 秒おきに実行することで程良いペースでスクロールしています。


#歌詞検索精度の向上
まずは全角アルファベットを半角に変換します。また、検索の際に「完全一致」だったのを「が含まれる」に変更しました。

```python:変更後
while True:
    # ~~~~~~~~

    # ---- 歌詞サイトにアクセス ----

    # 全角を半角に  # add
    name = str(name).translate(str.maketrans({chr(0xFF01 + i): chr(0x21 + i) for i in range(94)}))

    name = parse.quote(name)  # パーセントエンコーディング
    url = "https://www.uta-net.com/search/?Aselect=2&Keyword=" + name + "&Bselect=3&x=0&y=0"  # Bselect=4 を Bselect=3 に

```

またアクセス数の多いページに付く王冠マークの影響で歌手名の位置がずれることがあるため、修正しました。

```python:変更後
while True:
    # ~~~~~~~~

    try:
        # ~~~~~~~~

        name_url = ""
        flag = False
        for tag_tr in tr:
            a = tag_tr.find_all("a")
            for i, tag_a in enumerate(a):
                if i == 0:  # 曲名
                    name_url = tag_a.get("href")
                elif i == 1 or i == 2:  # 歌手名  # 1 だけだったところに 2 を追加
                    if tag_a.string == artist:
                        flag = True
                        break
            if flag:
                break
```


#コード全体
```python
import time
import sys
from select import select

import appscript
from urllib import request, parse
from bs4 import BeautifulSoup
from selenium import webdriver

driver = webdriver.Chrome(executable_path="chromedriver")
driver.get("https://www.google.com/")  # 初期タブ
driver.set_page_load_timeout(5)  # 回線の速さによってはもっと短くても大丈夫

music_time = 0
prev_track_name = ""
count = 0
kashi_length = 0
while True:
    # Music アプリにアクセスする
    music = appscript.app("Music")

    # ---- アプリ制御 ----

    # タイムアウト付き入力
    read, _, _ = select([sys.stdin], [], [], 0.5)

    if read:
        command = sys.stdin.readline().strip()
    else:
        command = ""

    if command == "b":  # 前のトラックへ
        music.previous_track()
    elif command == "p":  # 再生・一時停止
        music.playpause()
    elif command == "n":  # 次のトラックへ
        music.next_track()

    # ---- 曲名取得 ----

    # 再生中の曲名、アルバム名、アーティスト名、曲の長さ
    name = music.current_track.name.get()
    artist = music.current_track.artist.get()
    album = music.current_track.album.get()
    music_time = music.current_track.time.get()  # m:s の str

    # music_time を秒数に
    music_time = music_time.split(":")
    music_time = int(music_time[0]) * 60 + int(music_time[1])

    if name == prev_track_name:
        time.sleep(1)
        count += 1
        if count % 5 == 0:
            driver.execute_script(
                "window.scrollTo(window.pageXOffset, window.pageYOffset + " + str(kashi_length // music_time * 5) + ");"
            )
        continue

    prev_track_name = name

    print("{} / {} / {}".format(name, artist, album))

    # ---- 歌詞サイトにアクセス ----

    # 全角を半角に
    name = str(name).translate(str.maketrans({chr(0xFF01 + i): chr(0x21 + i) for i in range(94)}))

    name = parse.quote(name)  # パーセントエンコーディング
    url = "https://www.uta-net.com/search/?Aselect=2&Keyword=" + name + "&Bselect=3&x=0&y=0"

    try:
        html = request.urlopen(url)
        soup = BeautifulSoup(html, "html.parser")

        tr = soup.find_all("tr")

        name_url = ""
        flag = False
        for tag_tr in tr:
            a = tag_tr.find_all("a")
            for i, tag_a in enumerate(a):
                if i == 0:  # 曲名
                    name_url = tag_a.get("href")
                elif i == 1 or i == 2:  # 歌手名
                    if tag_a.string == artist:
                        flag = True
                        break
            if flag:
                break

        # 新しいタブで開く
        driver.execute_script("window.open()")
        driver.switch_to.window(driver.window_handles[1])
        try:
            driver.get("https://www.uta-net.com/" + name_url)
        except:
            pass
        # 歌詞の位置までスクロール
        driver.execute_script("arguments[0].scrollIntoView();", driver.find_element_by_id("view_kashi"))
        kashi_length = driver.find_element_by_id("view_mylink").location["y"] - \
                       driver.find_element_by_id("view_kashi").location["y"]
        # 前のタブを閉じる
        driver.switch_to.window(driver.window_handles[0])
        driver.close()
        driver.switch_to.window(driver.window_handles[0])

    except:
        pass

    print("b:前 | p:再生・一時停止 | n:次")
    print()

    time.sleep(1)
```