Note: These documents are written in Japanese.
by Claude (Anthropic)
このシステムは「CSV を介したタグ編集」という単純なワークフローを、確実かつ安全に実現することを目的としています。
派手な機能は一切ありません。「読み出す・編集する・書き戻す」の3ステップを、22,000件以上のファイルに対して壊さずに実行できること。それだけを目標にしました。
.mp3ファイル群(22,477件)
↓ analyze_mp3.py(事前分析)
↓ mp3_to_csv.py
mp3_tags.csv
↓ 【表計算アプリで編集】
mp3_tags.csv(編集済み)
↓ csv_to_mp3.py
.mp3ファイル群(タグ更新済み)
各スクリプトは独立して動作します。mp3_to_csv.py の出力を csv_to_mp3.py が受け取る以外に、スクリプト間の依存関係はありません。
実装前に analyze_mp3.py で全22,477件をスキャンし、データ構造を把握しました。
判明した主要データ:
| 項目 | 内容 |
|---|---|
| ID3v2.3 | 71.0%(15,964件)← 書き戻し対象バージョン |
| ID3v2.4 | 13.8%(3,113件) |
| ID3v1.1 | 10.8%(2,418件) |
| ID3v2.2 | 3.5%(778件) |
| タグなし | 0.9%(196件) |
COMMフレームの実態:
COMM:ID3v1 Comment:eng : 9,480件
COMM::eng : 8,950件
COMM:iTunNORM:eng : 365件(iTunes内部データ)
COMM:iTunSMPB:eng : 202件(iTunes内部データ)
COMM が人間のコメントとツール内部データを混在させていること、PRIV フレームがバイナリの Windows Media Player データを含むことが判明。これらを「CSV に出さず、触らず保持する」という設計判断に直結しました。
この分析なしに実装を始めていれば、COMM のカオスと PRIV のバイナリデータに実装途中で遭遇することになったはずです。
全フレームをCSVに出す案(B案)を採らず、編集対象のみを出力する設計(C案)を採用しました。
編集対象(CSVに出す):
TIT2, TPE1, TALB, TRCK, TDRC/TYER,
TCON, TPE2, TPOS, TCOP, TCOM, TPE3
保持のみ(CSVに出さない):
COMM(コメント群)
APIC(アートワーク)
PRIV(バイナリメタデータ)
TXXX(カスタムフィールド)
その他全て
「触らぬ神に祟りなし」。CSVにない情報は書き戻し時もそのまま保持します。
2000年代初頭の日本語環境でリッピングされたファイルは、encoding=0(Latin-1指定)のまま Shift-JIS バイト列が書き込まれています。mutagen はこれを Latin-1 として読むため文字化けします。
対処として try_decode_shift_jis() を実装しました:
def try_decode_shift_jis(text: str) -> str:
try:
return text.encode('latin-1').decode('shift-jis')
except (UnicodeEncodeError, UnicodeDecodeError):
return text # 本物のLatin-1テキストはそのまま返すencoding=0 のフレームに対してのみ適用します。UTF-16 や UTF-8 のフレームは触りません。
本物の Latin-1 テキスト(フランス語 Été など)への誤爆は実験で確認済みで、UnicodeDecodeError が発生して元の値が保持されます。
ID3v2.4 の TDRC フレームは 1996-04-22 のような日付まで持てますが、v2.3 の TYER は4桁の年のみです。
mutagen に v2_version=3 を指定しても TDRC から TYER への自動変換は行われないことを実験で確認しました(バイナリレベルで TDRC のまま書き込まれる)。
対処:
# "1996-04-22" → "1996" に丸める
year_only = year_str.split('-')[0].strip()
# TYER に書き込み、TDRC の残骸を削除
tags["TYER"] = TYER(encoding=1, text=year_only)
if "TDRC" in tags:
del tags["TDRC"]なお、mutagen は読み込み時に TYER を内部的に TDRC へ変換します。「書き戻し後に読んだら TDRC になっていた」は正常な挙動です。バイナリ上は TYER として正しく書かれています。
CSV のセルが空欄の場合、対応するフレームを削除します。
if value:
tags[frame_class.__name__] = frame_class(encoding=1, text=value)
else:
if frame_name in tags:
del tags[frame_name] # 空欄 = 削除「CSV はプレーンテキストで、データが勝手に消える心配がない。空欄にしたのは意図的」という判断によります。
書き戻しは全ファイル一律 ID3v2.3 + UTF-16 で保存します。
tags.save(mp3_path, v2_version=3, v1=0)
# encoding=1(UTF-16)は各フレームオブジェクト生成時に指定v2_version=3:ID3v2.3で保存v1=0:ID3v1タグを書かない(v2.3に統一)encoding=1:UTF-16(BOM付き)
ID3v2.3 + UTF-16 を選んだ理由は notes_why_id3v2_3.md に詳細があります。要約すると「互換性が最も高い組み合わせ」です。
表計算アプリによって書き出し形式が異なるため、先頭行を読んでタブの有無で自動判定します:
if '\t' in first_line:
return '\t'
else:
return ','文字コードは utf-8-sig → utf-8 → shift-jis → latin-1 の順で試みます。
CSV の file_path 列は MP3 ディレクトリからの相対パスで記録します。HDD を別の Mac に繋いだ時にマウントポイントが変わっても、スクリプトへの引数(ベースディレクトリ)さえ正しければ動作します。
両スクリプトとも「エラーが出ても続行」方針です:
try:
result = write_tags_to_file(mp3_path, row)
except Exception as e:
error_count += 1
error_list.append({"file": file_path_str, "error": str(e)})
continue # 次のファイルへ22,000件の一括処理で数件のエラーが全体を止めるのは非効率です。エラーの詳細は最後にサマリーとして表示します。
| 項目 | 結果 |
|---|---|
| Shift-JIS文字化けの修正 | ✓ 正常 |
year の日付丸め(1996-04-22 → 1996) |
✓ 正常 |
| APIC(アートワーク)の保持 | ✓ 残存確認 |
| 空欄での削除 | ✓ 削除確認 |
| バックアップCSVからの復元 | ✓ 復活確認 |
最後の「バックアップCSVからの復元」は「CSV がバックアップとして機能すること」の実証です。
現在の設計で対応していない項目:
- 読み取りエラーファイル8件(
_未満フォルダ内、【】を含むファイル名): OS レベルのパス問題と推測。このプロジェクトのスコープ外。 - 並列処理: 現在は逐次処理。22,000件でも実用上問題ない速度。
- 差分更新: 「変更のあったファイルだけ再処理」は未実装。必要になったら
id3_version列や更新日時の活用を検討。
スクリプトが独立しているため、これらの追加は既存コードに影響しません。
Claude's Note: このプロジェクトで技術的に最も興味深かったのは、mutagen の TYER/TDRC 変換挙動でした。「バイナリには TYER で書かれるが、読むと TDRC になる」という仕様は、実際にバイナリを確認するまで分かりませんでした。ドキュメントだけでは分からないことを、動かして確かめる。当たり前のことですが、重要です。
[End of File]