YvYang的AI摘要
Spark-Lite

前言

最近下载了很多音乐,上传到网盘后就想着能不能引用到网站上,正好网盘支持WebDav挂载,没有服务器就只能使用CouldFlare Workers来实现。

教程

有两种方案:有网页UI和无UI,前者方便在线播放,后者访问速度更佳,读者可以视需求选择

有网页UI版本

创建cloudfalre worker

登录CloudFlare,创建Workers,选择HelloWorld模版
覆盖式写入以下代码:

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
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
export default {
async fetch(request, env, ctx) {
const url = new URL(request.url);
const path = url.pathname;

const allowedOrigins = env.ALLOWED_ORIGINS ? env.ALLOWED_ORIGINS.split(',').map(s => s.trim()) : ['*'];
const debugMode = env.DEBUG_MODE === 'true';

// 1. 根路径显示测试页面
if (path === "/" || path === "/test") {
const origin = request.headers.get('Origin') || '';
if (origin && !isOriginAllowed(origin, allowedOrigins) && !allowedOrigins.includes('*')) {
return new Response("Forbidden: Origin not allowed", { status: 403 });
}
return new Response(getTestPage(), {
headers: {
"Content-Type": "text/html; charset=utf-8",
...getCORSHeaders(origin, allowedOrigins)
},
});
}

// 2. 调试接口
if (path === "/api/debug") {
return await debugInfo(env, url, request);
}

// 3. 测试 WebDAV 连接
if (path === "/api/test-connection") {
const origin = request.headers.get('Origin') || '';
if (origin && !isOriginAllowed(origin, allowedOrigins) && !allowedOrigins.includes('*')) {
return new Response("Forbidden", { status: 403 });
}
return await testConnection(env, origin, allowedOrigins);
}

// 4. 测试文件
if (path === "/api/test-file") {
const origin = request.headers.get('Origin') || '';
if (origin && !isOriginAllowed(origin, allowedOrigins) && !allowedOrigins.includes('*')) {
return new Response("Forbidden", { status: 403 });
}
const filePath = url.searchParams.get("file") || "/test.mp3";
return await testFile(request, env, filePath, origin, allowedOrigins, debugMode);
}

// 5. 获取文件列表
if (path === "/api/list") {
const origin = request.headers.get('Origin') || '';
if (origin && !isOriginAllowed(origin, allowedOrigins) && !allowedOrigins.includes('*')) {
return new Response("Forbidden", { status: 403 });
}
const dirPath = url.searchParams.get("path") || "/";
return await getFileList(env, dirPath, origin, allowedOrigins);
}

// 6. OPTIONS 预检
if (request.method === "OPTIONS") {
const origin = request.headers.get('Origin') || '';
return new Response(null, {
headers: {
...getCORSHeaders(origin, allowedOrigins),
"Access-Control-Allow-Methods": "GET, HEAD, OPTIONS, PROPFIND",
"Access-Control-Allow-Headers": "Content-Type, Range, Authorization, Depth",
"Access-Control-Max-Age": "86400",
},
});
}

// 7. 只允许 GET 和 HEAD
if (request.method !== "GET" && request.method !== "HEAD") {
return new Response("Method not allowed", { status: 405 });
}

// 8. 代理音乐请求
const origin = request.headers.get('Origin') || '';
if (origin && !isOriginAllowed(origin, allowedOrigins) && !allowedOrigins.includes('*')) {
return new Response("Forbidden: Origin not allowed", { status: 403 });
}
return await proxyRequest(request, env, path, origin, allowedOrigins, debugMode);
},
};

// ============== 辅助函数 ==============

