使用CloudFlare代理WebDav播放音乐
前言
最近下载了很多音乐,上传到网盘后就想着能不能引用到网站上,正好网盘支持WebDav挂载,没有服务器就只能使用CouldFlare Workers来实现。
教程
有两种方案:有网页UI和无UI,前者方便在线播放,后者访问速度更佳,读者可以视需求选择
有网页UI版本
创建cloudfalre worker
登录CloudFlare,创建Workers,选择HelloWorld模版
覆盖式写入以下代码:
JavaScript
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中覆盖写入一下内容
JavaScript
// 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版本 可以试用
引入方式
在文章中使用如下方式进行引入
Markdown
<audio controls preload="metadata">
<source src="链接" type="audio/mpeg">
<!-- /dav不一定要写,建议尝试一下你网盘是什么类型的WebDav -->
您的浏览器不支持 audio 标签。
</audio>Butterfly主题还可以使用标签
Markdown
{% 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
{% note simple %}
让风告诉你 141.1MB
{% endnote %}
{% note info solid %}
我不曾忘记 148 MB
{% endnote %}
{% note success solid %}
你从未离去 68.1MB
{% endnote %}

