# 音楽ファイルの統合とGemini CLIによる作業自動化の試み
長年にわたりiTunesを中心に運用してきた音楽ライブラリは、現在、複数の外付けハードディスク(HDD 1〜7)に分散して保存されており、ファイルが物理的にも論理的にも点在している状況です。この分散状態は、ライブラリの一元管理を著しく困難にし、着手を躊躇する要因となっていました。
こうした背景のもと、ファイル統合の効率化を目的として、GoogleのGemini CLIを活用することを試みました。当初は大きな期待を寄せていなかったものの、実際に使用してみた結果、その高い処理能力と柔軟性に驚かされることとなりました。
現在、1TBの作業用ディスクを準備し、統合作業を段階的に実施している最中です。今後は、統合完了後に不要重複の検出・削除、ファイル名・メタデータの正規化といった工程へと進む予定です。
何よりも、これがあの『ターミナル』?というほどのショックです!
アプリも作成でき、衝撃的な『機会』と巡りあうことができたことで睡眠時間を忘れて没頭しています。
1,はい、このまま一度だけ実行
2.はい、常に実行
3.調整する
4.いいえ
見ているだけで、python のScript を勉強できるというのが斬新な感覚!
ヴァイブコーディングで何をやっているのかわからない作業でああしてこうして、という『のびた君コーディング』ではなく、何をしてもらうのかを明確に指示をしながら、何をやっているのかがわかるコーディングスタイル。
一応、仮に、『CLIコーディング』としておこう。
自然言語のままScriptが読める
なんといっても、コメントアウトがとても丁寧。こちらの力量を学習しているからなんだろう。
すでに、スクリプトも対話を重ねてバージョン5になっている。
WriteFile Writing to organize_music_execute_v5.py │
│ │
│ 1 │
│ 2 import os │
│ 3 import pathlib │
│ 4 import shutil │
│ 5 │
│ 6 # --- 設定 --- │
│ 7 # 音楽ファイルが散らばっている検索範囲のトップフォルダ (今回は 4T_HD/iTunes のみ) │
│ 8 SOURCE_ROOTS = [ │
│ 9 pathlib.Path('/Volumes/4T_HD/iTunes'), │
│ 10 ] │
│ 11 # 整理後の音楽ファイルを保存するフォルダ │
│ 12 DEST_ROOT = pathlib.Path('/Volumes/samsung/iTunes_Music_Organized') │
│ 13 # 探す音楽ファイルの拡張子 │
│ 14 MUSIC_EXTENSIONS = {'.mp3', '.m4a', '.aac', '.flac', '.wav', '.aiff', '.alac'} │
│ 15 # iTunes/ミュージックアプリの音楽フォルダ構造の目印となるフォルダ名 │
│ 16 MUSIC_FOLDER_NAME = 'Music' # iTunes/Music/アーティスト/アルバム/曲名 を想定 │
│ 17 │
│ 18 # 検索から除外するフォルダ │
│ 19 EXCLUDE_DIRS = [ │
│ 20 # 4T_HD/iTunes 内で除外したいパスがあればここに追加 │
│ 21 ] │
│ 22 │
│ 23 # アーティスト名統一マッピング │
│ 24 ARTIST_MAPPING = { │
│ 25 'Paul kanda': 'PaulKanda', │
│ 26 'PaulKanda': 'PaulKanda', │
│ 27 # 他にも統一したいアーティスト名があればここに追加 │
│ 28 } │
│ 29 │
│ 30 def main(): │
│ 31 """ │
│ 32 メイン処理 │
│ 33 """ │
│ 34 print("--- 音楽ファイル整理 実行 (v5) ---") │
│ 35 print("検索フォルダ:") │
│ 36 for root in SOURCE_ROOTS: │
│ 37 print(f"- {root}") │
│ 38 print(f"移動先フォルダ: {DEST_ROOT}") │
│ 39 print("以下のフォルダは検索から除外されます:") │
│ 40 for d in EXCLUDE_DIRS: │
│ 41 print(f"- {d}") │
│ 42 print("以下のアーティスト名が統一されます:") │
│ 43 for old, new in ARTIST_MAPPING.items(): │
│ 44 print(f"- '{old}' -> '{new}'") │
│ 45 print("-" * 40) │
│ 46 │
│ 47 found_files = [] │
│ 48 │
│ 49 for current_source_root in SOURCE_ROOTS: │
│ 50 for p in current_source_root.rglob('*'): │
│ 51 is_excluded = False │
│ 52 for excluded_dir in EXCLUDE_DIRS: │
│ 53 if excluded_dir in p.parents: │
│ 54 is_excluded = True │
│ 55 break │
│ 56 if is_excluded: │
│ 57 continue │
│ 58 │
│ 59 if p.suffix.lower() in MUSIC_EXTENSIONS: │
│ 60 if DEST_ROOT in p.parents: │
│ 61 continue │
│ 62 found_files.append(p) │
│ 63 │
│ 64 plan = {} # {移動元パス: 移動先パス} の辞書 │
│ 65 destinations = set() # 移動先の重複を防ぐためのセット │
│ 66 │
│ 67 for source_path in found_files: │
│ 68 relative_path = None │
│ 69 try: │
│ 70 parts = source_path.parts │
│ 71 base_index = -1 │
│ 72 for root in SOURCE_ROOTS: │
│ 73 if root in source_path.parents: │
│ 74 base_index = len(root.parts) │
│ 75 break │
│ 76 │
│ 77 if base_index != -1: │
│ 78 # SOURCE_ROOT からの相対パスを取得 │
│ 79 relative_parts = list(parts[base_index:]) │
│ 80 │
│ 81 # iTunes Media/Music/アーティスト/アルバム/曲名 │
│ のような構造を想定し、 │
│ 82 # │
│ Musicフォルダが見つかればその直下から、そうでなければSOURCE_ROOT直下から処理 │
│ 83 processed_parts = [] │
│ 84 found_music_folder = False │
│ 85 for i, part in enumerate(relative_parts): │
│ 86 if part == MUSIC_FOLDER_NAME: │
│ 87 processed_parts.extend(relative_parts[i+1:]) │
│ 88 found_music_folder = True │
│ 89 break │
│ 90 if not found_music_folder: │
│ 91 processed_parts = relative_parts │
│ 92 │
│ 93 # アーティスト名マッピングの適用 │
│ 94 if len(processed_parts) > 0 and processed_parts[0] in │
│ ARTIST_MAPPING: │
│ 95 processed_parts[0] = ARTIST_MAPPING[processed_parts[0]] │
│ 96 │
│ 97 # アルバム名重複の平坦化 (例: Artist/Album/Album/Song -> │
│ Artist/Album/Song) │
│ 98 # 少なくとも3つの要素 (Artist, Album, Song) があり、 │
│ 99 # 2番目と3番目の要素が同じ場合に平坦化 │
│ 100 if len(processed_parts) >= 3 and processed_parts[1].lower() == │
│ processed_parts[2].lower(): │
│ 101 relative_path = pathlib.Path(processed_parts[0]) / │
│ pathlib.Path(*processed_parts[2:]) │
│ 102 elif processed_parts: │
│ 103 relative_path = pathlib.Path(*processed_parts) │
│ 104 else: │
│ 105 relative_path = pathlib.Path(source_path.name) │
│ 106 else: │
│ 107 # どの SOURCE_ROOT にも属さない場合 (通常は発生しないはず) │
│ 108 relative_path = pathlib.Path('__Sorted_Others') / source_path.name │
│ 109 │
│ 110 except Exception: │
│ 111 # 予期せぬエラーの場合も __Sorted_Others に分類 │
│ 112 relative_path = pathlib.Path('__Sorted_Others') / source_path.name │
│ 113 │
│ 114 if relative_path: │
│ 115 dest_path = DEST_ROOT / relative_path │
│ 116 │
│ 117 if dest_path in destinations: │
│ 118 counter = 1 │
│ 119 original_stem = dest_path.stem │
│ 120 original_suffix = dest_path.suffix │
│ 121 while dest_path in destinations: │
│ 122 dest_path = dest_path.with_name(f"{original_stem}_({counter}) │
│ {original_suffix}") │
│ 123 counter += 1 │
│ 124 │
│ 125 plan[source_path] = dest_path │
│ 126 destinations.add(dest_path) │
│ 127 │
│ 128 # 実際のファイル移動を実行 │
│ 129 moved_count = 0 │
│ 130 skipped_count = 0 │
│ 131 error_count = 0 │
│ 132 │
│ 133 if not plan: │
│ 134 print("移動対象の音楽ファイルが見つかりませんでした。") │
│ 135 else: │
│ 136 for src, dst in plan.items(): │
│ 137 try: │
│ 138 dst.parent.mkdir(parents=True, exist_ok=True) │
│ 139 │
│ 140 if dst.exists(): │
│ 141 print(f"[スキップ] '{src}' (理由: │
│ 移動先に同名ファイルが既に存在)") │
│ 142 skipped_count += 1 │
│ 143 else: │
│ 144 shutil.move(str(src), str(dst)) │
│ 145 print(f"[移動] '{src}' -> '{dst}'") │
│ 146 moved_count += 1 │
│ 147 except Exception as e: │
│ 148 print(f"[エラー] '{src}' の移動中にエラーが発生しました: {e}") │
│ 149 error_count += 1 │
│ 150 │
│ 151 print("-" * 40) │
│ 152 print(f"ファイル移動完了。") │
│ 153 print(f"移動されたファイル数: {moved_count}") │
│ 154 print(f"スキップされたファイル数 (重複): {skipped_count}") │
│ 155 print(f"エラーが発生したファイル数: {error_count}") │
│ 156 │
│ 157 # 移動元フォルダが空になったら削除 │
│ 158 for source_root_to_check in SOURCE_ROOTS: │
│ 159 if source_root_to_check.is_dir() and not any │
│ (source_root_to_check.iterdir()): │
│ 160 try: │
│ 161 os.rmdir(str(source_root_to_check)) │
│ 162 print(f"[削除] 空になったフォルダを削除しました: │
│ {source_root_to_check}") │
│ 163 except OSError as e: │
│ 164 print(f"[エラー] フォルダの削除中にエラーが発生しました: │
│ {source_root_to_check} - {e}") │
│ 165 │
│ 166 print("--- スクリプト終了 ---") │
│ 167 │
│ 168 if __name__ == '__main__': │
│ 169 main() │
╰────────────────────────────────────────────────────────────────────────────────────────────╯
✦ organize_music_execute_v5.py を作成しました。