From ee212a0e48d34135e0d7766a0f77a61277b8eeb6 Mon Sep 17 00:00:00 2001 From: zarzet Date: Sun, 1 Feb 2026 17:44:19 +0700 Subject: [PATCH] fix(tidal): fix DASH download path for HIGH quality AAC - Fix m4aPath calculation in downloadFromManifest for HIGH quality - When outputPath is already .m4a, use it directly instead of appending .m4a - Reset httputil.go to fix build errors from merge conflict --- go_backend/httputil.go | 176 ----------------------------------------- go_backend/tidal.go | 10 ++- 2 files changed, 9 insertions(+), 177 deletions(-) diff --git a/go_backend/httputil.go b/go_backend/httputil.go index ccd8c9b5..be0f75de 100644 --- a/go_backend/httputil.go +++ b/go_backend/httputil.go @@ -1,7 +1,6 @@ package gobackend import ( - "context" "crypto/tls" "errors" "fmt" @@ -12,12 +11,8 @@ import ( "net/url" "strconv" "strings" - "sync" "syscall" "time" - - utls "github.com/refraction-networking/utls" - "golang.org/x/net/http2" ) // getRandomUserAgent generates a random Windows Chrome User-Agent string @@ -65,96 +60,6 @@ var sharedTransport = &http.Transport{ DisableCompression: true, } -// uTLS transport that mimics Chrome's TLS fingerprint to bypass Cloudflare -// Uses HTTP/2 for optimal performance as uTLS works best with HTTP/2 -type utlsTransport struct { - dialer *net.Dialer - mu sync.Mutex - h2Transports map[string]*http2.Transport -} - -func newUTLSTransport() *utlsTransport { - return &utlsTransport{ - dialer: &net.Dialer{ - Timeout: 30 * time.Second, - KeepAlive: 30 * time.Second, - }, - h2Transports: make(map[string]*http2.Transport), - } -} - -func (t *utlsTransport) RoundTrip(req *http.Request) (*http.Response, error) { - // For non-HTTPS, use standard transport - if req.URL.Scheme != "https" { - return sharedTransport.RoundTrip(req) - } - - host := req.URL.Hostname() - port := t.getPort(req.URL) - addr := net.JoinHostPort(host, port) - - // Dial TCP connection - conn, err := t.dialer.DialContext(req.Context(), "tcp", addr) - if err != nil { - return nil, err - } - - // Create uTLS connection with Chrome fingerprint (supports HTTP/2 ALPN) - tlsConn := utls.UClient(conn, &utls.Config{ - ServerName: host, - NextProtos: []string{"h2", "http/1.1"}, // Prefer HTTP/2 - }, utls.HelloChrome_Auto) - - // Perform TLS handshake - if err := tlsConn.Handshake(); err != nil { - conn.Close() - return nil, err - } - - // Check if server supports HTTP/2 - negotiatedProto := tlsConn.ConnectionState().NegotiatedProtocol - - if negotiatedProto == "h2" { - // Use HTTP/2 transport - h2Transport := &http2.Transport{ - DialTLSContext: func(ctx context.Context, network, addr string, cfg *tls.Config) (net.Conn, error) { - return tlsConn, nil - }, - AllowHTTP: false, - DisableCompression: false, - } - return h2Transport.RoundTrip(req) - } - - // Fallback to HTTP/1.1 - transport := &http.Transport{ - DialTLSContext: func(ctx context.Context, network, addr string) (net.Conn, error) { - return tlsConn, nil - }, - DisableKeepAlives: true, - } - - return transport.RoundTrip(req) -} - -func (t *utlsTransport) getPort(u *url.URL) string { - if u.Port() != "" { - return u.Port() - } - if u.Scheme == "https" { - return "443" - } - return "80" -} - -// Cloudflare bypass client using uTLS Chrome fingerprint -var cloudflareBypassTransport = newUTLSTransport() - -var cloudflareBypassClient = &http.Client{ - Transport: cloudflareBypassTransport, - Timeout: DefaultTimeout, -} - var sharedClient = &http.Client{ Transport: sharedTransport, Timeout: DefaultTimeout, @@ -180,12 +85,6 @@ func GetDownloadClient() *http.Client { return downloadClient } -// GetCloudflareBypassClient returns an HTTP client that mimics Chrome's TLS fingerprint -// Use this when requests are blocked by Cloudflare (common when using VPN) -func GetCloudflareBypassClient() *http.Client { - return cloudflareBypassClient -} - // CloseIdleConnections closes idle connections in the shared transport func CloseIdleConnections() { sharedTransport.CloseIdleConnections() @@ -201,81 +100,6 @@ func DoRequestWithUserAgent(client *http.Client, req *http.Request) (*http.Respo return resp, err } -// DoRequestWithCloudflareBypass attempts request with standard client first, -// then retries with uTLS Chrome fingerprint if Cloudflare blocks it. -// This is useful when using VPN as Cloudflare detects Go's default TLS fingerprint. -func DoRequestWithCloudflareBypass(req *http.Request) (*http.Response, error) { - req.Header.Set("User-Agent", getRandomUserAgent()) - - // Try with standard client first - resp, err := sharedClient.Do(req) - if err == nil { - // Check for Cloudflare challenge page (403 with specific markers) - if resp.StatusCode == 403 || resp.StatusCode == 503 { - body, readErr := io.ReadAll(resp.Body) - resp.Body.Close() - - if readErr == nil { - bodyStr := strings.ToLower(string(body)) - cloudflareMarkers := []string{ - "cloudflare", "cf-ray", "checking your browser", - "please wait", "ddos protection", "ray id", - "enable javascript", "challenge-platform", - } - - isCloudflare := false - for _, marker := range cloudflareMarkers { - if strings.Contains(bodyStr, marker) { - isCloudflare = true - break - } - } - - if isCloudflare { - LogDebug("HTTP", "Cloudflare detected, retrying with Chrome TLS fingerprint...") - - // Clone request for retry - reqCopy := req.Clone(req.Context()) - reqCopy.Header.Set("User-Agent", getRandomUserAgent()) - - // Retry with uTLS Chrome fingerprint - return cloudflareBypassClient.Do(reqCopy) - } - } - - // Not Cloudflare, return original response (recreate body) - return &http.Response{ - Status: resp.Status, - StatusCode: resp.StatusCode, - Header: resp.Header, - Body: io.NopCloser(strings.NewReader(string(body))), - }, nil - } - return resp, nil - } - - // Check if error might be TLS-related (Cloudflare blocking) - errStr := strings.ToLower(err.Error()) - tlsRelated := strings.Contains(errStr, "tls") || - strings.Contains(errStr, "handshake") || - strings.Contains(errStr, "certificate") || - strings.Contains(errStr, "connection reset") - - if tlsRelated { - LogDebug("HTTP", "TLS error detected, retrying with Chrome TLS fingerprint: %v", err) - - // Clone request for retry - reqCopy := req.Clone(req.Context()) - reqCopy.Header.Set("User-Agent", getRandomUserAgent()) - - // Retry with uTLS Chrome fingerprint - return cloudflareBypassClient.Do(reqCopy) - } - - CheckAndLogISPBlocking(err, req.URL.String(), "HTTP") - return nil, err -} - // RetryConfig holds configuration for retry logic type RetryConfig struct { MaxRetries int diff --git a/go_backend/tidal.go b/go_backend/tidal.go index 7fecf2be..50809197 100644 --- a/go_backend/tidal.go +++ b/go_backend/tidal.go @@ -970,7 +970,15 @@ func (t *TidalDownloader) downloadFromManifest(ctx context.Context, manifestB64, return nil } - m4aPath := strings.TrimSuffix(outputPath, ".flac") + ".m4a" + // For DASH format, determine correct M4A path + // If outputPath already ends with .m4a, use it directly + // Otherwise, convert .flac to .m4a + var m4aPath string + if strings.HasSuffix(outputPath, ".m4a") { + m4aPath = outputPath + } else { + m4aPath = strings.TrimSuffix(outputPath, ".flac") + ".m4a" + } GoLog("[Tidal] DASH format - downloading %d segments directly to: %s\n", len(mediaURLs), m4aPath) out, err := os.Create(m4aPath)