YvYang的AI摘要
Spark-Lite

前言

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

教程

编辑代码

登录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 } });
}
}

配置境变量

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

添加自定义域名

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

这时候访问域名就可以使用了,把完整的文件名输入到播放测试的输入框里,点击测试播放解析完成后就可以完整试听

注意:目前还有一些Bug(但是懒得修了),不影响使用
作者自己部署了一个示例网站可以试用

引入方式

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

1
2
3
4
5
<audio controls preload="metadata">
<source src="https://[自定义域名](/dav/)[文件名带后缀]" type="audio/mpeg">
<!-- /dav不一定要写,建议尝试一下你网盘是什么类型的WebDav -->
您的浏览器不支持 audio 标签。
</audio>

示例体验

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

反乌托邦Pt.2

DAMIDAMI

提瓦特民谣