在huggingface部署navidrome——打造自己的音乐平台

项目地址:

https://github.com/navidrome/navidrome

1.创建Dockerfile文件

在huggingface官网创建spaces

Dockerfile文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
FROM deluan/navidrome

RUN apk update && apk add --no-cache \
bash \
curl \
python3 \
py3-pip \
&& rm -rf /var/cache/apk/*

RUN python3 -m venv /venv
ENV PATH="/venv/bin:$PATH"
RUN pip install --no-cache-dir huggingface_hub

RUN mkdir -p /data/cache /music /config /.cache

RUN chown -R 1000:1000 /data /music /config /venv /.cache

COPY start.sh /start.sh
COPY backup.py /backup.py
COPY update_music.py /update_music.py
RUN chmod +x /start.sh

USER 1000

WORKDIR /app

EXPOSE 4533

ENTRYPOINT ["/bin/bash", "/start.sh"]

README.md末尾添加

1
app_port: 4533

2.创建start.sh文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
#!/bin/bash

# 设置默认值
MUSIC_DIR=${MUSIC_DIR:-/music}
DATASET_MUSIC_NAME=${DATASET_MUSIC_NAME:-"your-username/music-dataset"}
MUSIC_TOKEN=${MUSIC_TOKEN:-""}
BACKUP_DATASET_ID=${BACKUP_DATASET_ID:-"your-username/navidrome-backup"}
BACKUP_INTERVAL=${BACKUP_INTERVAL:-3600} # 默认1小时备份一次
MUSIC_UPDATE_INTERVAL=${MUSIC_UPDATE_INTERVAL:-3600} # 默认1小时更新一次音乐

echo "[INFO] Starting Navidrome setup"

# 确保目录存在并有正确权限
mkdir -p ${MUSIC_DIR}
mkdir -p /data/cache
mkdir -p /.cache
chmod -R 755 ${MUSIC_DIR}
chmod -R 755 /data
chmod -R 755 /.cache

# 激活Python虚拟环境
source /venv/bin/activate

# 恢复备份(如果存在)
if [ -n "$BACKUP_DATASET_ID" ] && [ -n "$MUSIC_TOKEN" ]; then
echo "[INFO] Attempting to restore from backup..."
python /backup.py download "$MUSIC_TOKEN" "$BACKUP_DATASET_ID" "/data"
fi

# 启动音乐更新进程
if [ -n "$DATASET_MUSIC_NAME" ] && [ -n "$MUSIC_TOKEN" ]; then
echo "[INFO] Starting music update process..."
python /update_music.py "$DATASET_MUSIC_NAME" "$MUSIC_TOKEN" "$MUSIC_DIR" "$MUSIC_UPDATE_INTERVAL" "false" &
MUSIC_UPDATE_PID=$!
echo "[INFO] Music update process started with PID: $MUSIC_UPDATE_PID"
else
echo "[WARNING] Music update disabled. Set DATASET_MUSIC_NAME and MUSIC_TOKEN to enable."
fi

# 检查音乐目录
echo "[INFO] Checking music directory..."
ls -la ${MUSIC_DIR}

# 备份函数
backup_data() {
while true; do
echo "[INFO] Starting backup process $(date)"

if [ -d "/data" ]; then
echo "[INFO] Backing up Navidrome data to HuggingFace..."
python /backup.py upload "$MUSIC_TOKEN" "$BACKUP_DATASET_ID" "/data"
else
echo "[WARNING] Data directory does not exist, skipping backup..."
fi

echo "[INFO] Next backup in ${BACKUP_INTERVAL} seconds..."
sleep $BACKUP_INTERVAL
done
}

# 如果设置了备份数据集ID,则启动备份进程
if [ -n "$BACKUP_DATASET_ID" ] && [ -n "$MUSIC_TOKEN" ]; then
echo "[INFO] Starting backup process with dataset: ${BACKUP_DATASET_ID}"
backup_data &
BACKUP_PID=$!
echo "[INFO] Backup process started with PID: $BACKUP_PID"
else
echo "[WARNING] Backup disabled. Set BACKUP_DATASET_ID and MUSIC_TOKEN to enable."
fi

# 找到navidrome可执行文件的路径
NAVIDROME_PATH=$(which navidrome || find /app -name navidrome -type f 2>/dev/null | head -1)

if [ -z "$NAVIDROME_PATH" ]; then
echo "[ERROR] Could not find navidrome executable"
exit 1
else
echo "[INFO] Found navidrome at: $NAVIDROME_PATH"
# 启动Navidrome
echo "[INFO] Starting Navidrome..."
exec $NAVIDROME_PATH
fi

3.创建backup.py文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
#!/usr/bin/env python3
from huggingface_hub import HfApi
import sys
import os
import shutil
import time
import glob
from datetime import datetime
import tempfile

def manage_backups(api, repo_id, max_files=10):
"""管理备份文件数量,删除旧的备份"""
try:
files = api.list_repo_files(repo_id=repo_id, repo_type="dataset")
backup_files = [f for f in files if f.startswith('navidrome_backup_') and f.endswith('.tar.gz')]
backup_files.sort()

if len(backup_files) >= max_files:
files_to_delete = backup_files[:(len(backup_files) - max_files + 1)]
for file_to_delete in files_to_delete:
try:
api.delete_file(path_in_repo=file_to_delete, repo_id=repo_id, repo_type="dataset")
print(f'已删除旧备份: {file_to_delete}')
except Exception as e:
print(f'删除 {file_to_delete} 时出错: {str(e)}')
except Exception as e:
print(f'管理备份时出错: {str(e)}')

def upload_backup(file_path, file_name, token, repo_id):
"""上传备份文件到HuggingFace"""
api = HfApi(token=token)
try:
api.upload_file(
path_or_fileobj=file_path,
path_in_repo=file_name,
repo_id=repo_id,
repo_type="dataset"
)
print(f"成功上传 {file_name}")

manage_backups(api, repo_id)
except Exception as e:
print(f"文件上传出错: {str(e)}")

def download_latest_backup(token, repo_id, data_dir):
"""下载最新的备份文件并恢复"""
try:
# 确保目标目录存在并有正确权限
os.makedirs(data_dir, exist_ok=True)
os.chmod(data_dir, 0o755)

api = HfApi(token=token)
files = api.list_repo_files(repo_id=repo_id, repo_type="dataset")
backup_files = [f for f in files if f.startswith('navidrome_backup_') and f.endswith('.tar.gz')]

if not backup_files:
print("未找到备份文件")
return False

latest_backup = sorted(backup_files)[-1]

# 使用临时目录下载文件
with tempfile.TemporaryDirectory() as temp_dir:
filepath = api.hf_hub_download(
repo_id=repo_id,
filename=latest_backup,
repo_type="dataset",
local_dir=temp_dir
)

if filepath and os.path.exists(filepath):
# 解压备份文件
import tarfile
with tarfile.open(filepath, "r:gz") as tar:
tar.extractall(path=data_dir)

print(f"成功从 {latest_backup} 恢复备份到 {data_dir}")
return True
return False

except Exception as e:
print(f"下载备份时出错: {str(e)}")
return False

def create_backup(data_dir, exclude_dirs=None):
"""创建备份文件,排除指定目录"""
if exclude_dirs is None:
exclude_dirs = []

timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
backup_filename = f"navidrome_backup_{timestamp}.tar.gz"
backup_path = f"/tmp/{backup_filename}"

try:
import tarfile
with tarfile.open(backup_path, "w:gz") as tar:
for item in os.listdir(data_dir):
item_path = os.path.join(data_dir, item)
# 排除指定目录
if os.path.isdir(item_path) and item in exclude_dirs:
continue
tar.add(item_path, arcname=item)

print(f"备份文件创建成功: {backup_path}")
return backup_path, backup_filename
except Exception as e:
print(f"创建备份时出错: {str(e)}")
return None, None

if __name__ == "__main__":
action = sys.argv[1]
token = sys.argv[2]
repo_id = sys.argv[3]
data_dir = sys.argv[4]

if action == "upload":
# 排除音乐目录和缓存,只备份数据和配置
exclude_dirs = ["cache", "music"]
backup_path, backup_filename = create_backup(data_dir, exclude_dirs)
if backup_path:
upload_backup(backup_path, backup_filename, token, repo_id)
# 清理临时文件
os.remove(backup_path)
elif action == "download":
download_latest_backup(token, repo_id, data_dir)

4.创建 update_music.py文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
#!/usr/bin/env python3
from huggingface_hub import snapshot_download, HfApi
import os
import sys
import time
import json
import hashlib
from datetime import datetime

def get_dataset_info(repo_id, token):
"""获取数据集的最新信息,用于检测更新"""
try:
api = HfApi(token=token)
info = api.repo_info(repo_id=repo_id, repo_type="dataset")
return {
"sha": info.sha,
"last_modified": info.last_modified.isoformat() if info.last_modified else None
}
except Exception as e:
print(f"获取数据集信息出错: {str(e)}")
return None

def save_dataset_info(info, music_dir):
"""保存数据集信息到本地文件"""
info_file = os.path.join(music_dir, ".dataset_info.json")
try:
with open(info_file, "w") as f:
json.dump(info, f)
except Exception as e:
print(f"保存数据集信息出错: {str(e)}")

def load_dataset_info(music_dir):
"""从本地文件加载数据集信息"""
info_file = os.path.join(music_dir, ".dataset_info.json")
if not os.path.exists(info_file):
return None

try:
with open(info_file, "r") as f:
return json.load(f)
except Exception as e:
print(f"加载数据集信息出错: {str(e)}")
return None

def update_music(dataset_name, token, music_dir, force=False):
"""更新音乐文件,只在有变化时更新"""
print(f"[{datetime.now()}] 检查音乐数据集更新...")

# 获取远程数据集信息
remote_info = get_dataset_info(dataset_name, token)
if not remote_info:
print("无法获取远程数据集信息,跳过更新")
return False

# 获取本地数据集信息
local_info = load_dataset_info(music_dir)

# 检查是否需要更新
if not force and local_info and local_info.get("sha") == remote_info.get("sha"):
print("音乐数据集没有变化,无需更新")
return False

print(f"检测到音乐数据集有更新,开始下载...")

try:
# 下载数据集
snapshot_download(
repo_id=dataset_name,
repo_type="dataset",
local_dir=music_dir,
token=token
)

# 保存新的数据集信息
save_dataset_info(remote_info, music_dir)

print(f"[{datetime.now()}] 音乐数据集更新成功!")
return True
except Exception as e:
print(f"更新音乐数据集出错: {str(e)}")
return False

if __name__ == "__main__":
# 命令行参数: dataset_name token music_dir [interval] [force]
dataset_name = sys.argv[1]
token = sys.argv[2]
music_dir = sys.argv[3]

# 可选参数
interval = int(sys.argv[4]) if len(sys.argv) > 4 else 3600 # 默认1小时
force = sys.argv[5].lower() == "true" if len(sys.argv) > 5 else False

# 第一次运行时强制更新
update_music(dataset_name, token, music_dir, force=True)

# 定期检查更新
while True:
time.sleep(interval)
update_music(dataset_name, token, music_dir, force=force)

5.创建两个dataset数据库

一个用于备份,一个用于拉取音乐

记住你的数据库地址,格式:hugging名称/dataset名称

注:音乐需要你自己上传至dataset空间(可以使用我的:xjf666/music)

6.创建HF-token

完成后保存并记住你的token

7.在后台添加环境变量

1
2
3
4
5
6
MUSIC_DIR  #音乐文件存储的本地目录,默认为"/music"
DATASET_MUSIC_NAME #Hugging Face上的音乐数据集,格式:格式用户名/数据集名称(可以用我的:xjf666/music)
MUSIC_TOKEN #你的token
BACKUP_DATASET_ID # 用于存储备份的数据集,格式:用户名/数据集名称
BACKUP_INTERVAL # 备份同步时间(秒钟),默认3600秒
MUSIC_UPDATE_INTERVAL #音乐库同步时间(秒钟),默认3600秒

推荐添加额外变量

1
2
3
4
5
ND_LASTFM_APIKEY
ND_LASTFM_SECRET
ND_SPOTIFY_ID
ND_SPOTIFY_SECRET
ND_ENABLESHARING #true 分享音乐

参考文档:
https://www.navidrome.org/docs/usage/configuration-options/

余环境变量可以参考项目:
https://www.navidrome.org/docs/usage/configuration-options/

在cloudflare进行反代(可选)

1
2
3
4
5
6
7
export default {
async fetch(request, env) {
const url = new URL(request.url);
url.host = '你的地址';
return fetch(new Request(url, request))
}
}