【個人開発】スマホから申請 → 自動高音質DL → タグ・ジャケット付け → ライブラリ配置まで全自動の音楽管理システムを作った
はじめに
「聴きたい曲を YouTube で見つけたけど、毎回手動でダウンロードしてタグ付けして NAS に置くのが面倒...」
そんな悩みを解決するために、スマホのブラウザから曲名を送るだけで、PC 上で自動的にダウンロード → 高精度タグ付け → Navidrome 配置まで完結するシステム を PowerShell + Python で構築しました。
さらに、YouTube のプレイリスト URL をそのまま送るだけ で、バンドの全曲集やアルバム単位のプレイリストを丸ごと取り込むこともできます。もう「1曲ずつちまちま申請する」必要はありません。
本記事では、システムの全体像 と 構築でハマったポイント を共有します。
リポジトリ
# https://github.com/g206rgbitoodesu/YoutubeDownload
想定読者
| 対象 | 説明 |
|---|---|
| 自宅サーバー派 | Navidrome / Jellyfin などの音楽サーバーを立てている方 |
| yt-dlp ユーザー | 音楽を集めているが、タグ付けや整理が面倒だと感じている方 |
| PowerShell 好き | ちょっとした自動化ツールを作ってみたい方 |
前提環境
| ツール | バージョン(例) |
|---|---|
| Windows | 10 / 11 |
| PowerShell | 5.1 以上 |
| yt-dlp | 最新版 |
| ffmpeg / ffprobe | 最新版 |
| MusicBrainz Picard | 最新版 |
| Chromaprint (fpcalc) | 1.6.1 |
| Navidrome | 0.64.2 |
| Python | 3.10 以上(Web UI 用) |
完成したシステムの全体像
┌─────────────────────────────────────────────────────────────┐
│ スマホのブラウザ │
│ 「Queen, Bohemian Rhapsody」と入力して送信 │
│ or プレイリストURLをそのまま貼り付け │
└────────────────────────┬────────────────────────────────────┘
│ Wi-Fi (LAN内)
▼
┌─────────────────────────────────────────────────────────────┐
│ Web サーバー (Flask / port 5000) │
│ songlist.csv / playlist.txt に追記 │
│ → run_all.ps1 をバックグラウンド起動 │
└────────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 統合パイプライン (run_all.ps1) │
│ │
│ [1] CSV整形 clean_songlist.ps1 │
│ [2] 未DL抽出 make_todo.ps1 │
│ [3] ダウンロード download_pc.ps1 (yt-dlp) │
│ [4] 検査 verify_downloads.ps1 (ffprobe) │
│ [5] 再DL redownload_failed.ps1 │
│ [6] タグ付け run_picard.ps1 (MusicBrainz Picard) │
│ [7] ファイル名整形 clean_music_download.ps1 │
│ [8] 再マッチング run_picard.ps1 (再) │
│ [9] ライブラリ配置 move_leftovers.ps1 │
└────────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Navidrome music フォルダ │
│ → Quick scan で自動認識 │
└─────────────────────────────────────────────────────────────┘
各ツールの役割
| ツール | 役割 |
|---|---|
| yt-dlp | YouTube から音声をダウンロード |
| ffprobe | ダウンロードしたファイルの整合性検査 |
| MusicBrainz Picard | 音声指紋(AcoustID)を使った高精度タグ付け |
| Navidrome | 自宅音楽ストリーミングサーバー |
| Flask | スマホからの申請を受け付ける Web UI |
なぜこの構成にしたか
既存ツールの限界
-
yt-dlp 単体:ダウンロードはできるが、タグは YouTube のタイトルそのまま。
artist=NAになりがち - Picard 単体:GUI 操作が前提。大量ファイルの自動処理には向かない
- Navidrome:タグが正しくないとライブラリが崩壊する
これらを 「パイプで繋ぐ」 発想で解決しました。
Picard のコマンドラインモードを活用
Picard には -e "FROM_FILE commands.txt" でコマンドファイルを実行する機能があります。これを使えば、GUI を開かずにバッチ処理 が可能です。
LOAD music_download
CLUSTER
LOOKUP
SAVE_MATCHED
REMOVE_SAVED
FINGERPRINT
SCAN
SAVE_MATCHED
REMOVE_SAVED
QUIT
QUITを付けないと Picard が終了しない、というのが最初のハマりポイントでした。
実装のポイント
1️ yt-dlp のクライアントローテーション
YouTube は時々、特定のクライアント(web_safari など)に 画像しか返さない ことがあります。そこで、複数のクライアントを順に試す仕組みを入れました。
$Clients = @("android", "ios", "web_safari", "tv_embedded", "")
foreach ($Client in $Clients) {
# 各クライアントで最大 $MaxRetries 回リトライ
$ExtractorArgs = @("--extractor-args", "youtube:player_client=$Client")
& yt-dlp @CommonArgs @ExtractorArgs $Target
if ($LASTEXITCODE -eq 0) { break }
}
android を先頭 にすることで、失敗時の待ち時間を大幅に短縮できました。
2️ 音声指紋による高精度タグ付け
Picard の FINGERPRINT → SCAN コマンドで、音声そのものの指紋 から楽曲を識別します。YouTube のタイトルが Artist - Title (Official Video) のようなゴミ付きでも、正しいタグが付きます。
# Picard コマンドファイル(動的生成)
$commands = @"
LOAD $musicDirPosix
CLUSTER
LOOKUP
SAVE_MATCHED
REMOVE_SAVED
FINGERPRINT
SCAN
SAVE_MATCHED
REMOVE_SAVED
QUIT
"@
注意:Picard は
#コメントを解釈しないため、コマンドファイルにはコマンドのみを記述し、BOM なし UTF-8 で保存する必要があります。
3️ スマホからの Web 申請フォーム
Flask で songlist.csv に追記し、run_all.ps1 をバックグラウンド起動するだけのシンプルな構成です。
@app.route("/submit", methods=["POST"])
def submit():
# CSV に追記
append_songs(items)
# パイプラインをバックグラウンド起動
threading.Thread(target=run_pipeline, args=(mode,), daemon=True).start()
return jsonify({"ok": True, "msg": f"{len(items)} 曲を申請しました"})
make_todo.ps1 が 既DL曲を自動スキップ するので、何度申請しても安全です。
4️ プレイリスト URL をそのまま受け付ける
Web 申請フォームでは、単曲の「アーティスト, 曲名」形式だけでなく、YouTube のプレイリスト URL もそのまま受け付けます。
https://www.youtube.com/playlist?list=PLxxxxxxxxxxxxx
これを送信すると、プレイリスト内の全曲が自動でダウンロード対象 になります。バンドの全曲集やアルバム単位のプレイリストを丸ごと取り込みたいときに便利です。
プレイリストの展開フロー
[Web UI] プレイリストURL受信
↓
[playlist.txt] に URL を記録
↓
[run_all.ps1 -Source playlist] で処理開始
↓
yt-dlp --yes-playlist で全曲を一括DL
↓
以降は通常のダウンロードフローと同じ
設計のポイント:CSV モードとプレイリストモードを 完全分離 することで、
make_todo.ps1の既DL判定ロジックを汚さずに済みます。
🔧 --flat-playlist で高速展開(応用編)
CSV モードでプレイリストを展開したい場合は、yt-dlp --flat-playlist でメタ情報だけを高速取得する方法もあります。
# プレイリストを展開して各曲を CSV に追記
function Expand-Playlist {
param([string]$PlaylistUrl)
# 動画をダウンロードせず、メタ情報だけ取得
$items = & yt-dlp `
--flat-playlist `
--print "%(playlist_index)s`t%(title)s`t%(webpage_url)s" `
$PlaylistUrl
foreach ($line in $items) {
$parts = $line -split "`t"
$index = $parts[0]
$title = $parts[1]
$url = $parts[2]
# CSV に "NN,Artist,Title,URL" として追記
Add-Content -Path "songlist.csv" `
-Value "$index,$title,$url" `
-Encoding UTF8
}
}
--flat-playlist を使うことで、動画をダウンロードせずにメタ情報だけを高速取得できます。100曲のプレイリストでも数秒で展開できます。
プレイリスト番号の保持
展開時に playlist_index(01, 02, 03…)を ファイル名の先頭に残す のがポイントです。
02 Queen Official - Queen - Doing All Right (Official Lyric Video Remastered).mp3
これにより:
- アルバム内の曲順が保たれる(Navidrome で正しい順番で表示される)
- 同じ曲の別バージョン(Live 版、Remaster 版)が混在しても衝突しない
- タグ付け後のファイル名で トラック番号として機能 する
clean_music_download.ps1 側では、先頭の NN を一時退避してから装飾を除去 し、最後に prefix を戻す処理を入れています。
# 先頭の連番 "NN " を退避
$prefix = ""
$body = $base
if ($base -match '^(\d+\s+)(.+)$') {
$prefix = $matches[1] # "02 "
$body = $matches[2] # "Queen Official - Queen - Doing All Right (...)"
}
# ... 装飾除去処理 ...
# prefix を戻して最終名を組み立て
$base = "$prefix$body"
注意:prefix を退避せずに正規表現を適用すると、
02が装飾パターンに巻き込まれて消えることがあります。「連番を分離 → 本体をクリーニング → 連番を戻す」の3ステップが鉄則です。
ハマったポイントと解決策
① Picard が終了しない → QUIT を追加
LOAD 〜 REMOVE_SAVED の後、QUIT コマンドが無いと Picard は GUI で待機したまま終了しません。
終了しない
LOAD music_download
LOOKUP
SAVE_MATCHED
QUIT を追加
LOAD music_download
LOOKUP
SAVE_MATCHED
REMOVE_SAVED
QUIT
② # コメントで Picard がエラー → コマンドファイルからコメント削除
Picard の -e モードは # をコメントとして扱いません。
E: Unknown command: #
コマンドファイルには コマンドのみ を書くようにしました。
ファイル名が破壊される → 「」 を装飾として削除しない
YouTube の日本語タイトルでは 「曲名」 が 正式な曲名 であることが多いです。当初 「」 を装飾として削除していたため、曲名が完全に消える 事故が発生しました。
# 曲名が消える
'\s*「[^」]*」'
# 「」は残す(パターンから除外)
また、⧸(全角スラッシュ)は 区切り文字 なので、削除ではなく - に置換するのが正解でした。
# 単語が連結する
$body = $body -replace '\s*⧸\s*', ''
# 区切り文字として置換
$body = $body -replace '\s*⧸\s*', ' - '
④ Navidrome のパスがバージョン依存 → ワイルドカードで自動検出
navidrome_0.64.2_windows_amd64 のようなバージョン付きフォルダ名は、アップデートのたびに変わります。ワイルドカードで自動検出するようにしました。
$candidates = @(
"$env:USERPROFILE\Downloads\navidrome*\music",
"$env:USERPROFILE\navidrome*\music",
"C:\navidrome\music"
)
⑤ 短い曲が「壊れたファイル」と誤判定される
Her Majesty(23秒)のような短い正規曲が、最小再生時間 30 秒 の閾値で NG 判定されていました。閾値を 15 秒 に下げて解決。
[int]$MinDuration = 15, # 30 → 15
⑥ プレイリスト処理でありがちなハマりどころ
| 症状 | 原因 | 対策 |
|---|---|---|
| 削除済み動画で全体が止まる | プレイリスト内の1曲が非公開 |
--ignore-errors を付与 |
| 曲順がバラバラになる |
playlist_index を捨てている |
ファイル名の先頭に保持 |
| 同じ曲が重複DLされる | 判定漏れ | アーカイブ (--download-archive) で管理 |
| 巨大プレイリストでタイムアウト | 展開に時間がかかる |
--flat-playlist で高速化 |
--ignore-errors は必須 です。プレイリストには地域制限・非公開・削除済みの動画が混ざっていることが多く、1曲の失敗で全体が止まるとストレスになります。
$CommonArgs = @(
"--ignore-errors", # エラー曲をスキップして続行
"--no-warnings",
"--extract-audio",
"--audio-format", "mp3",
"--audio-quality", "0",
"--embed-thumbnail", # ジャケット埋め込み
"--add-metadata" # 基本メタデータ付与
)
--embed-thumbnailと--add-metadataを付けることで、ダウンロード直後の時点で最低限のタグとジャケットが付きます。Picard がマッチできなかった曲でも、Navidrome 上でサムネイル付きで表示されるため、ライブラリの見た目が大幅に向上します。
セットアップ手順
1. リポジトリを配置
git clone https://github.com/g206rgbitoodesu/YoutubeDownload.git
cd YoutubeDownload
2. 環境自動セットアップ
setup.bat をダブルクリックするだけ。以下のツールを 自動検出して PATH に追加 します。
- yt-dlp
- ffmpeg / ffprobe
- fpcalc
- MusicBrainz Picard
未検出のツールがあれば winget でのインストールを提案します。
3. Picard GUI の初回設定(1回だけ)
Picard を起動し、以下を設定してください。
-
オプション → ファイル名設定
- 「Move files to a folder structure based on tags」を ON
- 移動先を Navidrome music フォルダに指定
-
オプション → 指紋
-
fpcalcのパスを指定 - AcoustID API キーを入力
-
4. 一括実行
.\run_all.ps1
5. Web 申請フォーム(オプション)
cd webapp
python -m pip install -r requirements.txt
.\start.bat
スマホで http://<PCのIP>:5000 にアクセスすれば、ブラウザから曲を申請できます。
実際の処理ログ(抜粋)
プレイリスト一括DLの場合
======================================================================
[3/9] ダウンロード (download_playlist)
======================================================================
=== プレイリスト一括ダウンロード ===
[1/1] プレイリスト処理中
URL: https://youtube.com/playlist?list=PLgNFpUIWnmtv-...
-> [android] 試行 1/2...
[youtube:tab] Playlist Mrs.GREEN APPLEヒット曲一覧: Downloading 38 items of 38
[download] Downloading item 1 of 38
[download] Destination: music_download\01 Mrs. GREEN APPLE - インフェルノ(Inferno).mp3
[download] 100% of 4.42MiB
=> 成功
... (38曲分繰り返し)
Picard タグ付けの場合
======================================================================
[6/9] Picard タグ付け (run_picard)
======================================================================
Picard を起動します...
I: Executing command: LOAD '.../music_download'
I: Executing command: LOOKUP
I: Executing command: FINGERPRINT
I: Executing command: SCAN
I: Executing command: QUIT
Picard が終了しました (所要時間=00:51)
Navidrome 配置の場合
======================================================================
[9/9] 残りを移動 (move_leftovers)
======================================================================
Navidrome フォルダ検出: C:\Users\...\navidrome_0.64.2_windows_amd64\music
[MOVE] The Beatles - Her Majesty (2019 Mix).mp3
技術的な学び
本プロジェクトを通じて得た、他プロジェクトにも応用できる技術的知見 を共有します。
1. 異なるツールを「パイプ」で繋ぐ設計思想
PowerShell + Python + CLI ツールという 異なる言語・実行環境 を連携させる際、以下の原則が有効でした:
| 原則 | 具体例 |
|---|---|
| 単一責任 | 各スクリプトは1つの処理だけを行う(DL・検査・タグ付け・移動) |
| データの受け渡しはファイル経由 | CSV・ログファイルを介して疎結合に |
| 終了コードで成否を判定 |
$LASTEXITCODE を親プロセスで捕捉 |
| 失敗時は次のステップに進まない | 検査で NG があれば再 DL → 再検査 |
2. 子プロセスのバッファリング問題
最大のハマりポイント はこれでした。
# これだと yt-dlp の出力がリアルタイムで届かない
proc = subprocess.Popen(args, stdout=subprocess.PIPE)
for line in proc.stdout:
print(line) # ← バッファに溜まって、DL 完了まで何も見えない
解決策: 出力を ログファイルにリダイレクト し、Python 側はそれを tail する方式に切り替えました。
# ログファイル経由でストリーミング
with open(LOG_FILE, "a") as log_fp:
proc = subprocess.Popen(args, stdout=log_fp, stderr=subprocess.STDOUT)
proc.wait()
# 別スレッドで LOG_FILE を tail
def tail_log_loop():
last_pos = 0
while not stop_event.is_set():
with open(LOG_FILE) as f:
f.seek(last_pos)
new_lines = f.readlines()
last_pos = f.tell()
time.sleep(0.5)
これは PowerShell 側の Write-Host が stdout に出ない 問題と合わせて、子プロセス連携の鉄則と言えます。
3. Picard の CLI モードという「隠れた宝」
実はこれ導入する前はまともに動いてませんでした
① QUIT が無くて終了しない
② # がコメントにならない
③ BOM で壊れる
④ sanitize が絶対パスを破壊
⑤ ハングしても止まらない
事件です
だからCLI モードを追加した
Picard は GUI アプリですが、実は 強力な CLI モード を持っています:
LOAD <path>
CLUSTER
LOOKUP # MusicBrainz で検索
FINGERPRINT # AcoustID で音声指紋
SCAN # 指紋からメタデータ取得
SAVE_MATCHED # マッチした曲をタグ付け・移動
REMOVE_SAVED # 処理済みをリストから除去
QUIT # これが無いと終了しない
ポイント:
-
#コメントは 解釈されない → コマンドファイルにはコマンドのみ - BOM なし UTF-8 で保存する必要がある
-
QUITが無いと GUI で待機したまま
4. ファイル名操作の落とし穴
「」 や ⧸ を「装飾」と決めつけて削除すると、曲名が消える 事故が起きました。
# 日本語タイトル "Mrs. green pear「abcd」" から
# 曲名 "abcd" が消える
'\s*「[^」]*」'
# 曲名本体の可能性がある記号は保護する
教訓: 正規表現で「装飾を除去」する際は、その記号が本当に装飾か を慎重に判断する。特に日本語の 「」 【】 は 曲名の一部 であることが多い。
5. 繰り返し実行可能な重要性
「同じコマンドを2回実行しても同じ結果」 を保証することで、ユーザーが気軽に再実行できます。
| 処理 | 冪等性を保つ仕組み |
|---|---|
| ダウンロード |
--download-archive で DL 済みをスキップ |
| タグ付け | 既にタグ付きのファイルは Picard が上書きしない |
| ファイル移動 | 同名ファイルがあれば (1) (2) で退避 |
| CSV 追記 |
make_todo.ps1 で既 DL 曲を抽出除外 |
6. 段階的なエラーハンドリング
| レベル | 処理 |
|---|---|
| リトライ | 同じクライアントで最大 N 回 |
| フォールバック | クライアントを変えて再試行(android → ios → web_safari) |
| スキップ | 全クライアント失敗時は失敗ログに記録して次へ |
| 全体停止 | 致命的エラー(CSV 不在など)のみ即終了 |
「1曲の失敗で全体が止まらない」 設計が、大量処理では決定的に重要でした。
7. Windows / Linux クロスプラットフォームの壁
現状は Windows 専用 ですが、以下の部分が移植の課題です:
| 機能 | Windows | Linux |
|---|---|---|
| PATH 永続化 | レジストリ |
~/.bashrc / ~/.profile
|
| ファイル名禁止文字 | \ / : * ? " < > | |
/ のみ |
| PowerShell 実行 | 標準 |
pwsh を別途インストール |
| パス区切り | \ |
/ |
Python 一本化 が理想的な解決策ですが、既存の PowerShell 資産を活かすなら Bash 移植 が現実的です(本記事末尾の「お願い」参照)。
8. Web UI と CLI の分離設計
Flask 側は「入力の受付と起動」だけ に徹し、実際の処理は run_all.ps1 に丸投げ する設計にしました。
# Flask 側: 追記して起動するだけ
append_songs(items)
threading.Thread(target=run_pipeline, args=(mode,), daemon=True).start()
これにより:
- CLI からも Web からも 同じ処理 を実行できる
- ロジックの重複がない
- Web UI が落ちても CLI は動く
まとめ
| ポイント | 内容 |
|---|---|
| 統合 | yt-dlp × Picard × Navidrome を PowerShell で繋ぎ、完全自動化 |
| プレイリスト | URL をそのまま貼るだけで全曲一括取り込み |
| Picard CLI | コマンドラインモードが強力。QUIT を忘れずに |
| ファイル名 |
「」 や ⧸ の扱いが難しい。元の情報を消さないよう注意 |
| Web UI | スマホから気軽に曲を追加できる |
| ジャケット |
--embed-thumbnail で最初から見た目が整う |
ソースコードは GitHub で公開しています。改善点や「こうした方がいいよ」があればぜひ教えてください。 🙏
GitHub:YoutubeDownload
参考
お願い:Linux / Docker 対応へのご協力
現在、このパイプラインは Windows 向けの PowerShell で記述されています。
Navidrome ユーザーの多くは Linux や Docker 環境 で運用しているため、Linux への移植や Docker コンテナ化にご協力いただける方がいれば、非常に嬉しく思います。
- Linux ネイティブ対応(Bash / Python 化)
- Docker コンテナ化(docker-compose 対応)
- macOS 対応
ぜひお気軽に GitHub での Pull Request や Issue の作成をお願いします!
免責事項
本記事・本ツールは「技術デモ」です
本記事で紹介したシステムは、複数のオープンソースツール(yt-dlp / ffmpeg / MusicBrainz Picard / Navidrome)を PowerShell + Python で連携させる自動化技術のデモンストレーション です。著作権侵害を助長・推奨・幇助する意図は一切ありません。
想定用途
本ツールの主な想定用途は以下です:
- 自分自身がアップロードした YouTube 動画 のダウンロード
- 明示的にダウンロード許可を得ている コンテンツ
- Creative Commons / パブリックドメイン など自由に DL 可能なコンテンツ
- 本記事で示した 自動化技術・PowerShell テクニックの学習
禁止事項
以下には 使用しないでください:
- 権利者の許諾なく著作権で保護された楽曲・動画をダウンロードする行為
- DRM・ペイウォール・アクセス制限の回避
- ダウンロードしたコンテンツの再配布
- YouTube 利用規約・その他適用法令への違反
デフォルト設定: 自分の動画のみ
本ツールは責任ある利用を徹底するため、デフォルトで「自分のチャンネルの動画のみ」ダウンロード可能 に設定しています。他のソースからダウンロードするには、明示的にオプトイン し、自身が適法な権利を有していることを確認してください。
法的注意
YouTube からのダウンロードは、国によって法的扱いが異なります。私的利用が認められている国もあれば、厳格に禁止されている国もあります。自国の法律を理解し、遵守するのは利用者自身の責任 です。
本記事の著者は、本ツールの使用によって生じたいかなる損害・法的結果についても 一切の責任を負いません。
推奨される合法的な代替手段
音楽ライブラリを 合法的に 構築したい場合は、以下を検討してください:
- Bandcamp — アーティストへの直接支援
- Jamendo — Creative Commons 楽曲
- Free Music Archive — PD / CC 楽曲
- Internet Archive — パブリックドメイン音源
- 正規購入 — CD / デジタル配信
「技術デモ」としての本質:本記事の主眼は「複数のツールを連携させて自動化パイプラインを組む技術」の共有です。ダウンロード対象そのものではなく、PowerShell でのプロセス連携・エラーハンドリング・CLI ツールの組み合わせ方 に価値を見出していただければ幸いです。