プロジェクト

全般

プロフィール

機能 #690

完了
NO NO

Navidrome からのプレイリストエクスポートと WALKMAN 用自動編集

機能 #690: Navidrome からのプレイリストエクスポートと WALKMAN 用自動編集

nop_thread さんが1年以上前に追加. 約1ヶ月前に更新.

ステータス:
終了
優先度:
通常
担当者:
開始日:
2025/07/14
期日:
進捗率:

100%

一時中断:
いいえ
pinned:
いいえ
リマインド予定日:
前回確認日:
2026/06/07
管理外残件あり:

関連するチケット 1 (0件未完了1件完了)

関連している 鯖缶 - 機能 #108: 音楽の管理方式の改善終了nop_thread

操作

NO nop_thread さんが12ヶ月前に更新 操作 #1

  • 前回確認日2025/07/13 にセット

Release v0.56.0 · navidrome/navidrome

"M3U File Downloads & Unified Export Logic" が最近のバージョンで実装されたようなので、これを使えば済むかもしれない。
要確認。

NO nop_thread さんが12ヶ月前に更新 操作 #2

  • ステータス新規 から 進行中 に変更
  • 開始日2025/07/14 にセット
  • 前回確認日2025/07/13 から 2025/07/14 に変更

Subsonic API を使うなら ND_SUBSONIC_DEFAULTREPORTREALPATH=true したうえで getPlaylists.viewgetPlaylist.view から情報を持ってこられそう。

NO nop_thread さんが12ヶ月前に更新 操作 #3

native API についてはドキュメントがなさそう?
/api/playlist/${id}/tracks から持ってこられそうな雰囲気があるが、認証をどうすればいいか不明。
プレイリストの ID は Subsonic API の getPlaylists から持ってこられるやつだと思う (未確認)。

Documentation for the Navidrome API : r/navidrome を見た感じだと native API は意図して消極的公開になっているようなので、 Subsonic API を使うべきなのだろう。たぶん。

一応 /auth/login からトークンを取得して X-ND-AuthorizationBearer ${token} するといけそうだが、そこまでして得られるものがせいぜい「サーバの設定を変更しなくても動く」くらいで、むしろ失効期限の存在する bearer token など管理すべき状態が増えてしまうので、あまり嬉しくない。

NO nop_thread さんが12ヶ月前に更新 操作 #4

nop_thread さんは #note-2 で書きました:

Subsonic API を使うなら ND_SUBSONIC_DEFAULTREPORTREALPATH=true したうえで

nop_thread さんは #note-3 で書きました:

そこまでして得られるものがせいぜい「サーバの設定を変更しなくても動く」くらいで、

どうもサーバでの設定は新規 Subsonic API クライアントへのデフォルト値でしかないようで、 web UI の右上のメニューから「Players」→(クライアントを選択)→「Report Real Path」を有効化、と設定することで実際のファイルパスを取得できるようになった。

NO nop_thread さんが12ヶ月前に更新 · 編集済み 操作 #5

たぶんいけた。

旧版

curl で --insecure しているのは単に LAN 用 root CA の証明書をシステムにインストールするのをサボっているせい。
気が向いたらそのうちやる。

