Windowsで重複した音楽ファイルを安全に検出して整理する方法(実体験解説)

MacからWindowsへメインPCを移行して半年。音楽ライブラリ管理のためのデータは暗号署名を切るという危険を冒して速さを求めたNASに保存されています。そのため、Windowsへの移行はすんなりと終わりました。
しかし、いざライブラリを読み込むと重複ファイルが大量に眠っていることが発覚しました。

本記事では、重複した音楽ファイル問題に直面した私の実体験を元に、確実で安全に重複音楽ファイルを見つけ出し、人の判断で整理する方法を解説します。

この記事では、ずぼらな私が直面したことと、失敗したことやうまくいったことを紹介します。

なぜ「重複音楽ファイル」は放置すると不都合?

生来ずぼらな性格であったため、音楽ファイルの管理もずさんそのものでした。それ故に重複ファイルが大量にあったことに気が付くまで、かなりの時間を要しました。

大量の音楽ライブラリを扱うと、次のような問題が起こりがちになります。

  • ストレージ容量を無駄に消費する
  • 同じ曲が複数プレイリストに混在する
  • MusicアプリやiTunesなどのスキャン時に時間がかかる
  • 「音」は同じでも見た目やタグが違うため認識のずれが起こる

こうした音楽ファイルの重複は.DS_Storeや.AppleDoubleのようなゴミファイルよりも管理の混乱を招きます。

今回は使いませんが、重複検出ツールを使って完全自動削除を行う方法もありましたが、誤動作で別バージョンや偶然似た曲まで消されてしまうリスクがあります。

例えばLive版とスタジオ版の曲が似ていると判定されて削除されてしまったらショックです。

PCでの重複ファイル検出は意外と難しい

一般的にファイルの重複検出は「ハッシュ値」で行います。完全一致ならばそのファイルは重複していることが確定しているからです。画像ならばかなりの精度で検出できますが、音楽ファイルの場合は結構難しいです。

  • 同じ音でもタグ情報やアルバムアートが違うだけで別ファイルになる
  • バイナリ違い(1bit)で別ファイルと判断される
  • 完全一致基準だと「同じファイルなのに異なるファイル」と誤認されることが多い

実際、MD5、SHA256、pHashといった主要なハッシュ方式で検出を試みても、結果は実用レベルに達しませんでした。

ハッシュ値での重複検出が向いていない理由

具体的にハッシュ方式でうまくいかなかった点についてまとめてみました。

手法問題
MD5タグやアートワークの違いでファイルを別物と判断されることが多いです
SHA256MD5より高精度だけれども、微妙な違いも厳密に別物として扱います
pHash(知覚ハッシュ)音の特徴ベースは良かった。しかし緩さゆえ、誤検知や別曲まで含んでしまった

どの方式も「音」として同じでも何かが違えば別物扱いになるため、実際に役に立つようなものではありませんでした。

最も確実な方法:ログ出力+人の目で判断する

もう、本末転倒ですね。私は重複する曲を洗い出して、別フォルダに退避させて、自分の判断で削除か戻すかをしたかっただけです。しかし、なかなかうまくいかない現状を目の当たりにし、一つの結論にたどり着きました。

それは、PCに探してもらって、目視で手動削除という解でした。実行するとDuplicate_Music_Log.txtにフルパスとファイルサイズが表示されます。

スクリプトでライブラリ全体を捜索してもらい、曲ファイルの重複を検出し、ログにフルパスを記述していきます。削除はそれら重複ファイルを見比べ、不要な方を人力で削除するという方法を取りました。

面倒ですが、意外とメリットもありました。

  • 誤削除のリスクがほぼ無い
  • 音楽DBやタグ情報に依存せず判定できる
  • プレイリストやタグ違いも人の判断で安全に整理できる

Windows PowerShell 5.1で重複検出とログの出力

この記事で使ったPowerShellのコンセプトは「重複の疑いがある音楽ファイルをなるべく早く見つけ出す」です。

  • 正規化ファイル名 + 再生時間を目安に重複候補を抽出
  • 重複候補をテキストに出力
  • テキストのフルパスを見比べながら手動で削除を行う

実際のスクリプト(PowerShell 5.1とUTF-8 with BOM)

今回のスクリプトは、単にファイルを比べるだけでなく、エクスプローラーのインデックスを直接見て、同じフォルダの情報を再利用するという、方法を採用してみました。10,000曲とかあった場合は速度の差が出ると思います。

また日本語があるため、次のスクリプトはUTF-8 with BOMで保存して実行してください。

# ===================================================
# 音楽ライブラリ整理
# Windows PowerShell 5.1 と UTF-8 with BOM です
# 判定:正規化ファイル名 + 演奏時間で重複を判断します
# ===================================================

$targetDir = "F:\iTunes"
$logPath   = "F:\iTunes\Duplicate_Music_Log.txt"

