玩式草子─ソフトウェアとたわむれる日々

第58回「らじる★らじる」もう一度(その3)

前回NHKの「Now On Air情報」⁠以下NOA情報)から入手した番組情報を、SQLite3を用いてデータベースに記録するためのスクリプトを紹介しました。手元では、このスクリプトを「らじる★らじる」録音用スクリプトから起動し、録音した番組の情報を自動収集して、日夜増えていく番組情報データベースをほくそ笑みながら眺めています。

 $ sqlite3 radiru_titles.sql3
 SQLite version 3.8.0.2 2013-09-03 17:11:13
 Enter ".help" for instructions
 Enter SQL statements terminated with a ";"
 
 sqlite> select oid,* from titles where date like '2014-05-24%';
 1567|2014-05-24-17-00_奥の細道~名句でたどるみちのくの旅.mp3|2014-
 05-24 17:00:00|001netr20|古典講読「奥の細道~名句でたどるみちのくの
 旅~」(8)|和洋女子大学副学長…佐藤勝明
 ...
 1570|2014-05-24-21-00_私の日本語辞典「能をささえることば」_004.mp3|
 2014-05-24 21:00:00|001netr20|私の日本語辞典「能をささえることば」
 (4)|法政大学名誉教授、能楽研究家…西野春雄, 【アナウンサー】秋山和平
 1571|2014-05-24-21-00_クラシックの迷宮‐私の試聴室~ヴァインベルクの
 作品~.mp3|2014-05-24 21:00:00|001netfm0|クラシックの迷宮 -私の試聴
 室~ヴァインベルクの作品~-|
 
 sqlite> select * from contents where id=1571;
 1571|片山杜秀
 - 私の試聴室~ヴァインベルクの作品~ -
 1571|「バイオリンと弦楽のための小協奏曲 作品42から 第3楽章」
 ヴァインベルク作曲
 (4分39秒)
 (バイオリン)ギドン・クレーメル
 ....

録音したMP3ファイルにどういう内容が入っているかは、この番組情報データベースを調べればわかるようにはなったものの、MP3ファイルは別のファイルサーバに移動することもありますし、Nexus 7や携帯MP3プレイヤーなどに移して外部に持ち出すこともあります。そのような場合、楽曲情報データベースを調べないと収録曲目等がわからないのは不便です。

何かいい方法はないかな、と考えているうちに、MP3ファイルにはID3タグとよばれるタイトルや演奏者の情報を埋めこむ機能があったことを思いだしました。PythonにはMP3ファイルのID3タグを操作するためのeyeD3というモジュールが開発されていたはずです。そこで、このモジュールを使って、録音したMP3ファイルに番組情報を直接記録してみることにしました。

ID3タグについて

実際のコード等を紹介するまえに、少しID3タグについておさらいしておきましょう。

ID3タグはMP3ファイルにタイトルやアーティスト名を埋めこむために広く利用されている機能なものの、もともとのMP3形式に定められていた仕様ではなく、MP3形式が普及していくにつれ「デファクト・スタンダード」的に広まっていった仕様です。

最初に定義されたID3タグはID3v1と呼ばれ、128バイトの固定長のデータがMP3ファイルの末尾に添付される形式になっています。128バイトのデータは、曲名、アーティスト名、アルバム名とコメント欄に30バイトづつ割り振られ、残りは、日付や楽曲ジャンル、トラック番号などに使われています。このタグは構造が単純なこともあって携帯型メディアプレイヤーなどにも広く採用され、ほとんどのMP3プレイヤーで利用できるようになっています。

一方、ID3v1では曲名やコメントとして記録できるのは30バイトに限られており、30バイトというと日本語では15文字なので、あまり長いタイトルやアルバム名は記録できません。また、文字コードに関する規定もなかったため、Windows環境で作られたShift-JISのID3タグが、UNIX/Linux用のメディアプレイヤーでは文字バケするなど、不便な点がありました。

そこで新たに開発されたのがID3v2と呼ばれる形式です。この形式では、従来はMP3ファイルの末尾に置かれていた固定長の楽曲情報が、可変長のフレームに収めてMP3ファイルの先頭に置かれるように変更されています。可変長のフレームは256MBの上限まで複数個置くことが可能で、歌詞を収めたり、ジャケット写真などの画像データを埋めこむことも可能になりました。また、文字コードもUnicodeで記録するように定められたので、環境による互換性の問題も少なくなりました。

前述のように、元々ID3タグは、あるソフトウェアメーカが自社のソフトウェア用に開発した機能だったものの、デファクト・スタンダードとして普及した結果、最近ではid3.orgという組織が仕様を定義するようになりました。

