package main import ( "io" "net/http" "net/url" "strings" ) // ============================================================ // 本地音频代理 // // 播放地址来自第三方 CDN(无 CORS 头),浏览器侧的 WebAudio // MediaElementSource 会因跨源污染输出全零频谱,导致无法做真实的 // 音频可视化。这里通过 Wails AssetServer 的 Handler 暴露 // /audioproxy?u=,由 Go 侧代取音频流回给 WebView: // - 前端 audio.src 变成同源地址,WebAudio 可正常分析频谱 // - 转发 Range 头,进度条拖动(206 分段加载)不受影响 // - 仅流式转发,不在本地落盘任何音频数据 // ============================================================ // 逐段透传给上游/下游的响应头 var proxyPassHeaders = []string{ "Content-Type", "Content-Length", "Content-Range", "Accept-Ranges", "Last-Modified", "ETag", } // audioProxyUA 纯浏览器 UA:部分音乐 CDN 的 WAF 会拦截带 // 非浏览器后缀的 UA(如 StarmusicDesktop/xxx),这里不带任何标识 const audioProxyUA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " + "(KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36" // audioProxy 处理 /audioproxy 请求:代理拉取音频流。 func (a *App) audioProxy(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/audioproxy" { http.NotFound(w, r) return } rawURL := r.URL.Query().Get("u") if rawURL == "" { http.Error(w, "missing u", http.StatusBadRequest) return } u, err := url.Parse(rawURL) if err != nil || (u.Scheme != "http" && u.Scheme != "https") { http.Error(w, "invalid url", http.StatusBadRequest) return } req, err := http.NewRequestWithContext(r.Context(), http.MethodGet, rawURL, nil) if err != nil { http.Error(w, "bad request", http.StatusBadRequest) return } // CDN 对非浏览器 UA 可能拒绝访问,用纯浏览器 UA;Range 透传以支持拖动进度 req.Header.Set("User-Agent", audioProxyUA) if rg := r.Header.Get("Range"); rg != "" { req.Header.Set("Range", rg) } resp, err := a.client.Do(req) if err != nil { // 客户端主动取消(切歌/暂停)属正常情况,不刷错误日志 if r.Context().Err() != nil { return } http.Error(w, "upstream error", http.StatusBadGateway) return } defer resp.Body.Close() h := w.Header() for _, k := range proxyPassHeaders { if v := resp.Header.Get(k); v != "" { h.Set(k, v) } } // 只拦截明确是网页的响应,避免代理被当成网页中转; // 其余(含未知/空 Content-Type)放行,由音频元素自行嗅探, // 部分音乐 CDN 会返回 text/plain 等非常规类型 if ct := h.Get("Content-Type"); strings.Contains(ct, "text/html") { resp.Body.Close() http.Error(w, "unsupported content-type", http.StatusBadGateway) return } w.WriteHeader(resp.StatusCode) _, _ = io.Copy(w, resp.Body) }