使用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) } }); }};
// 处理CORSfunction 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' } });}
// 获取允许的Originfunction 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/[文件名带后缀] |
| 有UI | https://[自定义域名](/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/ 相关文章 智能推荐
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图标服务的优势,并鼓励读者尝试这个工具以丰富网站内容。
随机文章 随机推荐