使用CloudFlare代理WebDav播放音乐

3833 字
19 分钟
使用CloudFlare代理WebDav播放音乐

前言#

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

教程#

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

有网页UI版本#

创建cloudfalre worker#

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

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中覆盖写入一下内容

// 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版本 可以试用

引入方式#

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

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

Butterfly主题还可以使用标签

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

链接部分

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

示例体验#

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

反乌托邦Pt.2 38.8 MB#

DAMIDAMI 39.3 MB#

提瓦特民谣 129 MB#

让风告诉你 141.1MB

我不曾忘记 148 MB

你从未离去 68.1MB

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或赞助支持!

赞助
使用CloudFlare代理WebDav播放音乐
https://blog.yvyang.top/posts/20242/index.HTML/
作者
余漾
发布于
2026-03-15
许可协议
CC BY-NC-SA 4.0
相关文章 智能推荐
1
添加来访者卡片
建站札记 这篇文章介绍了在Hexo博客中使用小小API替代失效的Nice猫API来实现访客卡片功能的方法。文章首先说明了更换API的背景原因,随后详细讲解了创建welcome.js文件的步骤,包括IP数据获取、地址解析、缓存管理、欢迎消息生成等核心代码逻辑,并提供了完整的JavaScript代码示例。最后指导用户如何在主题配置文件中引入JS文件和配置卡片内容,使博客能够根据访客的地理位置和IP地址显示个性化的欢迎信息,提升网站交互体验。
2
Butterfly:为博客添加微软Clarity数据统计
建站札记 这篇文章介绍了如何为博客添加微软Clarity数据统计工具,通过Hexo框架和Butterfly主题进行演示。首先需要注册微软账号并登录Clarity官网,然后新建项目并将网站添加到项目中。接下来,在`_config.butterfly.yml`主题配置文件中复制粘贴代码,按照缩进格式填写,最后保存后等待效果显现。
3
Reverse:1999解包——音乐《箱中气候》
闲时偶记 这篇文章讲述了作者对游戏重返未来1999中配乐的欣赏,并尝试在游戏文件内找到这些音乐。通过参考教程,作者成功解包了1600首音乐文件,并记录下了其中两首的主要信息,包括标题和专辑名称,封面则取自1999官网。文章最后提供了下载链接,供读者下载这些音乐。
4
网站加速方案:阿里云免费版边缘安全加速ESA
建站札记 这篇文章介绍了如何通过阿里云免费版边缘安全加速ESA服务为个人博客网站加速。作者首先描述了自己无法使用国内CDN服务的情况,然后发现了阿里云的ESA服务,并决定尝试开通该服务。在开通过程中,作者详细解释了如何在控制台添加站点、设置DNS解析记录以及申请证书的具体步骤。
5
为网站添加并使用iconfont图标
建站札记 这篇文章介绍了如何在hexo-theme-butterfly主题中添加并使用Iconfont图标库。作者首先说明了在默认的FontAwesome图标库中存在的问题,如需要付费以及有时找不到特定图标的需求,随后转向引入Iconfont作为补充。Iconfont是由阿里妈妈MUX开发的一个矢量图标管理、交流平台。文章详细指导了注册账号和获取图标代码的过程,包括如何访问官网、选择图标加入购物车、添加到项目、更新配置文件等步骤。最后,文章总结了使用Iconfont图标服务的优势,并鼓励读者尝试这个工具以丰富网站内容。
随机文章 随机推荐

评论区

Profile Image of the Author
余漾
澄空碧山 绿水余漾
公告
欢迎来到我的博客!这是一则示例公告。
音乐
封面

音乐

暂未播放

0:00 0:00
暂无歌词
分类
标签
站点统计
文章
19
分类
2
标签
33
总字数
17,397
运行时长
0
总访问量
0
总访客数
0

文章目录