MP3/MP4ファイル内の歌詞情報を取り出してSony walkman用の歌詞ファイル(*.lrc)に保存する

# 経緯

数年前にiPodからソニーのwalkmanに乗り換えました。
walkmanでは歌詞表示することができ、歌詞の1行1行に時間情報を記入することで、歌や英会話にあわせてスクロール表示してくれます。歌の練習に使ったり、英会話を学習するのにとても便利です。
ただし、MP3/MP4ファイル内の歌詞は表示してくれなくて、歌詞専用のLRCファイルに歌詞テキストを保存しておく必要があります。

これまでは、ソニーが提供するソフトウェア「Media Go」で、iPod用にiTunesで保存していた楽曲を再生したりwalkmanに転送したり、そして iTunesで1曲1曲プロパティを開いて歌詞テキストをコピーしては Media Go 側にペーストして、歌詞の時間編集をしてきました。
この転送作業が面倒になり、MP3/MP4ファイルから歌詞テキストを抜き出し、LRCファイルに保存するPythonスクリプトを作りました。
単に歌詞を表示したり、ディレクトリ指定で全ファイルのLRCファイルをまとめて作ったりできます。

# 動作環境

Python3 と mutagen モジュールが必要です。
私はWindows環境で使っていて、日本の楽曲は日本語ファイル名が多くて、日本語をうまく処理するのに Python3 が必要でした。
また、MP3/MP4から 歌詞情報を読み出すのに mutagen モジュールをpipでインストールして使っています。

### mutagenモジュールのインストール

```
$ pip3 install mutagen
または
$ python3 -m pip install mutagen
または
$ sudo python3 -m pip install mutagen
```

# 使い方

### ヘルプ表示 (help)

```
$ lyrics.py -h
usage: lyrics.py [-h] [-l] [-s] [-c] [-r] file [file ...]

positional arguments:
  file           file or directory name

optional arguments:
  -h, --help     show this help message and exit
  -l, --list     show file names containing lyrics (default)
  -s, --show     show lyrics
  -c, --create   create "*.lrc" lyrics files if it does not exist
  -r, --replace  create or replace "*.lrc" lyrics files
```

### 歌詞情報を含むファイル名一覧表示 (list)

    $ lyrics.py -l .

### 歌詞の表示 (show)

    $ lyrics.py -s "1-01 Just The Way You Are.mp3"
    $ lyrics.py -s "Billy Joel"

### LRCファイルへの保存 (create: 存在しない場合のみ)

    $ lyrics.py -c "Billy Joel"

### LRCファイルへの保存 (replace: 存在する場合は上書き、時間情報が消えるので要注意)

    $ lyrics.py -r "Billy Joel"

# ソースコード

### github

    https://github.com/shiracamus/lyrics_converter

### Pythonスクリプト

```py
#!/usr/bin/env python3
#
# Lyrics converter
#
# version: 2017.4.18
# author: @shiracamus
# github: https://github.com/shiracamus/lyrics_converter
#
# Need to install mutagen module
#     $ python3 -m pip install mutagen
# or
#     $ sudo python3 -m pip install mutagen
#

import os
import sys
import argparse
from mutagen.id3 import ID3, ID3NoHeaderError
from mutagen.mp4 import MP4, MP4StreamInfoError


def read_lyrics_in_mp3(path):
    try:
        tags = ID3(path)
        for key in tags:
            if key.startswith('USLT'):
                return tags[key].text
    except ID3NoHeaderError:
        pass
    return None


def read_lyrics_in_mp4(path):
    try:
        tags = MP4(path).tags
        if '\xa9lyr' in tags:
            return tags['\xa9lyr'][0]
    except MP4StreamInfoError:
        pass
    return None


def read_lyrics(path):
    return (read_lyrics_in_mp3(path) or
            read_lyrics_in_mp4(path) or
            None)


class Lyrics:

    def __init__(self, path):
        self.path = path
        self.path_base, self.path_ext = os.path.splitext(path)
        self.text = read_lyrics(path)
        self.exists = bool(self.text)

    def __repr__(self):
        return '<base="%s", ext="%s", lyrics=%s>' % (self.path_base,
                                                     self.path_ext,
                                                     self.exists)


class LRC:
    ext = '.lrc'

    def __init__(self, path):
        self.path = path
        self.exists = os.path.exists(self.path)

    def save(self, text):
        with open(self.path, 'w') as f:
            f.write(text)
        self.exists = True


def show_filename(lyrics):
    print(lyrics.path)


def show_lyrics(lyrics):
    print('=' * 30)
    print(lyrics.path)
    print('-' * 30)
    print(lyrics.text)
    print()


def save_lrc(lyrics, replace=False):
    lrc = LRC(lyrics.path_base + LRC.ext)
    if not lrc.exists or replace:
        lrc.save(lyrics.text)
        print('Saved "%s"' % lrc.path)
    else:
        print('Already exists "%s"' % lrc.path)


def pathes(files_and_directories):
    for file_or_directory in files_and_directories:
        if os.path.isfile(file_or_directory):
            yield file_or_directory
        elif os.path.isdir(file_or_directory):
            for root, dirs, files in os.walk(file_or_directory):
                for filename in files:
                    yield os.path.join(root, filename)


def parse_args():
    p = argparse.ArgumentParser()
    p.add_argument('-l', '--list', action='store_true',
                   help='show file names containing lyrics (default)')
    p.add_argument('-s', '--show', action='store_true',
                   help='show lyrics')
    p.add_argument('-c', '--create', action='store_true',
                   help='create "*.lrc" lyrics files if it does not exist')
    p.add_argument('-r', '--replace', action='store_true',
                   help='create or replace "*.lrc" lyrics files')
    p.add_argument('file', nargs='+', default='.',
                   help='file or directory name')
    args = p.parse_args()
    if not (args.show or args.create or args.replace):
        args.list = True
    return args


def main(args):
    for path in pathes(args.file):
        lyrics = Lyrics(path)
        if not lyrics.text:
            continue
        if args.list:
            show_filename(lyrics)
        if args.show:
            show_lyrics(lyrics)
        if args.create or args.replace:
            save_lrc(lyrics, replace=args.replace)

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

# 今後

ソース内コメントは順次追加します。
MP3/MP4以外も扱えるようにしたいので、処理方法を知っていたら是非教えてください。
GUIラッパーも作ってみたいですね。
時間情報も自動で作れたら最高ですが、楽曲の中から音声認識して歌詞を作り出すライブラリなんてまだないですよね？