Introduce
初投稿です。
曲を解析し近似したものをユーザーに提供するアプリケーションを作っています。
以下のコードは、解析する曲を準備する段階のものです。
iTunesライブラリにある曲をwavファイルに変換し、別のディレクトリにコピーするコードを書きました。
スパゲティって言わないでください。わかってます。
Flowchart
iTunesライブラリに曲が増えた場合や消された場合、全てをコンバートし直すには時間がかかり同じタイトルを再度コンバートすることになる為、1回目と2回目以降の実行の挙動を変えることで効率化を図りました。
1回目の実行かの判断は、コピー先のディレクトリが存在しているかを基準にしています。
同時に、ユーザーがcsvを消した場合、2回目の実行が不可能になる為csvの存在も基準にしています。
csvは、保護された曲のタイトル名を保存しています。1回目の実行結果を2回目の実行に引き継ぐ方法を知らないので止むを得ずcsvを使用しました。2回目の実行では、保護されたタイトルを引いてからiTunesライブラリとコピー先のディレクトリの比較をすることで整合性を保っています。
1回目の実行
- コピー先のディレクトリを生成
- iTunesライブラリに存在するタイトルのパスをglobで取得。
- コンバート可能な曲は、コピー先のディレクトリに出し不可のものは絶対パスをcsvへ格納
- iTunesライブラリとコピー先のディレクトリを比較し正常に動作したかを確認
- 終了
2回目の実行
- コピー先のディレクトリとcsvファイルの存在を確認。どちらか存在しない場合は1回目の処理を実行
- iTunesライブラリとコピー先のディレクトリをglobして比較。Trueであれば終了。Falseは続行
- iTunesライブラリに増減があった場合、コピー先のディレクトリの中にコピー若しくは削除
- 再度、iTunesライブラリとコピー先のディレクトリの比較を行う。Falseであれば上の行を実行。
- 終了
Code
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行で書けちゃいました...
悲しかったです...