2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

GeminiCLIに、バラバラになったiTunesフォルダを整理させる!

Last updated at Posted at 2025-07-05

# 音楽ファイルの統合とGemini CLIによる作業自動化の試み

スクリーンショット 2025-07-06 3.37.06.png

長年にわたりiTunesを中心に運用してきた音楽ライブラリは、現在、複数の外付けハードディスク(HDD 1〜7)に分散して保存されており、ファイルが物理的にも論理的にも点在している状況です。この分散状態は、ライブラリの一元管理を著しく困難にし、着手を躊躇する要因となっていました。

こうした背景のもと、ファイル統合の効率化を目的として、GoogleのGemini CLIを活用することを試みました。当初は大きな期待を寄せていなかったものの、実際に使用してみた結果、その高い処理能力と柔軟性に驚かされることとなりました。

スクリーンショット 2025-07-06 2.49.43.png

現在、1TBの作業用ディスクを準備し、統合作業を段階的に実施している最中です。今後は、統合完了後に不要重複の検出・削除、ファイル名・メタデータの正規化といった工程へと進む予定です。

何よりも、これがあの『ターミナル』?というほどのショックです!
アプリも作成でき、衝撃的な『機会』と巡りあうことができたことで睡眠時間を忘れて没頭しています。

スクリーンショット 2025-07-06 3.40.09.png

1,はい、このまま一度だけ実行
2.はい、常に実行
3.調整する
4.いいえ

見ているだけで、python のScript を勉強できるというのが斬新な感覚!

ヴァイブコーディングで何をやっているのかわからない作業でああしてこうして、という『のびた君コーディング』ではなく、何をしてもらうのかを明確に指示をしながら、何をやっているのかがわかるコーディングスタイル。
一応、仮に、『CLIコーディング』としておこう。

スクリーンショット 2025-07-06 3.42.12.png

自然言語のまま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 を作成しました。

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?