しかしながら、⁠ID3v2」にはすでに「ID3v2.2」⁠ID3v2.3」⁠ID3v2.4」の3つのバージョンが存在し、バージョン間での下位互換性は保証されない(⁠⁠ID3v2.3」のみに対応しているメディアプレイヤーでは「ID3v2.4」のタグは読めない)といった問題が生じています。その結果、最新の仕様は「ID3v2.4」なものの、メディアプレイヤー等では「ID3v2.3」が最も広く利用されているようです。

PythonのeyeD3モジュール

前述のように、⁠ID3タグ」は元々は特定のソフトウェア用に開発された機能だったものの、MP3ファイルに楽曲情報を記録できる便利さから急速に普及して、現在ではMP3ファイルの標準機能のように利用されています。Plamo Linuxでも、EasyTagというID3タグを編集するためのソフトウェアを用意していますし、JuKAmarokといったメディアプレイヤーからもタグ情報を編集することができます。

図1 EasyTagによるID3タグの編集
図1 EasyTagによるID3タグの編集
図2 メディアプレイヤーJuKによるID3タグの編集
図2 メディアプレイヤーJuKによるID3タグの編集

これらタグ編集機能を使えば、録音したMP3ファイルにタイトル名等の情報を手書きしていくことは可能なものの、一日何本も自動録音しているファイルに一々手動でタグを書き込んでゆくのも面倒です。そこで注目したのがeyeD3です。

eyeD3はID3タグを操作するために開発されたPython用のモジュールで、PythonスクリプトからMP3ファイルのID3タグを操作するためのインターフェイスを提供します。また、eyeD3というコマンド(実体はPythonスクリプトを起動するためのラッパー)も用意されており、コマンドラインからID3タグを操作することもできる便利なツールになっています。

 $ eyeD3 02.銀座カンカン娘.mp3 
 02.銀座カンカン娘.mp3	[ 3.92 MB ]
 -------------------------------------------------------------------------------
 Time: 02:51	MPEG1, Layer III	[ 192 kb/s @ 44100 Hz - Joint stereo ]
 -------------------------------------------------------------------------------
 ID3 v2.3:
 title: 銀座カンカン娘
 artist: 遊佐未森
 album: スヰート檸檬
 recording date: 2008
 track: 2		genre: JPop (id 146)
 OTHER Image: [Size: 7453 bytes] [Type: image/jpeg]
 Description: 
 
 -------------------------------------------------------------------------------

さて、本来、ID3タグはCDから切り出した1つのトラックのMP3ファイルに、曲名やアーティスト名、アルバム名などを記録するように設計されています。一方、この連載で扱ってきた「らじる★らじる」の場合、1つの番組が1つのMP3ファイルになるので、複数の曲名やアーティスト名などをID3タグに書き込むことになります。そのあたりを考慮して、ダウンロードできる番組情報を以下のような形でID3タグに対応させることにしました。

ID3タグ名番組情報
albumファイル名
title番組名(title)
artist出演者(act)
date放送日(date)
comment番組内容(contents)

これらの情報は番組の放送中ならばNOA情報からダウンロードできます。しかし、録音中のMP3ファイルにタグを書き込むことはできないので、ID3タグの書き込みは録音終了後に行うことにします。その場合、NOA情報は次の番組に変っているので、必要な情報は手元に保存しているデータベースから取り出すことになります。

そのあたりを考慮して、録音したファイル名をキーに、番組情報データベースを検索して、必要な情報を集める処理を書いてみました。

 62  filepath = sys.argv[1]
 63  file = os.path.basename(filepath)
 64  ufile = file.decode('euc-jp')
 65 
 66  query = u"select oid,title,date,ch,act from titles where filename='{}';".format(ufile)
 67  (oid, title, date, channel, act) = query_db(query)[0]