function isOriginAllowed(origin, allowedOrigins) {
if (!origin) return true;
if (allowedOrigins.includes('*')) return true;
return allowedOrigins.some(allowed => {
const allowedTrimmed = allowed.trim().toLowerCase();
const originLower = origin.toLowerCase();
return originLower === allowedTrimmed ||
originLower.endsWith('.' + allowedTrimmed.replace(/^https?:\/\//, ''));
});
}

function getCORSHeaders(origin, allowedOrigins) {
if (allowedOrigins.includes('*')) {
return {
"Access-Control-Allow-Origin": "*",
"Access-Control-Expose-Headers": "Content-Range, Content-Length, Accept-Ranges, X-Debug-Info",
"Vary": "Origin",
};
}
if (origin && isOriginAllowed(origin, allowedOrigins)) {
return {
"Access-Control-Allow-Origin": origin,
"Access-Control-Expose-Headers": "Content-Range, Content-Length, Accept-Ranges, X-Debug-Info",
"Vary": "Origin",
};
}
return {};
}

// ============== 关键:路径规范化函数 ==============
/**
* 规范化请求路径,避免与 WEBDAV_URL 的路径前缀重复
* 例如:
* - WEBDAV_URL = https://pan.xxbyq.net/dav
* - 请求路径 = /dav/音乐/歌.mp3
* - 返回 = /音乐/歌.mp3 (移除重复的 /dav)
*/
function normalizePath(requestPath, webdavUrl) {
try {
const urlObj = new URL(webdavUrl);
const webdavPath = urlObj.pathname.replace(/\/$/, ""); // 移除末尾 /

// 如果 WEBDAV_URL 有路径前缀(如 /dav),且请求路径也以它开头,则移除
if (webdavPath && webdavPath !== '/' && requestPath.startsWith(webdavPath + '/')) {
return requestPath.substring(webdavPath.length);
}
return requestPath;
} catch (e) {
return requestPath;
}
}

// ============== 调试接口 ==============
async function debugInfo(env, url, request) {
const file = url.searchParams.get('file') || url.pathname;
const webdavBase = env.WEBDAV_URL ? env.WEBDAV_URL.replace(/\/$/, "") : '';
const normalizedPath = normalizePath(file, env.WEBDAV_URL);
const computedTargetUrl = webdavBase + normalizedPath;

const debug = {
requestUrl: url.href,
requestPath: url.pathname,
webdavUrl: env.WEBDAV_URL,
originalFilePath: file,
normalizedFilePath: normalizedPath,
computedTargetUrl: computedTargetUrl,
timestamp: new Date().toISOString(),
};

return new Response(JSON.stringify(debug, null, 2), {
headers: { "Content-Type": "application/json" },
});
}

// ============== 测试页面 HTML ==============
function getTestPage() {
return `
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Worker 调试面板</title>
<style>
body { font-family: monospace; max-width: 900px; margin: 20px auto; padding: 20px; background: #1a1a2e; color: #eee; }
.card { background: #16213e; padding: 20px; margin: 15px 0; border-radius: 8px; border: 1px solid #0f3460; }
h2 { color: #e94560; margin-top: 0; }
.btn { background: #e94560; color: white; border: none; padding: 10px 20px; border-radius: 4px; cursor: pointer; margin: 5px; }
.btn:hover { background: #c73e54; }
pre { background: #0f0f23; padding: 15px; border-radius: 4px; overflow-x: auto; font-size: 12px; max-height: 400px; }
.success { color: #00d9a5; }
.error { color: #ff6b6b; }
.warn { color: #ffc107; }
input { padding: 8px; background: #0f0f23; border: 1px solid #0f3460; color: #eee; border-radius: 4px; width: 400px; }
</style>
</head>
<body>
<h1>🔧 Worker 路径调试面板</h1>

<div class="card">
<h2>🔍 路径调试</h2>
<p>输入文件路径,查看 Worker 实际请求的 WebDAV URL:</p>
<input type="text" id="testPath" placeholder="/dav/xxx.mp3" value="/dav/きらめく湖畔 - 加藤達也.mp3">
<button class="btn" onclick="debugPath()">🔎 查看实际请求 URL</button>
<pre id="debugResult">点击按钮查看结果...</pre>
</div>

<div class="card">
<h2>📁 文件列表</h2>
<input type="text" id="listPath" placeholder="目录路径" value="/">
<button class="btn" onclick="loadList()">📂 加载列表</button>
<pre id="listResult">-</pre>
</div>

<div class="card">
<h2>🎵 播放测试</h2>
<input type="text" id="playPath" placeholder="文件路径" value="/dav/きらめく湖畔 - 加藤達也.mp3">
<button class="btn" onclick="testPlay()">▶️ 测试播放</button>
<div id="playResult"></div>
<audio controls id="audio" style="width:100%;margin-top:10px;display:none;"></audio>
</div>

<script>
const workerUrl = window.location.origin;

async function debugPath() {
const path = document.getElementById('testPath').value;
const result = document.getElementById('debugResult');
result.textContent = '请求中...';
try {
const res = await fetch(workerUrl + '/api/debug?file=' + encodeURIComponent(path));
const data = await res.json();
result.textContent = JSON.stringify(data, null, 2);
} catch (e) { result.textContent = '❌ 错误:' + e.message; }
}

async function loadList() {
const path = document.getElementById('listPath').value;
const result = document.getElementById('listResult');
result.textContent = '加载中...';
try {
const res = await fetch(workerUrl + '/api/list?path=' + encodeURIComponent(path));
const data = await res.json();
result.textContent = JSON.stringify(data, null, 2);
} catch (e) { result.textContent = '❌ 错误:' + e.message; }
}

async function testPlay() {
const path = document.getElementById('playPath').value;
const result = document.getElementById('playResult');
const audio = document.getElementById('audio');
result.innerHTML = '<span class="warn">测试中...</span>';
audio.style.display = 'none';
try {
const res = await fetch(workerUrl + path, { method: 'HEAD' });
if (res.status === 200 || res.status === 206) {
result.innerHTML = '<span class="success">✅ 文件可访问!状态码:' + res.status + '</span>';
audio.src = workerUrl + path;
audio.style.display = 'block';
} else {
result.innerHTML = '<span class="error">❌ 状态码:' + res.status + '</span>';
}
} catch (e) { result.innerHTML = '<span class="error">❌ 请求失败:' + e.message + '</span>'; }
}
window.onload = debugPath;
</script>
</body>
</html>
`;
}

// ============== 测试 WebDAV 连接 ==============
async function testConnection(env, origin, allowedOrigins) {
const result = { timestamp: new Date().toISOString(), envStatus: false, connectionStatus: false, error: null };
if (!env.WEBDAV_URL || !env.WEBDAV_USER || !env.WEBDAV_PASS) {
result.error = "环境变量未配置完整";
return new Response(JSON.stringify(result), { headers: { "Content-Type": "application/json", ...getCORSHeaders(origin, allowedOrigins) } });
}
result.envStatus = true;
try {
const auth = btoa(`${env.WEBDAV_USER}:${env.WEBDAV_PASS}`);
const res = await fetch(env.WEBDAV_URL, { method: "PROPFIND", headers: { Authorization: `Basic ${auth}`, Depth: "0" } });
result.connectionStatus = res.status === 207 || res.status === 200;
result.httpStatus = res.status;
if (!result.connectionStatus) result.error = `HTTP ${res.status}`;
} catch (e) { result.error = e.message; }
return new Response(JSON.stringify(result), { headers: { "Content-Type": "application/json", ...getCORSHeaders(origin, allowedOrigins) } });
}

// ============== 测试文件访问 ==============
async function testFile(request, env, filePath, origin, allowedOrigins, debugMode) {
const result = { filePath, status: 0, headers: {}, supportsRange: false, targetUrl: '' };
try {
const webdavBase = env.WEBDAV_URL.replace(/\/$/, "");
const normalizedPath = normalizePath(filePath, env.WEBDAV_URL); // ✅ 关键:规范化路径
const targetUrl = webdavBase + normalizedPath;
result.targetUrl = targetUrl;
const auth = btoa(`${env.WEBDAV_USER}:${env.WEBDAV_PASS}`);
const res = await fetch(targetUrl, { method: "HEAD", headers: { Authorization: `Basic ${auth}`, Range: "bytes=0-1" } });
result.status = res.status;
result.supportsRange = res.status === 206;
res.headers.forEach((v, k) => { if (["content-type", "content-length", "content-range", "accept-ranges"].includes(k.toLowerCase())) result.headers[k] = v; });
if (debugMode) result.debug = { targetUrl, normalizedPath };
} catch (e) { result.error = e.message; }
return new Response(JSON.stringify(result), { headers: { "Content-Type": "application/json", ...getCORSHeaders(origin, allowedOrigins) } });
}

// ============== 获取文件列表 ==============
async function getFileList(env, dirPath, origin, allowedOrigins) {
const result = { path: dirPath, files: [], error: null };
if (!env.WEBDAV_URL || !env.WEBDAV_USER || !env.WEBDAV_PASS) {
result.error = "环境变量未配置";
return new Response(JSON.stringify(result), { status: 500, headers: { "Content-Type": "application/json", ...getCORSHeaders(origin, allowedOrigins) } });
}
try {
const webdavBase = env.WEBDAV_URL.replace(/\/$/, "");
const normalizedDir = normalizePath(dirPath, env.WEBDAV_URL); // ✅ 规范化目录路径
const targetUrl = webdavBase + normalizedDir;
const auth = btoa(`${env.WEBDAV_USER}:${env.WEBDAV_PASS}`);
const propfindBody = `<?xml version="1.0" encoding="utf-8"?><D:propfind xmlns:D="DAV:"><D:prop><D:displayname/><D:getcontentlength/><D:getcontenttype/><D:resourcetype/><D:getlastmodified/></D:prop></D:propfind>`;
const res = await fetch(targetUrl, { method: "PROPFIND", headers: { Authorization: `Basic ${auth}`, Depth: "1", "Content-Type": "application/xml" }, body: propfindBody });
if (res.status !== 207) { result.error = `WebDAV 返回状态码:${res.status}`; result.targetUrl = targetUrl; return new Response(JSON.stringify(result), { status: res.status, headers: { "Content-Type": "application/json", ...getCORSHeaders(origin, allowedOrigins) } }); }
const xmlText = await res.text();
const files = parseWebDAVXML(xmlText, dirPath, env.WEBDAV_URL);
result.files = files.filter(f => f.path !== dirPath);
} catch (e) { result.error = e.message; }
return new Response(JSON.stringify(result), { headers: { "Content-Type": "application/json", ...getCORSHeaders(origin, allowedOrigins) } });
}

// ============== 解析 WebDAV XML ==============
function parseWebDAVXML(xmlText, requestPath, webdavUrl) {
const files = [];
const responseRegex = /<D:response[^>]*>([\s\S]*?)<\/D:response>/gi;
let responseMatch;
while ((responseMatch = responseRegex.exec(xmlText)) !== null) {
const responseBlock = responseMatch[1];
const hrefMatch = responseBlock.match(/<D:href[^>]*>([^<]+)<\/D:href>/i);
if (!hrefMatch || !hrefMatch[1]) continue;
let href = decodeURIComponent(hrefMatch[1].trim());
try { const urlObj = new URL(href, 'http://localhost'); href = urlObj.pathname; } catch (e) {}
// ✅ 规范化路径:移除 WEBDAV_URL 中的路径前缀
try {
const urlObj = new URL(webdavUrl);
const webdavPath = urlObj.pathname.replace(/\/$/, "");
if (webdavPath && webdavPath !== '/' && href.startsWith(webdavPath + '/')) {
href = href.substring(webdavPath.length);
}
} catch (e) {}
if (!href.startsWith('/')) href = '/' + href;
const displayNameMatch = responseBlock.match(/<D:displayname[^>]*>([^<]*)<\/D:displayname>/i);
let displayName = displayNameMatch ? displayNameMatch[1].trim() : href.split('/').filter(p => p).pop() || '/';
const isCollection = /<D:collection[^>]*\/>|<D:collection[^>]*><\/D:collection>/i.test(responseBlock);
const contentLengthMatch = responseBlock.match(/<D:getcontentlength[^>]*>([^<]+)<\/D:getcontentlength>/i);
const size = contentLengthMatch ? parseInt(contentLengthMatch[1]) : null;
const lastModifiedMatch = responseBlock.match(/<D:getlastmodified[^>]*>([^<]+)<\/D:getlastmodified>/i);
const modified = lastModifiedMatch ? lastModifiedMatch[1].trim() : null;
const contentTypeMatch = responseBlock.match(/<D:getcontenttype[^>]*>([^<]+)<\/D:getcontenttype>/i);
const contentType = contentTypeMatch ? contentTypeMatch[1].trim() : null;
files.push({ name: displayName, path: href, type: isCollection ? 'directory' : 'file', size, modified, contentType });
}
return files;
}

// ============== 代理音乐请求 ==============
async function proxyRequest(request, env, path, origin, allowedOrigins, debugMode) {
if (!env.WEBDAV_URL || !env.WEBDAV_USER || !env.WEBDAV_PASS) return new Response("环境变量未配置", { status: 500 });
const webdavBase = env.WEBDAV_URL.replace(/\/$/, "");
const normalizedPath = normalizePath(path, env.WEBDAV_URL); // ✅ 关键:规范化路径
const targetUrl = webdavBase + normalizedPath;
const headers = new Headers();
const auth = btoa(`${env.WEBDAV_USER}:${env.WEBDAV_PASS}`);
headers.set("Authorization", `Basic ${auth}`);
if (request.headers.has("Range")) headers.set("Range", request.headers.get("Range"));
if (request.headers.has("If-Range")) headers.set("If-Range", request.headers.get("If-Range"));
try {
const response = await fetch(targetUrl, { method: request.method, headers });
const newHeaders = new Headers(response.headers);
Object.assign(newHeaders, getCORSHeaders(origin, allowedOrigins));
if (debugMode) newHeaders.set("X-Debug-Target-Url", targetUrl);
return new Response(response.body, { status: response.status, statusText: response.statusText, headers: newHeaders });
} catch (e) {
return new Response("Proxy Error: " + e.message, { status: 502, headers: { "X-Debug-Target-Url": targetUrl } });
}
}

配置境变量

在Worker设置中,添加以下环境变量:

  • ALLOWED_ORIGINS: 跨域设置
  • WEBDAY_PASS: 密码
  • WEBDAV_URL: 连接地址
  • WEBDAY _USER: 账号

无UI版本

编辑代码

在worker中覆盖写入一下内容

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
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
// Cloudflare Worker代码 - webdav-proxy.js
// 保存为index.js并部署到Cloudflare Workers

// WebDAV服务器配置(通过环境变量设置)
// 需要在Workers设置中添加以下环境变量:
// WEBDAV_URL: 您的WebDAV服务器地址(如https://example.com/dav)
// WEBDAV_USERNAME: WebDAV用户名
// WEBDAV_PASSWORD: WebDAV密码

// 允许的音频文件扩展名
const ALLOWED_EXTENSIONS = [
'mp3', 'm4a', 'ogg', 'wav', 'flac', 'aac', 'opus', 'webm'
];

// 允许的CORS来源
const ALLOWED_ORIGINS = [
'http://localhost:3000',
'http://localhost:8080',
'https://your-static-site.com' // 替换为您的静态网站域名
];

export default {
async fetch(request, env) {
// 处理CORS预检请求
if (request.method === 'OPTIONS') {
return handleCORS(request);
}

const url = new URL(request.url);
const path = url.pathname;

// API路由处理
if (path.startsWith('/api/')) {
return handleAPIRequest(request, env, url);
}

// 静态文件请求(用于直接播放)
if (path.startsWith('/stream/')) {
return handleStreamRequest(request, env, url);
}

// 默认返回API信息
return new Response(JSON.stringify({
name: 'WebDAV音乐代理服务',
version: '1.0',
endpoints: {
list: '/api/list?path=文件夹路径',
stream: '/stream/文件路径',
search: '/api/search?q=搜索词',
cover: '/api/cover?path=音乐文件路径'
}
}), {
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': getOriginHeader(request)
}
});
}
};

// 处理CORS
function handleCORS(request) {
const origin = request.headers.get('Origin') || '*';
return new Response(null, {
status: 204,
headers: {
'Access-Control-Allow-Origin': ALLOWED_ORIGINS.includes(origin) ? origin : ALLOWED_ORIGINS[0],
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
'Access-Control-Max-Age': '86400'
}
});
}

// 获取允许的Origin
function getOriginHeader(request) {
const origin = request.headers.get('Origin');
if (origin && ALLOWED_ORIGINS.includes(origin)) {
return origin;
}
return ALLOWED_ORIGINS[0];
}

// 处理API请求
async function handleAPIRequest(request, env, url) {
const path = url.pathname;
const searchParams = url.searchParams;

try {
// 获取文件列表
if (path === '/api/list') {
const dirPath = searchParams.get('path') || '/';
return await listFiles(env, dirPath);
}

// 搜索文件
if (path === '/api/search') {
const query = searchParams.get('q') || '';
return await searchFiles(env, query);
}

// 获取专辑封面
if (path === '/api/cover') {
const filePath = searchParams.get('path') || '';
return await getAlbumCover(env, filePath);
}

return new Response(JSON.stringify({ error: 'API endpoint not found' }), {
status: 404,
headers: { 'Content-Type': 'application/json' }
});

} catch (error) {
return new Response(JSON.stringify({
error: error.message,
stack: env.WORKER_ENV === 'development' ? error.stack : undefined
}), {
status: 500,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': getOriginHeader(request)
}
});
}
}

