From e9fd50a776b189bcf8e2dc68f63dda22b5b58373 Mon Sep 17 00:00:00 2001 From: Sayem Chowdhury Date: Fri, 27 Sep 2024 00:19:15 +0600 Subject: [PATCH] pkg download --- pkg/download/types.go | 16 ++++ pkg/download/url.go | 186 +++++++++++++++++++++++++++++++++++++++ pkg/download/url_test.go | 73 +++++++++++++++ 3 files changed, 275 insertions(+) create mode 100644 pkg/download/types.go create mode 100644 pkg/download/url.go create mode 100644 pkg/download/url_test.go diff --git a/pkg/download/types.go b/pkg/download/types.go new file mode 100644 index 0000000..7c6356c --- /dev/null +++ b/pkg/download/types.go @@ -0,0 +1,16 @@ +package download + +// UserData struct stores user license and streaming capabilities +type UserData struct { + LicenseToken string + CanStreamLossless bool + CanStreamHQ bool + Country string +} + +// TrackDownloadUrl represents the details of a track's download URL. +type TrackDownloadUrl struct { + TrackUrl string // The URL to download the track. + IsEncrypted bool // Indicates if the track URL points to an encrypted file. + FileSize int // The size of the track file in bytes. +} diff --git a/pkg/download/url.go b/pkg/download/url.go new file mode 100644 index 0000000..19433d3 --- /dev/null +++ b/pkg/download/url.go @@ -0,0 +1,186 @@ +package download + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/d-fi/GoFi/pkg/decrypt" + "github.com/d-fi/GoFi/pkg/request" + "github.com/d-fi/GoFi/pkg/types" + "github.com/d-fi/GoFi/pkg/utils" +) + +// WrongLicense error for when the user's license doesn't allow streaming certain formats. +type WrongLicense struct { + Format string +} + +func (e *WrongLicense) Error() string { + return fmt.Sprintf("Your account can't stream %s tracks", e.Format) +} + +// GeoBlocked error for when the track is not available in the user's country. +type GeoBlocked struct { + Country string +} + +func (e *GeoBlocked) Error() string { + return fmt.Sprintf("This track is not available in your country (%s)", e.Country) +} + +var userData *UserData + +// DzAuthenticate authenticates with Deezer and retrieves user data. +func DzAuthenticate() (*UserData, error) { + if userData != nil { + return userData, nil // Use cached user data if available + } + + resp, err := request.Client.R(). + SetQueryParams(map[string]string{ + "method": "deezer.getUserData", + "api_version": "1.0", + "api_token": "null", + }). + Get("https://www.deezer.com/ajax/gw-light.php") + + if err != nil { + return nil, err + } + + var data map[string]interface{} + if err := json.Unmarshal(resp.Body(), &data); err != nil { + return nil, err + } + + results := data["results"].(map[string]interface{}) + options := results["USER"].(map[string]interface{})["OPTIONS"].(map[string]interface{}) + country := results["COUNTRY"].(string) + + userData = &UserData{ + LicenseToken: options["license_token"].(string), + CanStreamLossless: options["web_lossless"].(bool) || options["mobile_loseless"].(bool), + CanStreamHQ: options["web_hq"].(bool) || options["mobile_hq"].(bool), + Country: country, + } + + return userData, nil +} + +// GetTrackUrlFromServer fetches the track URL from the server based on the track token and format. +func GetTrackUrlFromServer(trackToken, format string) (string, error) { + user, err := DzAuthenticate() + if err != nil { + return "", err + } + + // Check if the user license allows streaming the requested format. + if (format == "FLAC" && !user.CanStreamLossless) || (format == "MP3_320" && !user.CanStreamHQ) { + return "", &WrongLicense{Format: format} + } + + resp, err := request.Client.R(). + SetBody(map[string]interface{}{ + "license_token": user.LicenseToken, + "media": []map[string]interface{}{ + { + "type": "FULL", + "formats": []map[string]string{{"format": format, "cipher": "BF_CBC_STRIPE"}}, + }, + }, + "track_tokens": []string{trackToken}, + }). + Post("https://media.deezer.com/v1/get_url") + + if err != nil { + return "", err + } + + var response map[string]interface{} + if err := json.Unmarshal(resp.Body(), &response); err != nil { + return "", err + } + + data := response["data"].([]interface{}) + if len(data) > 0 { + trackData := data[0].(map[string]interface{}) + if errors, exists := trackData["errors"]; exists { + errorCode := errors.([]interface{})[0].(map[string]interface{})["code"].(float64) + if errorCode == 2002 { + return "", &GeoBlocked{Country: user.Country} + } + return "", fmt.Errorf("API error: %v", errors) + } + + if media := trackData["media"].([]interface{}); len(media) > 0 { + sources := media[0].(map[string]interface{})["sources"].([]interface{}) + return sources[0].(map[string]interface{})["url"].(string), nil + } + } + + return "", nil +} + +// GetTrackDownloadUrl retrieves the download URL of a track based on quality. +func GetTrackDownloadUrl(track types.TrackType, quality int) (*TrackDownloadUrl, error) { + var formatName string + switch quality { + case 9: + formatName = "FLAC" + case 3: + formatName = "MP3_320" + case 1: + formatName = "MP3_128" + default: + return nil, fmt.Errorf("unknown quality %d", quality) + } + + var wrongLicense *WrongLicense + var geoBlocked *GeoBlocked + + // Attempt to get the URL with the official API. + url, err := GetTrackUrlFromServer(track.TRACK_TOKEN, formatName) + if err == nil && url != "" { + fileSize, err := utils.CheckURLFileSize(url) + if err == nil && fileSize > 0 { + return &TrackDownloadUrl{ + TrackUrl: url, + IsEncrypted: strings.Contains(url, "/mobile/") || strings.Contains(url, "/media/"), + FileSize: fileSize, + }, nil + } + } else if err != nil { + if wl, ok := err.(*WrongLicense); ok { + wrongLicense = wl + } else if gb, ok := err.(*GeoBlocked); ok { + geoBlocked = gb + } else { + return nil, err + } + } + + // Fallback to the old method. + filename := decrypt.GetSongFileName(&decrypt.TrackType{ + MD5_ORIGIN: track.MD5_ORIGIN, + SNG_ID: track.SNG_ID, + MEDIA_VERSION: track.MEDIA_VERSION, + }, quality) + fallbackURL := fmt.Sprintf("https://e-cdns-proxy-%s.dzcdn.net/mobile/1/%s", string(track.MD5_ORIGIN[0]), filename) + fileSize, err := utils.CheckURLFileSize(fallbackURL) + if err == nil && fileSize > 0 { + return &TrackDownloadUrl{ + TrackUrl: fallbackURL, + IsEncrypted: strings.Contains(fallbackURL, "/mobile/") || strings.Contains(fallbackURL, "/media/"), + FileSize: fileSize, + }, nil + } + + if wrongLicense != nil { + return nil, wrongLicense + } + if geoBlocked != nil { + return nil, geoBlocked + } + return nil, err +} diff --git a/pkg/download/url_test.go b/pkg/download/url_test.go new file mode 100644 index 0000000..b7d2386 --- /dev/null +++ b/pkg/download/url_test.go @@ -0,0 +1,73 @@ +package download + +import ( + "os" + "strconv" + "testing" + + "github.com/d-fi/GoFi/pkg/api" + "github.com/d-fi/GoFi/pkg/request" + "github.com/stretchr/testify/assert" +) + +const ( + SNG_ID = "3135556" // Harder, Better, Faster, Stronger by Daft Punk +) + +func init() { + // Initialize the Deezer API for all tests + arl := os.Getenv("DEEZER_ARL") + _, err := request.InitDeezerAPI(arl) + if err != nil { + panic("Failed to initialize Deezer API: " + err.Error()) + } +} + +func TestDzAuthenticate(t *testing.T) { + user, err := DzAuthenticate() + assert.NoError(t, err) + assert.NotNil(t, user) + assert.NotEmpty(t, user.LicenseToken) + assert.True(t, user.CanStreamLossless || user.CanStreamHQ) + assert.NotEmpty(t, user.Country) +} + +func TestGetTrackUrlFromServer(t *testing.T) { + trackToken := "example_track_token" + _, err := GetTrackUrlFromServer(trackToken, "MP3_320") + assert.Error(t, err, "Expected error due to incorrect token or unavailable track") +} + +func TestGetTrackDownloadUrl(t *testing.T) { + track, err := api.GetTrackInfo(SNG_ID) + assert.NoError(t, err, "Failed to fetch track information") + assert.NotEmpty(t, track.MD5_ORIGIN, "MD5 origin should not be empty") + assert.NotEmpty(t, track.TRACK_TOKEN, "Track token should not be empty") + + // Testing various qualities + qualities := []int{1, 3, 9} + + for _, quality := range qualities { + t.Run("Quality "+strconv.Itoa(quality), func(t *testing.T) { + trackURL, err := GetTrackDownloadUrl(track, quality) + if err == nil { + assert.NotNil(t, trackURL) + assert.NotEmpty(t, trackURL.TrackUrl) + assert.Greater(t, trackURL.FileSize, int(0)) + } else { + assert.Error(t, err) + assert.Contains(t, err.Error(), "Your account can't stream") + } + }) + } +} + +func TestGetTrackDownloadUrlWithInvalidQuality(t *testing.T) { + track, err := api.GetTrackInfo(SNG_ID) + assert.NoError(t, err, "Failed to fetch track information") + assert.NotEmpty(t, track.TRACK_TOKEN, "Track token should not be empty") + + _, err = GetTrackDownloadUrl(track, 999) // Testing an invalid quality + assert.Error(t, err) + assert.Contains(t, err.Error(), "unknown quality 999") +}