Utoljára aktív 5 months ago

香港商业电台转发直播流

Revízió 368e6ca08b0039137123872b3a8db596cb915a9b

main.go Eredeti
1package main
2
3import (
4 "fmt"
5 "io"
6 "log"
7 "net/http"
8 "net/http/cookiejar"
9 "net/url"
10 "regexp"
11 "strings"
12 "sync"
13 "time"
14)
15
16// 配置常量
17const (
18 BasePath = "/edge-aac/903hd"
19 ServerPort = ":8080"
20)
21
22// ProxyServer 代理服务器结构 (帶狀態管理)
23type ProxyServer struct {
24 jar *cookiejar.Jar
25 cookieExpiration time.Time
26 upstreamHost string
27 mu sync.RWMutex
28}
29
30// NewProxyServer 创建新的代理服务器
31func NewProxyServer() *ProxyServer {
32 jar, _ := cookiejar.New(nil)
33 return &ProxyServer{
34 jar: jar,
35 cookieExpiration: time.Now(), // 設置為當前時間,以觸發首次請求時立即獲取
36 }
37}
38
39// ensureValidCookies 確保 Cookie 有效,如果過期則刷新
40func (ps *ProxyServer) ensureValidCookies() error {
41 ps.mu.RLock()
42 isExpired := time.Now().After(ps.cookieExpiration)
43 ps.mu.RUnlock()
44
45 if isExpired {
46 log.Println("Cookie expired or not yet fetched, refreshing...")
47 ps.mu.Lock()
48 defer ps.mu.Unlock()
49
50 if time.Now().After(ps.cookieExpiration) {
51 if err := ps.refreshCookies(); err != nil {
52 log.Printf("Failed to refresh cookies: %v", err)
53 return err
54 }
55 log.Println("Cookies refreshed successfully.")
56 }
57 }
58 return nil
59}
60
61// refreshCookies 執行完整的 Cookie 獲取和解析流程 (已按新邏輯修改)
62func (ps *ProxyServer) refreshCookies() error {
63 // --- 步驟 1 & 2: 獲取 liveJsUrl ---
64 liveJsURL, err := getLiveJsURL("https://www.881903.com/live/903")
65 if err != nil {
66 return err
67 }
68
69 // --- 步驟 3: 獲取 302 回應 ---
70 resp302, err := get302Response(liveJsURL)
71 if err != nil {
72 return err
73 }
74 defer resp302.Body.Close()
75
76 // --- 步驟 4: 訪問重定向地址以獲取真正的授權 Cookie ---
77 redirectURL, err := resp302.Location()
78 if err != nil {
79 return fmt.Errorf("failed to get redirect location: %w", err)
80 }
81
82 respFromRedirect, err := http.Get(redirectURL.String())
83 if err != nil {
84 return fmt.Errorf("failed to visit redirect URL: %w", err)
85 }
86 defer respFromRedirect.Body.Close()
87
88 cookies := respFromRedirect.Cookies()
89 if len(cookies) == 0 {
90 return fmt.Errorf("no cookies found in redirect response")
91 }
92
93 // --- 按照新邏輯設置狀態 ---
94
95 // 1. 清空舊的 Jar 並設置新的 Cookies
96 ps.jar, _ = cookiejar.New(nil)
97 ps.jar.SetCookies(redirectURL, cookies)
98
99 // 2. 將 Upstream Host 設置為 redirectURL 的 Host
100 ps.upstreamHost = redirectURL.Host
101 log.Printf("Upstream host set to: %s", ps.upstreamHost)
102
103 // 3. 將過期時間設置為當前時間 + 24 小時
104 ps.cookieExpiration = time.Now().Add(24 * time.Hour)
105 log.Printf("New cookie expiration set to: %s", ps.cookieExpiration.Format(time.RFC3339))
106
107 return nil
108}
109
110// proxyRequest 處理代理請求的核心邏輯
111func (ps *ProxyServer) proxyRequest(w http.ResponseWriter, r *http.Request) {
112 if err := ps.ensureValidCookies(); err != nil {
113 http.Error(w, fmt.Sprintf("Internal Server Error: %v", err), http.StatusInternalServerError)
114 return
115 }
116
117 ps.mu.RLock()
118 upstreamHost := ps.upstreamHost
119 ps.mu.RUnlock()
120
121 targetURL, err := ps.buildTargetURL(r.URL.Path, upstreamHost)
122 if err != nil {
123 http.Error(w, err.Error(), http.StatusNotFound)
124 return
125 }
126
127 log.Printf("Proxying request for %s to %s", r.URL.Path, targetURL)
128
129 client := &http.Client{
130 Jar: ps.jar,
131 Timeout: 30 * time.Second,
132 }
133 proxyReq, err := http.NewRequest(r.Method, targetURL, nil)
134 if err != nil {
135 http.Error(w, "Failed to create proxy request", http.StatusInternalServerError)
136 return
137 }
138
139 resp, err := client.Do(proxyReq)
140 if err != nil {
141 http.Error(w, fmt.Sprintf("Proxy request failed: %v", err), http.StatusBadGateway)
142 return
143 }
144 defer resp.Body.Close()
145
146 enableCORS(w)
147 for key, values := range resp.Header {
148 for _, value := range values {
149 w.Header().Add(key, value)
150 }
151 }
152
153 body, err := io.ReadAll(resp.Body)
154 if err != nil {
155 http.Error(w, "Failed to read response", http.StatusInternalServerError)
156 return
157 }
158
159 w.WriteHeader(resp.StatusCode)
160 w.Write(body)
161}
162
163// --- ServeHTTP 和其他輔助函數 (與之前版本相同) ---
164func (ps *ProxyServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
165 if r.Method == http.MethodOptions {
166 enableCORS(w)
167 w.WriteHeader(http.StatusNoContent)
168 return
169 }
170 ps.proxyRequest(w, r)
171}
172
173func (ps *ProxyServer) buildTargetURL(path string, host string) (string, error) {
174 switch {
175 case path == "/" || path == "/playlist.m3u8":
176 return fmt.Sprintf("https://%s%s/chunks.m3u8", host, BasePath), nil
177 case strings.HasPrefix(path, "/chunk/"):
178 return fmt.Sprintf("https://%s%s/%s", host, BasePath, strings.TrimPrefix(path, "/chunk/")), nil
179 default:
180 // 增加對 .aac 文件的直接代理
181 if strings.HasSuffix(path, ".aac") {
182 return fmt.Sprintf("https://%s%s%s", host, BasePath, path), nil
183 }
184 return "", fmt.Errorf("unsupported path: %s", path)
185 }
186}
187
188func enableCORS(w http.ResponseWriter) {
189 w.Header().Set("Access-Control-Allow-Origin", "*")
190}
191
192// --- Cookie 獲取輔助函數 (與之前版本相同) ---
193func getLiveJsURL(pageURL string) (string, error) {
194 log.Println("Fetching liveJsUrl from", pageURL)
195 resp, err := http.Get(pageURL)
196 if err != nil {
197 return "", err
198 }
199 defer resp.Body.Close()
200 body, err := io.ReadAll(resp.Body)
201 if err != nil {
202 return "", err
203 }
204 re := regexp.MustCompile(`"liveJsUrl":"([^"]+)"`)
205 matches := re.FindStringSubmatch(string(body))
206 if len(matches) < 2 {
207 return "", fmt.Errorf("liveJsUrl not found")
208 }
209 log.Println("Found liveJsUrl")
210 return matches[1], nil
211}
212
213func get302Response(jsURL string) (*http.Response, error) {
214 log.Println("Fetching 302 redirect from", jsURL)
215 client := &http.Client{
216 CheckRedirect: func(req *http.Request, via []*http.Request) error {
217 return http.ErrUseLastResponse
218 },
219 }
220 req, err := http.NewRequest("GET", jsURL, nil)
221 if err != nil {
222 return nil, err
223 }
224 req.Header.Set("accept", "*/*")
225 req.Header.Set("referer", "https://www.881903.com/live/903")
226 req.Header.Set("user-agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36")
227 resp, err := client.Do(req)
228 if urlErr, ok := err.(*url.Error); ok && urlErr.Err == http.ErrUseLastResponse {
229 return resp, nil
230 }
231 return resp, err
232}
233
234// --- Main 函數 (與之前版本相同) ---
235func main() {
236 proxy := NewProxyServer()
237
238 log.Println("Performing initial cookie fetch...")
239 if err := proxy.ensureValidCookies(); err != nil {
240 log.Fatalf("Initial cookie fetch failed: %v. Server cannot start.", err)
241 }
242
243 http.Handle("/", proxy)
244 log.Printf("Starting HLS proxy server on port %s", ServerPort)
245 log.Printf("Access playlist at: http://localhost%s/playlist.m3u8", ServerPort)
246 if err := http.ListenAndServe(ServerPort, nil); err != nil {
247 log.Fatal("Server failed to start:", err)
248 }
249}
250