あとは Navidrome 上で削除されたプレイリストをローカルからも消す処理を入れたい。
雑にやるなら rm -r ${PLAYLIST_DEST_DIR}/* などすればよさそう。
効率化とか差分更新はプレイリスト数が増えすぎて待てなくなってから考えればいいか。

NO nop_thread さんが12ヶ月前に更新 · 編集済み 操作 #6

  • ステータス進行中 から 終了 に変更
  • 進捗率0 から 100 に変更

nop_thread さんは #note-5 で書きました:

あとは Navidrome 上で削除されたプレイリストをローカルからも消す処理を入れたい。
雑にやるなら rm -r ${PLAYLIST_DEST_DIR}/* などすればよさそう。

旧版

まあ良いでしょう。

NO nop_thread さんが約1ヶ月前に更新 操作 #7

  • 関連している 機能 #108: 音楽の管理方式の改善 を追加

NO nop_thread さんが約1ヶ月前に更新 操作 #8

内容に変化がないときファイルを上書きしないようにした:

#!/bin/sh
set -eu

# last update: 2025-08-31

cd "$(dirname "$(readlink -f "$0")")/.."

USER=nop_thread
PASSWORD_SALT='REPLACE_WITH_RANDOM_PASSWORD_SALT'
# `echo -n "${PASSWORD}${SALT}" | md5sum - | cut -d' ' -f1`.
HASHED_PASSWORD='REPLACE_WITH_MD5_OF_PASSWORD_AND_SALT'
# Should NOT have a trailing slash.
BASE_URL='https://my.navidrome.host.example'
API_VERSION='1.16.1'
FORMAT=json
# Don't forget to update the config from the web UI.
# (Menu) > "Players" > $CLIENT_STRING > Enable "Report Real Path".
CLIENT_STRING='REPLACE_WITH_YOUR_CLIENT_NAME'

PLAYLIST_DEST_DIR="playlists/navidrome"
# This temp dir should not be inside `$PLAYLIST_DEST_DIR` since they will be
# sync'ed using rsync.
PLAYLIST_DEST_DIR_TEMP="playlists/navidrome.temp"

get() {
	ENDPOINT="$1"
	PARAMS="${2:+&$2}"
	curl --silent --insecure "${BASE_URL}/rest/${ENDPOINT}?u=${USER}&s=${PASSWORD_SALT}&t=${HASHED_PASSWORD}&v=${API_VERSION}&f=${FORMAT}&c=${CLIENT_STRING}${PARAMS}"
}

get_playlists() {
	get getPlaylists.view | jq --raw-output '."subsonic-response".playlists.playlist[] | [.id, .name] | @tsv'
}

rm -rf "$PLAYLIST_DEST_DIR_TEMP"
mkdir -p "$PLAYLIST_DEST_DIR_TEMP"

# Create `.ndignore` file (or use the existing one).
if [ -f "${PLAYLIST_DEST_DIR}/.ndignore" ] ; then
	cp --reflink=always --preserve "${PLAYLIST_DEST_DIR}/.ndignore" "${PLAYLIST_DEST_DIR_TEMP}/.ndignore"
else
	{
		echo '# Files in this directory will be ignored by Navidrome.'
		echo '# See <https://github.com/navidrome/navidrome/issues/297>.'
	} >"${PLAYLIST_DEST_DIR_TEMP}/.ndignore"
fi

get_playlists | while read id name ; do
	SAFE_NAME="$(echo "$name" | tr '/<>:"/\|?*' '[_*]')"
	OLD_FILE="${PLAYLIST_DEST_DIR}/${SAFE_NAME}.m3u8"
	DEST_FILE="${PLAYLIST_DEST_DIR_TEMP}/${SAFE_NAME}.m3u8"
	DEST_FILE_TEMP="${PLAYLIST_DEST_DIR_TEMP}/${SAFE_NAME}.m3u8.tmp"
	DEST_REL_PATH_TO_MUSIC_ROOT="../.."

	# Generate the playlist content and save as a temporary file.
	echo '#EXTM3U' >"${DEST_FILE_TEMP}"
	echo "#PLAYLIST:${name} (from Navidrome)" >>"${DEST_FILE_TEMP}"
	RAW_JSON="$(get getPlaylist.view "id=${id}")"
	ATTRS="$(echo "$RAW_JSON" | jq --raw-output '."subsonic-response".playlist.comment | select(.!=null)' | sed -ne 's/^ATTR://p')"
	if echo "$ATTRS" | grep -q '^\(sortable\|unordered\)$' ; then
		#echo "Playlist $name is unordered."
		ENTRIES_JSON="$(echo "$RAW_JSON" | jq --raw-output '."subsonic-response".playlist.entry // [] | sort_by(.path)')"
	else
		#echo "Playlist $name is ordered."
		ENTRIES_JSON="$(echo "$RAW_JSON" | jq --raw-output '."subsonic-response".playlist.entry')"
	fi
	echo "$ENTRIES_JSON" | jq --raw-output '.[] | "#EXTINF:\(.duration|tostring),\(.displayArtist) - \(.title)\n\(.path)"' | sed -e '/^#/! s:/mnt/music/:'"${DEST_REL_PATH_TO_MUSIC_ROOT}/:" >>"${DEST_FILE_TEMP}"

	# Overwrite the destination playlist only when they really differ.
	if ! diff --brief "$OLD_FILE" "$DEST_FILE_TEMP" >/dev/null 2>&1 ; then
		# Different or a new file. Overwrite by the new content.
		mv "$DEST_FILE_TEMP" "$DEST_FILE"
	else
		# Unchanged. Use the original file as is.
		cp --reflink=always --preserve "$OLD_FILE" "$DEST_FILE"
		rm "$DEST_FILE_TEMP"
	fi
done

# Note that timestamps should not be sync'ed if the content is identical.
rsync -r --delete-after --info=name "${PLAYLIST_DEST_DIR_TEMP}/" "${PLAYLIST_DEST_DIR}/"
rm -r "${PLAYLIST_DEST_DIR_TEMP}"

NO nop_thread さんが約1ヶ月前に更新 操作 #9

  • 前回確認日2025/07/14 から 2026/06/07 に変更

他の形式にエクスポート: PDF Atom