// 处理流媒体请求
async function handleStreamRequest(request, env, url) {
const filePath = url.pathname.replace('/stream/', '');

if (!filePath) {
return new Response(JSON.stringify({ error: 'File path is required' }), {
status: 400,
headers: { 'Content-Type': 'application/json' }
});
}

try {
// 检查文件扩展名
const ext = filePath.split('.').pop().toLowerCase();
if (!ALLOWED_EXTENSIONS.includes(ext)) {
return new Response(JSON.stringify({ error: 'File type not allowed' }), {
status: 403,
headers: { 'Content-Type': 'application/json' }
});
}

// 构建WebDAV URL
const webdavUrl = `${env.WEBDAV_URL}/${filePath}`;

// 创建认证头
const auth = btoa(`${env.WEBDAV_USERNAME}:${env.WEBDAV_PASSWORD}`);

// 获取文件范围(支持断点续传)
const range = request.headers.get('Range');
const headers = {
'Authorization': `Basic ${auth}`,
'Accept': '*/*',
'User-Agent': 'WebDAV-Music-Player/1.0'
};

if (range) {
headers['Range'] = range;
}

// 发起请求
const response = await fetch(webdavUrl, { headers });

if (!response.ok) {
throw new Error(`WebDAV request failed: ${response.status} ${response.statusText}`);
}

// 确定内容类型
const contentType = getContentType(ext) || response.headers.get('Content-Type') || 'audio/mpeg';

// 返回响应
const responseHeaders = {
'Content-Type': contentType,
'Content-Length': response.headers.get('Content-Length'),
'Accept-Ranges': 'bytes',
'Access-Control-Allow-Origin': getOriginHeader(request),
'Access-Control-Expose-Headers': 'Content-Length, Content-Range',
'Cache-Control': 'public, max-age=31536000'
};

// 处理范围请求
if (response.status === 206) {
responseHeaders['Content-Range'] = response.headers.get('Content-Range');
}

return new Response(response.body, {
status: response.status,
headers: responseHeaders
});

} catch (error) {
return new Response(JSON.stringify({ error: error.message }), {
status: 500,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': getOriginHeader(request)
}
});
}
}

