This commit is contained in:
afkarxyz
2026-02-10 21:18:05 +07:00
parent 36a77ad8d1
commit df56049db2
30 changed files with 1689 additions and 488 deletions
+129 -70
View File
@@ -101,6 +101,8 @@ func (t *TidalDownloader) GetTidalURLFromSpotify(spotifyTrackID string) (string,
return "", fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36")
fmt.Println("Getting Tidal URL...")
resp, err := t.client.Do(req)
@@ -157,7 +159,15 @@ func (t *TidalDownloader) GetDownloadURL(trackID int64, quality string) (string,
url := fmt.Sprintf("%s/track/?id=%d&quality=%s", t.apiURL, trackID, quality)
fmt.Printf("Tidal API URL: %s\n", url)
resp, err := t.client.Get(url)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
fmt.Printf("✗ failed to create request: %v\n", err)
return "", fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36")
resp, err := t.client.Do(req)
if err != nil {
fmt.Printf("✗ Tidal API request failed: %v\n", err)
return "", fmt.Errorf("failed to get download URL: %w", err)
@@ -214,7 +224,14 @@ func (t *TidalDownloader) DownloadFile(url, filepath string) error {
return t.DownloadFromManifest(strings.TrimPrefix(url, "MANIFEST:"), filepath)
}
resp, err := t.client.Get(url)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36")
resp, err := t.client.Do(req)
if err != nil {
return fmt.Errorf("failed to download file: %w", err)
@@ -244,7 +261,7 @@ func (t *TidalDownloader) DownloadFile(url, filepath string) error {
}
func (t *TidalDownloader) DownloadFromManifest(manifestB64, outputPath string) error {
directURL, initURL, mediaURLs, err := parseManifest(manifestB64)
directURL, initURL, mediaURLs, mimeType, err := parseManifest(manifestB64)
if err != nil {
return fmt.Errorf("failed to parse manifest: %w", err)
}
@@ -253,10 +270,19 @@ func (t *TidalDownloader) DownloadFromManifest(manifestB64, outputPath string) e
Timeout: 120 * time.Second,
}
if directURL != "" {
doRequest := func(url string) (*http.Response, error) {
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36")
return client.Do(req)
}
if directURL != "" && (strings.Contains(strings.ToLower(mimeType), "flac") || mimeType == "") {
fmt.Println("Downloading file...")
resp, err := client.Get(directURL)
resp, err := doRequest(directURL)
if err != nil {
return fmt.Errorf("failed to download file: %w", err)
}
@@ -283,83 +309,116 @@ func (t *TidalDownloader) DownloadFromManifest(manifestB64, outputPath string) e
return nil
}
fmt.Printf("Downloading %d segments...\n", len(mediaURLs)+1)
tempPath := outputPath + ".m4a.tmp"
out, err := os.Create(tempPath)
if err != nil {
return fmt.Errorf("failed to create temp file: %w", err)
}
fmt.Print("Downloading init segment... ")
resp, err := client.Get(initURL)
if err != nil {
out.Close()
os.Remove(tempPath)
return fmt.Errorf("failed to download init segment: %w", err)
}
if resp.StatusCode != 200 {
resp.Body.Close()
out.Close()
os.Remove(tempPath)
return fmt.Errorf("init segment download failed with status %d", resp.StatusCode)
}
_, err = io.Copy(out, resp.Body)
resp.Body.Close()
if err != nil {
out.Close()
os.Remove(tempPath)
return fmt.Errorf("failed to write init segment: %w", err)
}
fmt.Println("OK")
if directURL != "" {
fmt.Printf("Downloading non-FLAC file (%s)...\n", mimeType)
totalSegments := len(mediaURLs)
var totalBytes int64
lastTime := time.Now()
var lastBytes int64
for i, mediaURL := range mediaURLs {
resp, err := client.Get(mediaURL)
resp, err := doRequest(directURL)
if err != nil {
return fmt.Errorf("failed to download file: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return fmt.Errorf("download failed with status %d", resp.StatusCode)
}
out, err := os.Create(tempPath)
if err != nil {
return fmt.Errorf("failed to create temp file: %w", err)
}
pw := NewProgressWriter(out)
_, err = io.Copy(pw, resp.Body)
out.Close()
if err != nil {
os.Remove(tempPath)
return fmt.Errorf("failed to write temp file: %w", err)
}
fmt.Printf("\rDownloaded: %.2f MB (Complete)\n", float64(pw.GetTotal())/(1024*1024))
} else {
fmt.Printf("Downloading %d segments...\n", len(mediaURLs)+1)
out, err := os.Create(tempPath)
if err != nil {
return fmt.Errorf("failed to create temp file: %w", err)
}
fmt.Print("Downloading init segment... ")
resp, err := doRequest(initURL)
if err != nil {
out.Close()
os.Remove(tempPath)
return fmt.Errorf("failed to download segment %d: %w", i+1, err)
return fmt.Errorf("failed to download init segment: %w", err)
}
if resp.StatusCode != 200 {
resp.Body.Close()
out.Close()
os.Remove(tempPath)
return fmt.Errorf("segment %d download failed with status %d", i+1, resp.StatusCode)
return fmt.Errorf("init segment download failed with status %d", resp.StatusCode)
}
n, err := io.Copy(out, resp.Body)
totalBytes += n
_, err = io.Copy(out, resp.Body)
resp.Body.Close()
if err != nil {
out.Close()
os.Remove(tempPath)
return fmt.Errorf("failed to write segment %d: %w", i+1, err)
return fmt.Errorf("failed to write init segment: %w", err)
}
fmt.Println("OK")
totalSegments := len(mediaURLs)
var totalBytes int64
lastTime := time.Now()
var lastBytes int64
for i, mediaURL := range mediaURLs {
resp, err := doRequest(mediaURL)
if err != nil {
out.Close()
os.Remove(tempPath)
return fmt.Errorf("failed to download segment %d: %w", i+1, err)
}
if resp.StatusCode != 200 {
resp.Body.Close()
out.Close()
os.Remove(tempPath)
return fmt.Errorf("segment %d download failed with status %d", i+1, resp.StatusCode)
}
n, err := io.Copy(out, resp.Body)
totalBytes += n
resp.Body.Close()
if err != nil {
out.Close()
os.Remove(tempPath)
return fmt.Errorf("failed to write segment %d: %w", i+1, err)
}
mbDownloaded := float64(totalBytes) / (1024 * 1024)
now := time.Now()
timeDiff := now.Sub(lastTime).Seconds()
var speedMBps float64
if timeDiff > 0.1 {
bytesDiff := float64(totalBytes - lastBytes)
speedMBps = (bytesDiff / (1024 * 1024)) / timeDiff
SetDownloadSpeed(speedMBps)
lastTime = now
lastBytes = totalBytes
}
SetDownloadProgress(mbDownloaded)
fmt.Printf("\rDownloading: %.2f MB (%d/%d segments)", mbDownloaded, i+1, totalSegments)
}
mbDownloaded := float64(totalBytes) / (1024 * 1024)
now := time.Now()
timeDiff := now.Sub(lastTime).Seconds()
var speedMBps float64
if timeDiff > 0.1 {
bytesDiff := float64(totalBytes - lastBytes)
speedMBps = (bytesDiff / (1024 * 1024)) / timeDiff
SetDownloadSpeed(speedMBps)
lastTime = now
lastBytes = totalBytes
}
SetDownloadProgress(mbDownloaded)
out.Close()
fmt.Printf("\rDownloading: %.2f MB (%d/%d segments)", mbDownloaded, i+1, totalSegments)
tempInfo, _ := os.Stat(tempPath)
fmt.Printf("\rDownloaded: %.2f MB (Complete) \n", float64(tempInfo.Size())/(1024*1024))
}
out.Close()
tempInfo, _ := os.Stat(tempPath)
fmt.Printf("\rDownloaded: %.2f MB (Complete) \n", float64(tempInfo.Size())/(1024*1024))
fmt.Println("Converting to FLAC...")
ffmpegPath, err := GetFFmpegPath()
if err != nil {
@@ -633,10 +692,10 @@ type MPD struct {
} `xml:"Period"`
}
func parseManifest(manifestB64 string) (directURL string, initURL string, mediaURLs []string, err error) {
func parseManifest(manifestB64 string) (directURL string, initURL string, mediaURLs []string, mimeType string, err error) {
manifestBytes, err := base64.StdEncoding.DecodeString(manifestB64)
if err != nil {
return "", "", nil, fmt.Errorf("failed to decode manifest: %w", err)
return "", "", nil, "", fmt.Errorf("failed to decode manifest: %w", err)
}
manifestStr := string(manifestBytes)
@@ -644,15 +703,15 @@ func parseManifest(manifestB64 string) (directURL string, initURL string, mediaU
if strings.HasPrefix(strings.TrimSpace(manifestStr), "{") {
var btsManifest TidalBTSManifest
if err := json.Unmarshal(manifestBytes, &btsManifest); err != nil {
return "", "", nil, fmt.Errorf("failed to parse BTS manifest: %w", err)
return "", "", nil, "", fmt.Errorf("failed to parse BTS manifest: %w", err)
}
if len(btsManifest.URLs) == 0 {
return "", "", nil, fmt.Errorf("no URLs in BTS manifest")
return "", "", nil, "", fmt.Errorf("no URLs in BTS manifest")
}
fmt.Printf("Manifest: BTS format (%s, %s)\n", btsManifest.MimeType, btsManifest.Codecs)
return btsManifest.URLs[0], "", nil, nil
return btsManifest.URLs[0], "", nil, btsManifest.MimeType, nil
}
fmt.Println("Manifest: DASH format")
@@ -717,7 +776,7 @@ func parseManifest(manifestB64 string) (directURL string, initURL string, mediaU
mediaURL := strings.ReplaceAll(mediaTemplate, "$Number$", fmt.Sprintf("%d", i))
mediaURLs = append(mediaURLs, mediaURL)
}
return "", initURL, mediaURLs, nil
return "", initURL, mediaURLs, "", nil
}
fmt.Println("Using regex fallback for DASH manifest...")
@@ -733,7 +792,7 @@ func parseManifest(manifestB64 string) (directURL string, initURL string, mediaU
}
if initURL == "" {
return "", "", nil, fmt.Errorf("no initialization URL found in manifest")
return "", "", nil, "", fmt.Errorf("no initialization URL found in manifest")
}
initURL = strings.ReplaceAll(initURL, "&", "&")
@@ -754,7 +813,7 @@ func parseManifest(manifestB64 string) (directURL string, initURL string, mediaU
}
if segmentCount == 0 {
return "", "", nil, fmt.Errorf("no segments found in manifest (XML: %d, Regex: 0)", len(matches))
return "", "", nil, "", fmt.Errorf("no segments found in manifest (XML: %d, Regex: 0)", len(matches))
}
fmt.Printf("Parsed manifest via Regex: %d segments\n", segmentCount)
@@ -764,7 +823,7 @@ func parseManifest(manifestB64 string) (directURL string, initURL string, mediaU
mediaURLs = append(mediaURLs, mediaURL)
}
return "", initURL, mediaURLs, nil
return "", initURL, mediaURLs, "", nil
}
func getDownloadURLRotated(apis []string, trackID int64, quality string) (string, string, error) {