Edited at

【Python】iTunesの曲を全てコンバートした。


Introduce

初投稿です。

曲を解析し近似したものをユーザーに提供するアプリケーションを作っています。

以下のコードは、解析する曲を準備する段階のものです。

iTunesライブラリにある曲をwavファイルに変換し、別のディレクトリにコピーするコードを書きました。

スパゲティって言わないでください。わかってます。


Flowchart

iTunesライブラリに曲が増えた場合や消された場合、全てをコンバートし直すには時間がかかり同じタイトルを再度コンバートすることになる為、1回目と2回目以降の実行の挙動を変えることで効率化を図りました。

1回目の実行かの判断は、コピー先のディレクトリが存在しているかを基準にしています。

同時に、ユーザーがcsvを消した場合、2回目の実行が不可能になる為csvの存在も基準にしています。

csvは、保護された曲のタイトル名を保存しています。1回目の実行結果を2回目の実行に引き継ぐ方法を知らないので止むを得ずcsvを使用しました。2回目の実行では、保護されたタイトルを引いてからiTunesライブラリとコピー先のディレクトリの比較をすることで整合性を保っています。

1回目の実行

1. コピー先のディレクトリを生成

2. iTunesライブラリに存在するタイトルのパスをglobで取得。

3. コンバート可能な曲は、コピー先のディレクトリに出し不可のものは絶対パスをcsvへ格納

4. iTunesライブラリとコピー先のディレクトリを比較し正常に動作したかを確認

5. 終了

2回目の実行

1. コピー先のディレクトリとcsvファイルの存在を確認。どちらか存在しない場合は1回目の処理を実行

2. iTunesライブラリとコピー先のディレクトリをglobして比較。Trueであれば終了。Falseは続行

3. iTunesライブラリに増減があった場合、コピー先のディレクトリの中にコピー若しくは削除

4. 再度、iTunesライブラリとコピー先のディレクトリの比較を行う。Falseであれば上の行を実行。

5. 終了


Code


copymusic.py

import os

import shutil
import glob
import csv
from pydub import AudioSegment,exceptions

homedir = os.environ['HOME']
prj_dir = '{}/Desktop/project'.format(homedir)
new_dir = '{}/Desktop/project/wav'.format(homedir)
artist_dir = '{}/Music/iTunes/iTunes Media/Music/'.format(homedir)
wav_tree = '{}*/*'.format(new_dir)
music_tree = '{}*/*/*'.format(artist_dir)
csv_path = '{}/DONOTTOUCH.csv'.format(prj_dir)
extension = ['.m4a','.mp3','.wav','.aac','.aiff']
drm_title_list = []
flag = 0

def initialize():
os.makedirs(new_dir)
for path in glob.glob(music_tree):
title = os.path.basename(path)
title_extension = os.path.splitext(title)[1]
re_extension_path = os.path.join(new_dir,'{}.wav'.format(os.path.splitext(title)[0]))
if title_extension == '.m4a':
try:
song = AudioSegment.from_file(path)
except exceptions.CouldntDecodeError:
drm_title_list.append(path)
else:
song.export(re_extension_path,format='wav')
if title_extension == '.mp3':
try:
song = AudioSegment.from_mp3(path)
except exceptions.CouldntDecodeError:
drm_title_list.append(path)
else:
song.export(re_extension_path,format='wav')
if title_extension == '.wav':
shutil.copy(path,os.path.join(new_dir,title))
if title_extension == '.aac':
try:
song = AudioSegment.from_file(path)
except exceptions.CouldntDecodeError:
drm_title_list.append(path)
else:
song.export(re_extension_path,format='aac')
if title_extension == '.aiff':
try:
song = AudioSegment.from_file(path)
except exceptions.CouldntDecodeError:
drm_title_list.append(path)
else:
song.export(re_extension_path,format='aiff')
with open(csv_path,'w') as f:
writer = csv.writer(f)
writer.writerow(drm_title_list)
return print('Copy completion')

def title_list(path_list):
"""
ディレクトリパスのリストを引数とし、拡張子の省いた曲名だけが返される。
:param path_list: list glob.glob(path).
:return: list song title.
"""

