diff --git a/lib/app.go b/lib/app.go index ea98230..5d14f44 100644 --- a/lib/app.go +++ b/lib/app.go @@ -1,8 +1,21 @@ package lib type App struct { + UserAgent string // User agent used for scraping requests + SelectedTidalApiUrl string } func NewApp() App { - return App{} + return App{ + UserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36", + } +} + +func (app *App) Init() error { + err := app.LoadTidalApis() + if err != nil { + return err + } + + return nil } diff --git a/lib/download.go b/lib/download.go index 34e521f..e070e10 100644 --- a/lib/download.go +++ b/lib/download.go @@ -39,10 +39,22 @@ func (app *App) Download(url string, outputFolder string, serviceString string) // } // println(metadata.Data.TrackUnion.Id) - _, err := app.ConvertSongUrl(url) + songlink, err := app.ConvertSongUrl(url) if err != nil { return err } + + tidalId, err := app.GetTidalIdFromSonglink(songlink) + if err != nil { + return err + } + + // err = app.DownloadFromTidal(tidalId) + url, err = app.GetTidalDownloadUrl(tidalId, "LOSSLESS") + if err != nil { + return err + } + println(url) } return nil diff --git a/lib/tidal.go b/lib/tidal.go new file mode 100644 index 0000000..fe5e2f0 --- /dev/null +++ b/lib/tidal.go @@ -0,0 +1,148 @@ +package lib + +import ( + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "strings" +) + +func (app *App) LoadTidalApis() error { + var found bool + + for _, url := range app.GetAvailableApis() { + res, err := http.Get(url) + if err != nil { + continue + } + + if res.StatusCode == http.StatusOK { + app.SelectedTidalApiUrl = url + found = true + break + } + } + + if !found { + return errors.New("No available Tidal APIs found.") + } + + return nil +} + +func (app *App) GetAvailableApis() []string { + // TODO: Make this load from a JSON file inside of $HOME/.config/spotiflac-cli/apis.json + return []string{ + "https://triton.squid.wtf", + "https://hifi-one.spotisaver.net", + "https://hifi-two.spotisaver.net", + "https://tidal.kinoplus.online", + "https://tidal-api.binimum.org", + } +} + +func (app *App) DownloadFromTidal(tidalId string) error { + // url, err := app.GetTidalDownloadUrl(tidalId) + // if err != nil { + // return err + // } + // rawResponse, err := http.Get(tidalUrl) + // if err != nil { + // return err + // } + // defer rawResponse.Body.Close() + // + // _, err = io.ReadAll(rawResponse.Body) + // if err != nil { + // return err + // } + + return nil +} + +type TidalAPIResponseV2 struct { + Version string `json:"version"` + Data struct { + TrackID int64 `json:"trackId"` + AssetPresentation string `json:"assetPresentation"` + AudioMode string `json:"audioMode"` + AudioQuality string `json:"audioQuality"` + ManifestMimeType string `json:"manifestMimeType"` + ManifestHash string `json:"manifestHash"` + Manifest string `json:"manifest"` + BitDepth int `json:"bitDepth"` + SampleRate int `json:"sampleRate"` + } `json:"data"` +} + +func (app *App) GetTidalDownloadUrl(tidalId string, quality string) (string, error) { + url := fmt.Sprintf("%s/track/?id=%s&quality=%s", app.SelectedTidalApiUrl, tidalId, quality) + + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return "", err + } + + req.Header.Set("User-Agent", app.UserAgent) + + rawResponse, err := http.DefaultClient.Do(req) + if err != nil { + return "", err + } + defer rawResponse.Body.Close() + + body, err := io.ReadAll(rawResponse.Body) + if err != nil { + return "", err + } + + var response TidalAPIResponseV2 + err = json.Unmarshal(body, &response) + if err != nil { + return "", err + } + + if response.Data.Manifest != "" { + manifest, err := app.ParseTidalManifestFromBase64(response.Data.Manifest) + if err != nil { + return "", err + } + + if len(manifest.Urls) == 0 { + return "", errors.New("No download URL found inside of Tidal APIs manifest.") + } + + return manifest.Urls[0], nil + } + + return "", errors.New("Unimplemented download from API v1.") +} + +type TidalManifest struct { + MimeType string `json:"mimeType"` + Codecs string `json:"codecs"` + Urls []string `json:"urls"` +} + +func (app *App) ParseTidalManifestFromBase64(manifestBase64 string) (TidalManifest, error) { + var result TidalManifest + + manifestDecoded, err := base64.StdEncoding.DecodeString(strings.TrimSpace(manifestBase64)) + if err != nil { + return result, err + } + + err = json.Unmarshal(manifestDecoded, &result) + if err != nil { + return result, err + } + + return result, nil +} + +func (app *App) GetTidalIdFromSonglink(songlink SongLinkResponse) (string, error) { + return ParseTrackId(songlink.LinksByPlatform.Tidal.Url) +} diff --git a/main.go b/main.go index 71acc13..99e97d9 100644 --- a/main.go +++ b/main.go @@ -13,6 +13,7 @@ func main() { var outputFolder, service string app := lib.NewApp() + app.Init() cmd := &cli.Command{ Name: "spotiflac-cli",