// 获取文件列表
async function listFiles(env, dirPath) {
// 使用PROPFIND方法获取目录列表
const webdavUrl = `${env.WEBDAV_URL}${dirPath}`;
const auth = btoa(`${env.WEBDAV_USERNAME}:${env.WEBDAV_PASSWORD}`);

const response = await fetch(webdavUrl, {
method: 'PROPFIND',
headers: {
'Authorization': `Basic ${auth}`,
'Depth': '1',
'Content-Type': 'application/xml; charset=utf-8'
},
body: `<?xml version="1.0" encoding="utf-8"?>
<propfind xmlns="DAV:">
<prop>
<displayname/>
<resourcetype/>
<getcontentlength/>
<getlastmodified/>
<getcontenttype/>
</prop>
</propfind>`
});

if (!response.ok) {
throw new Error(`Failed to list directory: ${response.status}`);
}

const xmlText = await response.text();
const files = parseWebDAVResponse(xmlText, dirPath);

return new Response(JSON.stringify({
path: dirPath,
files: files
}), {
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
}
});
}

// 解析WebDAV XML响应
function parseWebDAVResponse(xmlText, currentPath) {
const files = [];
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(xmlText, 'text/xml');

// 获取所有响应
const responses = xmlDoc.getElementsByTagNameNS('DAV:', 'response');

for (let response of responses) {
const href = response.getElementsByTagNameNS('DAV:', 'href')[0]?.textContent || '';
const displayName = response.getElementsByTagNameNS('DAV:', 'displayname')[0]?.textContent || '';
const resourceType = response.getElementsByTagNameNS('DAV:', 'resourcetype')[0];
const contentType = response.getElementsByTagNameNS('DAV:', 'getcontenttype')[0]?.textContent || '';
const contentLength = response.getElementsByTagNameNS('DAV:', 'getcontentlength')[0]?.textContent || '0';
const lastModified = response.getElementsByTagNameNS('DAV:', 'getlastmodified')[0]?.textContent || '';

// 跳过当前目录
if (href.endsWith('/') && href.split('/').filter(Boolean).pop() === currentPath.split('/').filter(Boolean).pop()) {
continue;
}

const isDirectory = resourceType.getElementsByTagNameNS('DAV:', 'collection').length > 0;
const name = displayName || href.split('/').filter(Boolean).pop() || '';

// 跳过隐藏文件
if (name.startsWith('.')) {
continue;
}

files.push({
name: name,
path: href,
isDirectory: isDirectory,
size: parseInt(contentLength) || 0,
type: isDirectory ? 'directory' : (contentType || getContentType(name.split('.').pop())),
modified: lastModified
});
}

return files;
}