Write-Host "スキャン中..." -ForegroundColor Cyan

# =========================
# 初期設定
# =========================
$extPattern    = '\.(mp3|m4a|m4p|flac|wav)$'
$suffixPattern = '\s\d+$|\s-\sコピー$'

$shell = New-Object -ComObject Shell.Application

# =========================
# ファイル取得
# =========================
$allFiles = Get-ChildItem -LiteralPath $targetDir -Recurse -File |
            Where-Object { $_.Name -match $extPattern }

# 容量予約
$musicData = [System.Collections.Generic.List[object]]::new($allFiles.Count)

# フォルダCOMキャッシュ
$folderCache = @{}

# =========================
# メタ情報収集
# =========================
foreach ($file in $allFiles) {

    $dir = $file.DirectoryName

    # NameSpaceは1フォルダ1回だけ
    if (-not $folderCache.ContainsKey($dir)) {
        $folderCache[$dir] = $shell.NameSpace($dir)
    }

    $folder = $folderCache[$dir]
    $item   = $folder.ParseName($file.Name)

    if (-not $item) { continue }

    # Explorerと同じ「長さ」取得
    $duration = $folder.GetDetailsOf($item, 27) -replace '[^0-9:]', ''

    # 取れなければ即スキップ
    if (-not $duration) { continue }

    $normalizedName = $file.BaseName -replace $suffixPattern, ''

    $musicData.Add([PSCustomObject]@{
        FullName = $file.FullName
        NameKey  = $normalizedName.ToLower().Trim()
        Duration = $duration
        Size     = $file.Length
    })
}

# =========================
# 重複抽出
# =========================
$duplicateMusic =
$musicData |
Group-Object NameKey, Duration |
Where-Object Count -gt 1


# =========================
# 空フォルダ検出
# =========================
$emptyFolders =
Get-ChildItem -LiteralPath $targetDir -Recurse -Directory |
Where-Object { (Get-ChildItem -LiteralPath $_.FullName).Count -eq 0 }


# ========================
# ログ
# ========================
$log = [System.Collections.Generic.List[string]]::new()

$log.Add("=== 音楽整理ログ $(Get-Date) ===")
$log.Add("対象: $targetDir")
$log.Add("判定基準: ファイル名(正規化) + 演奏時間")
$log.Add("----------------------------------------------------")

$log.Add("")
$log.Add("[重複ファイル]")

foreach ($group in $duplicateMusic) {

    $log.Add("")
    $log.Add("● $($group.Values[0])  ($($group.Values[1]))")

    foreach ($item in $group.Group) {
        $sizeMB = [Math]::Round($item.Size / 1MB, 2)
        $log.Add("  - $($item.FullName)  ($sizeMB MB)")
    }
}

$log.Add("")
$log.Add("----------------------------------------------------")
$log.Add("[空フォルダ]")

if ($emptyFolders) {
    $emptyFolders | ForEach-Object { $log.Add("  - $($_.FullName)") }
}
else {
    $log.Add("なし")
}

# UTF-8 with BOM(5.1安全)
$log | Out-File -FilePath $logPath -Encoding utf8

Write-Host "完了 → $logPath" -ForegroundColor Green

# =============================
# Compornent Object Modelの解放
# =============================
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($shell) | Out-Null
[System.GC]::Collect()
[System.GC]::WaitForPendingFinalizers()

※ 実行結果はログとしてDuplicate_Music_Log.txtに出力されます。重複候補が一覧で確認できます。

300件近く見つかりましたが、3,700件あっても2分もかからない程度でした。
環境としては、音楽ファイルは外付けHDDでUSB3.0に格納されています。

よくある質問(FAQ)

Q
MD5やSHA256は絶対に使えませんか?
A

「音」として同一でもタグやメタ情報が違うだけで別物になってしまいます。そのため音楽ファイルには向いていません。

Q
自動で削除した方が楽ではないのですか?
A

.mp3と.flacなど別フォーマットでも同じ曲として検出される可能性は0%ではありません。意図しない削除と悲しみを避けるために手動で削除します。

Q
実行中のエラーや、ファイル名により止まったりしませんか?
A

確かに[Disk 1]のように特殊な記号が含まれていると止まりました。今回悩んだところです。解決策として -LiteralPathというオプションを試したら通りました。特殊文字を含むパスでもエラーを出さずに動作します。権限に関しては力及ばず、自動スキップで妥協しました。

まとめ

今回のスクリプト作成にあたり、重視したことは「安全かつ速く」です。

最初は自動削除を実装する予定がありました。しかし、ログを取ってみたところ、違うアルバムの同じ曲が重複として判定されることがわかったり、アルバムアートの好みなど、自分で選ぶ必要があると判断し、自動削除はやめました。

「PCはツール、素人のスクリプトは怪しい、最後は人間が選ぶ」役割分担が確実性に優れていると確信しました。

リファレンス

関連ページ

コメント