titles = []
for path in path_list:
basename = os.path.basename(path)
if os.path.splitext(basename)[1] in extension:
title = os.path.splitext(basename)[0]
titles.append(title)
return set(titles)

def copy_title(add_title):
for title in add_title:
for path in music_path_set:
if title in path:
basename = os.path.basename(path)
title_extension = os.path.splitext(basename)[1]
re_extension_path = os.path.join(new_dir,'{}.wav'.format(title))
if title_extension == '.m4a':
try:
song = AudioSegment.from_file(path)
except exceptions.CouldntDecodeError:
drm_title_list.append(path)
else:
song.export(re_extension_path,format='wav')
if title_extension == '.mp3':
try:
song = AudioSegment.from_mp3(path)
except exceptions.CouldntDecodeError:
drm_title_list.append(path)
else:
song.export(re_extension_path,format='wav')
if title_extension == '.wav':
shutil.copy(path,os.path.join(new_dir,title))
if title_extension == '.aac':
try:
song = AudioSegment.from_file(path)
except exceptions.CouldntDecodeError:
drm_title_list.append(path)
else:
song.export(re_extension_path,format='aac')
if title_extension == '.aiff':
try:
song = AudioSegment.from_file(path)
except exceptions.CouldntDecodeError:
drm_title_list.append(path)
else:
song.export(re_extension_path,format='aiff')

def rm_title(titles):
for title in titles:
extension_add_title = '{}.wav'.format(title)
rm_title_path = os.path.join(new_dir,extension_add_title)
os.remove(rm_title_path)

if not os.path.isdir(new_dir) or not os.path.exists(csv_path):
flag = flag + 1
initialize()
with open(csv_path) as f:
reader = csv.reader(f)
for row in reader:
drm_title_list = row
music_path_set = set(glob.glob(music_tree))
for drm_title in drm_title_list:
music_path_set.remove(drm_title)
wav_path_set = set(glob.glob(wav_tree))
music_set = title_list(music_path_set)
wav_set = title_list(wav_path_set)
while music_set != wav_set:
if music_set > wav_set:
titles = music_set - wav_set
copy_title(titles)
elif music_set < wav_set:
titles = wav_set - music_set
rm_title(titles)
music_path_set = set(glob.glob(music_tree))
for drm_title in drm_title_list:
music_path_set.remove(drm_title)
wav_path_set = set(glob.glob(wav_tree))
music_set = title_list(music_path_set)
wav_set = title_list(wav_path_set)
if flag == 0:
print('差異なし')



Commentary


initialize()

iTunesライブラリにあるコンバート可能な曲をコピーする関数です。

愚直で大量の分岐処理は、拡張子毎に使用するモジュールが違うためこのようになりました。

各分岐にある例外処理は、保護された曲をコンバートした際のエラーを条件にリストに格納しています。

このリストは、全てのコンバートが終わった後にcsvに書き出されます。

この部分は改良の余地がありますが、スパゲティを美味しくするためのソースだと思えば問題ありません。


title_list()

globで取得したパスからファイル名のみをset型で返す関数です。

globは、対象のディレクトリのパスを全て取ってきてくれます。

実行結果はこのようなものです。ーー>「hoge/fuga/piyo.m4a」,「hoge/fuga/piyo.mp3」,「hoge/fuga/piyo.txt」

このような結果が返ってきては、iTunesライブラリとコピー先のディレクトリをglobしただけでは比較が取れません。そこで、この関数を通すことで拡張子とパスを省いた純粋なファイル名「piyo」を取得し結果同士を比較することができます。


copy_title()

効率化の肝となる関数です。

iTunesライブラリに新しい曲が追加された際、その曲のみをコピー先のディレクトリにコンバート&コピーします。


rm_title()

copy_title()と同様に、効率化の肝となる関数です。

iTunesライブラリから曲が消された場合、その曲のみをコピー先のディレクトリから削除します。


P.S

このコードを書き始めた時、iTunesライブラリの曲の全て取ってくるところから始まりました。

その時はglobを知りませんでした。10行程度でしたがかなり頭を捻らせて考えました。

書き終えた時はとても嬉しかったのですが、globを知ってしまいました。

1行で書けちゃいました...

悲しかったです...