番組情報データベースに記録しているのはファイル名だけなので、パス名の部分は取りのぞき(63行目⁠⁠、UTF-8形式でデータを保存しているSQLite3用にファイル名を変換して(64行目⁠⁠、そのファイル名を用いた検索リクエストを作って、query_db()関数に投げます。

データベースを検索するquery_db()関数はこういう風にしてみました。実際に検索しているのは30行目から33行目で、それ以外はデータベースファイルが見つからなかったり、指定されたファイル名が登録されていなかった場合のエラー終了処理です。

 23  def query_db(query):
 24      dbdir = config['DB_dir']
 25      dbname = dbdir + '/radiru_titles.sql3'
 26      if os.access(dbname, os.R_OK) == False:
 27          print("cannot find title DB({})".format(dbname))
 28          sys.exit(1)
 29  
 30      connection = sqlite3.connect(dbname)
 31      cursor = connection.cursor()
 32      c = cursor.execute(query)
 33      res = c.fetchall()
 34      if len(res) == 0:
 35          print("DB doesn't have entry for id: {} ".format(query))
 36          print("please check title DB:{}".format(dbname))
 37          sys.exit(1)
 38      else:
 39          return(res)

前回紹介したように、番組情報データベースは項目数が決まっているtitlesテーブルと、番組内容を複数行に記録したcontentsテーブルから構成されています。曲目リストなどはcontentsテーブルから取り出す必要があるので、titlesテーブルから得たoidをキーにcontentsテーブルを検索する処理を書きました。

 41  def query_contents(oid):
 42      query = u"select * from contents where id={}".format(oid)
 43      t = query_db(query)
 44      return t
 45  

contentsテーブルから取り出した曲目リストは、(id, 曲目)というタプルが並んだリストになっているので、曲目の部分を順に取りだして、1つの文字列につないでいきます。

 69      res = query_contents(oid)
 70      contents = ''
 71      for i in res:
 72          (id, l) = i
 73          contents = contents + l
 74  

ID3タグの"album"に記録するファイル名は、拡張子や日付の情報を省いた形にしてみました。

 75      album_parts = ufile.rstrip('.mp3').split('_')
 76      album = album_parts[1]

以上で必要な情報が揃ったので、それらを"tag"という辞書型のデータ構造にまとめて、set_id3tag()関数に渡してID3タグを書き込みます。

 77      tag = {'title':title, 'album':album, 'act':act, 'date':date, 'ch':channel, 'contents':contents}
 78      set_id3tag(filepath, tag)

set_id3tag()関数では、書き込み先のMP3ファイル名と、書き込みたいタグ情報を受けとり、eyeD3モジュールを使ってそれらを結びつけ、save()メソッドで保存します。

 46  def set_id3tag(file, tag):
 47      mp3file = file
 48  
 49      new_tag = eyed3.id3.Tag()
 50      new_tag.file_info = eyed3.id3.FileInfo(mp3file)
 51      new_tag.title = tag['title']
 52      new_tag.album = tag['album']
 53      new_tag.artist = tag['act']
 54      new_tag.release_date = tag['date']
 55      if len(tag['contents']) > 0:
 56          new_tag.comments.set(tag['contents'])
 57  
 58      new_tag.save()

なお、今回利用した eyeD3 はバージョン 0.7.3 で、以前から広く使われていた0.6.x系とはAPIが大きく異なっており、このコードは0.6.x系では動きません。

今回作成したスクリプトの全体は本稿の末尾に添付しておきます。手元では、このスクリプトをradiru_id3.pyという名前で/usr/local/bin/に保存しておき、MP3ファイルの録音が無事終了した際に起動して、録音ファイルに番組情報を書き込むようにしています。たとえば、atコマンドで5/31の20:30に実行する予定の録音用シェルスクリプトはこんな感じになっています。

  1  #!/bin/sh
  2  sleep 10
  3  mkfifo /home/kojima/radiru_scripts/fifo/1619
  4  file=/home/kojima/MP3/2014-05-31-20-30_漢詩をよむ「中国のこころのうた」_009.mp3
  5  (mplayer -prefer-ipv4 -ipv4-only-proxy -slave -input file=/home/kojima/radiru_scripts/fifo/1619 -playlist http://mfile.akamai.com/129932/live/reflector:46056.asx -af format=s16le -ao pcm:file=/dev/stdout -vc null -really-quiet -quiet | lame -r --quiet -q 4 - $file 2> /dev/null) &
  6  sleep 1m
  7  radiru_noa.py r2 2014-05-31-20-30_漢詩をよむ「中国のこころのうた」_009.mp3
  8  sleep 29m
  9  echo 'quit' > /home/kojima/radiru_scripts/fifo/1619
 10  radiru_id3.py /home/kojima/MP3/2014-05-31-20-30_漢詩をよむ「中国のこころのうた」_009.mp3
 11  rm -f /home/kojima/radiru_scripts/fifo/1619 /home/kojima/radiru_scripts/1619

録音作業自体は5行目の mplayer と lame が行ない、それらをバックグラウンドで動かしておいて、録音開始から1分後にradiru_noa.pyスクリプトで番組情報を取ってきてデータベースに記録します(7行目⁠⁠。そして30分の番組が終了するまで再度sleepして、9行目でmplayerを停止させた後、今回紹介したradiru_id3.pyスクリプトで録音が完了したMP3ファイルにID3タグを書き込みます(10行目⁠⁠。

radiru_id3.py スクリプトから書き込んだID3タグは、amarok等のメディアプレイヤーから確認することができます。

図3 Amarokから見たID3タグ
図3 Amarokから見たID3タグ

本シリーズで紹介してきた各スクリプトや、それに合わせて改造した「らじる★らじる」予約録音用スクリプト一式は、筆者のホームページで公開しているので、興味ある人は試してみてください。

なお、ここで公開しているスクリプトはPlamo Linuxを前提にしているので、他のディストリビューションでは文字コードの操作回りを変更する必要があります。もっとも、EUC-JP向けの処理を削ればいいだけなので、そう難しいことではないでしょう。

また、今回紹介したスクリプトでは、書き込むID3タグのバージョンは指定していないので、最新の「ID3v2.4」形式で記録されることになります。このバージョンのID3タグを正しく処理できないメディアプレイヤーで利用するの場合は、set_id3tag()関数あたりで使用するID3タグのバージョンを指定する必要があります。


先に切り抜いたFM情報誌をラベルにしたカセットテープの写真を紹介したように、⁠エア・チェック」マニアは、楽曲だけではなく、そのデータも残したいと考えます。その意味で、今回紹介してきたNOA情報を取り込むスクリプトまで揃えて、ようやく満足のいく録音環境が整った、という気がします。

もっとも最近では、加齢のせいか、音楽番組を聞きながらコードや原稿を書く「ながら作業」がしづらくなってきたので、どんどん未聴のMP3ファイルが溜ってきています。各ファイルにはID3タグを書き込んでいるので、後からでも整理はできるだろう、と放置しているものの、このままでは困ったことになりそうです。

かってのように、カセットテープ代や整理場所の工面に頭を悩ませる必要が無くなったのに、今度はどんどんたまっていくMP3ファイルをどう消化していくかが悩みのタネになるというのは、マニアの性(さが)は年を取っても変わらないということでしょうか(苦笑⁠⁠。

リスト radiru_id3.py
 1  #! /usr/bin/python
 2  # -*- coding: utf-8 -*-;
 3  
 4  import sqlite3, os, sys, eyed3
 5  from eyed3 import id3
 6  
 7  # global variables which show each directries
 8  config_path = "~/.radiru_confs"
 9  config = {'script_dir':'~/radiru_scripts', 'MP3_dir':'~/MP3','DB_dir':'~/MP3'}
10  
11  def init():
12      global config
13      conf_path = os.path.expanduser(config_path)
14      if os.path.isfile(conf_path):
15          f = open(conf_path,'r')
16          lines = f.readlines()
17          for i in lines:
18              (it, dt) = i.rstrip().split(':')
19              config[it] = dt
20          for key in config:
21              config[key] = os.path.expanduser(config[key])
22  
23  def query_db(query):
24      dbdir = config['DB_dir']
25      dbname = dbdir + '/radiru_titles.sql3'
26      if os.access(dbname, os.R_OK) == False:
27          print("cannot find title DB({})".format(dbname))
28          sys.exit(1)
29  
30      connection = sqlite3.connect(dbname)
31      cursor = connection.cursor()
32      c = cursor.execute(query)
33      res = c.fetchall()
34      if len(res) == 0:
35          print("DB doesn't have entry for id: {} ".format(query))
36          print("please check title DB:{}".format(dbname))
37          sys.exit(1)
38      else:
39          return(res)
40  
41  def query_contents(oid):
42      query = u"select * from contents where id={}".format(oid)
43      t = query_db(query)
44      return t
45  
46  def set_id3tag(file, tag):
47      mp3file = file
48  
49      new_tag = eyed3.id3.Tag()
50      new_tag.file_info = eyed3.id3.FileInfo(mp3file)
51      new_tag.title = tag['title']
52      new_tag.album = tag['album']
53      new_tag.artist = tag['act']
54      new_tag.release_date = tag['date']
55      if len(tag['contents']) > 0:
56          new_tag.comments.set(tag['contents'])
57  
58      new_tag.save()
59  
60  def main():
61      init()
62      filepath = sys.argv[1]
63      file = os.path.basename(filepath)
64      ufile = file.decode('euc-jp')
65  
66      query = u"select oid,title,date,ch,act from titles where filename='{}';".format(ufile)
67      (oid, title, date, channel, act) = query_db(query)[0]
68  
69      res = query_contents(oid)
70      contents = ''
71      for i in res:
72          (id, l) = i
73          contents = contents + l
74  
75      album_parts = ufile.rstrip('.mp3').split('_')
76      album = album_parts[1]
77      tag = {'title':title, 'album':album, 'act':act, 'date':date, 'ch':channel, 'contents':contents}
78      set_id3tag(filepath, tag)
79  
80  if __name__ == "__main__":
81      main()

おすすめ記事

記事・ニュース一覧