// 搜索文件
async function searchFiles(env, query) {
// 由于WebDAV没有原生搜索,我们需要递归遍历
// 这里简化实现,只搜索根目录下的音乐文件
const allFiles = await getAllAudioFiles(env, '/');
const results = allFiles.filter(file =>
file.name.toLowerCase().includes(query.toLowerCase()) ||
file.path.toLowerCase().includes(query.toLowerCase())
);

return new Response(JSON.stringify({
query: query,
results: results
}), {
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
}
});
}

// 递归获取所有音频文件(简化版)
async function getAllAudioFiles(env, path) {
const files = [];

// 获取当前目录文件
const dirFiles = await getDirectoryFiles(env, path);

for (const file of dirFiles) {
if (file.isDirectory) {
// 递归获取子目录
const subFiles = await getAllAudioFiles(env, file.path);
files.push(...subFiles);
} else if (ALLOWED_EXTENSIONS.some(ext => file.name.toLowerCase().endsWith(`.${ext}`))) {
files.push(file);
}
}

return files;
}

// 获取目录文件(简化版,不使用PROPFIND)
async function getDirectoryFiles(env, path) {
// 这里简化实现,实际应该使用上面的listFiles逻辑
return [];
}

// 获取专辑封面(从MP3文件提取)
async function getAlbumCover(env, filePath) {
// 简化实现,返回默认封面
// 实际应解析MP3文件的ID3标签获取封面

return new Response(JSON.stringify({
path: filePath,
cover: null,
message: 'Album cover extraction not implemented in this example'
}), {
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
}
});
}

