仰望星辰工作室

starmusic-pc

starmusic-pc/ proxy.go 2.9 KB · 93 行 原始文件
1package main
2
3import (
4 "io"
5 "net/http"
6 "net/url"
7 "strings"
8)
9
10// ============================================================
11// 本地音频代理
12//
13// 播放地址来自第三方 CDN(无 CORS 头),浏览器侧的 WebAudio
14// MediaElementSource 会因跨源污染输出全零频谱,导致无法做真实的
15// 音频可视化。这里通过 Wails AssetServer 的 Handler 暴露
16// /audioproxy?u=<cdn url>,由 Go 侧代取音频流回给 WebView:
17// - 前端 audio.src 变成同源地址,WebAudio 可正常分析频谱
18// - 转发 Range 头,进度条拖动(206 分段加载)不受影响
19// - 仅流式转发,不在本地落盘任何音频数据
20// ============================================================
21
22// 逐段透传给上游/下游的响应头
23var proxyPassHeaders = []string{
24 "Content-Type",
25 "Content-Length",
26 "Content-Range",
27 "Accept-Ranges",
28 "Last-Modified",
29 "ETag",
30}
31
32// audioProxyUA 纯浏览器 UA:部分音乐 CDN 的 WAF 会拦截带
33// 非浏览器后缀的 UA(如 StarmusicDesktop/xxx),这里不带任何标识
34const audioProxyUA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " +
35 "(KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36"
36
37// audioProxy 处理 /audioproxy 请求:代理拉取音频流。
38func (a *App) audioProxy(w http.ResponseWriter, r *http.Request) {
39 if r.URL.Path != "/audioproxy" {
40 http.NotFound(w, r)
41 return
42 }
43 rawURL := r.URL.Query().Get("u")
44 if rawURL == "" {
45 http.Error(w, "missing u", http.StatusBadRequest)
46 return
47 }
48 u, err := url.Parse(rawURL)
49 if err != nil || (u.Scheme != "http" && u.Scheme != "https") {
50 http.Error(w, "invalid url", http.StatusBadRequest)
51 return
52 }
53
54 req, err := http.NewRequestWithContext(r.Context(), http.MethodGet, rawURL, nil)
55 if err != nil {
56 http.Error(w, "bad request", http.StatusBadRequest)
57 return
58 }
59 // CDN 对非浏览器 UA 可能拒绝访问,用纯浏览器 UA;Range 透传以支持拖动进度
60 req.Header.Set("User-Agent", audioProxyUA)
61 if rg := r.Header.Get("Range"); rg != "" {
62 req.Header.Set("Range", rg)
63 }
64
65 resp, err := a.client.Do(req)
66 if err != nil {
67 // 客户端主动取消(切歌/暂停)属正常情况,不刷错误日志
68 if r.Context().Err() != nil {
69 return
70 }
71 http.Error(w, "upstream error", http.StatusBadGateway)
72 return
73 }
74 defer resp.Body.Close()
75
76 h := w.Header()
77 for _, k := range proxyPassHeaders {
78 if v := resp.Header.Get(k); v != "" {
79 h.Set(k, v)
80 }
81 }
82 // 只拦截明确是网页的响应,避免代理被当成网页中转;
83 // 其余(含未知/空 Content-Type)放行,由音频元素自行嗅探,
84 // 部分音乐 CDN 会返回 text/plain 等非常规类型
85 if ct := h.Get("Content-Type"); strings.Contains(ct, "text/html") {
86 resp.Body.Close()
87 http.Error(w, "unsupported content-type", http.StatusBadGateway)
88 return
89 }
90
91 w.WriteHeader(resp.StatusCode)
92 _, _ = io.Copy(w, resp.Body)
93}