// 根据扩展名获取内容类型
function getContentType(ext) {
const types = {
'mp3': 'audio/mpeg',
'm4a': 'audio/mp4',
'ogg': 'audio/ogg',
'wav': 'audio/wav',
'flac': 'audio/flac',
'aac': 'audio/aac',
'opus': 'audio/opus',
'webm': 'audio/webm'
};

return types[ext.toLowerCase()] || 'application/octet-stream';
}

配置环境变量

在Worker设置中,添加以下环境变量:

  • WEBDAV_URL: 您的WebDAV服务器地址
  • WEBDAV_USERNAME: WebDAV用户名
  • WEBDAV_PASSWORD: WebDAV密码
  • WORKER_ENV: 设置为 “production” 或 “development”

添加自定义域名

Workers的域名在国内的访问速度很不理想,所以需要添加自定义域名

版本 使用方法
无UI 通过域名/stream/[文件名带后缀]访问测试
有UI 把完整的文件名输入到播放测试的输入框里,点击测试播放解析完成后就可以完整试听

注意:目前还有一些Bug(但是懒得修了),不影响使用

作者自己部署了示例网站 有UI版本 可以试用

引入方式

在文章中使用如下方式进行引入

1
2
3
4
5
<audio controls preload="metadata">
<source src="链接" type="audio/mpeg">
<!-- /dav不一定要写,建议尝试一下你网盘是什么类型的WebDav -->
您的浏览器不支持 audio 标签。
</audio>

Butterfly主题还可以使用标签

1
2
3
4
{% note info solid %}
**自定义文本**
<audio src="链接" controls style="width: 100%;"></audio>
{% endnote %}

链接部分

版本 内容
无UI 域名/stream/[文件名带后缀]
有UI https://[自定义域名](/dav/)[文件名带后缀]
/dav不一定要写

示例体验

速度慢一点无所谓了,下面放几首歌,可以体验

有UI

反乌托邦Pt.2 38.8 MB

DAMIDAMI 39.3 MB

提瓦特民谣 129 MB

无UI

让风告诉你 141.1MB

我不曾忘记 148 MB

你从未离去 68.1MB