From fcae48753b78045440b874cf255ce13110af336c Mon Sep 17 00:00:00 2001 From: Superredstone Date: Sat, 28 Feb 2026 16:18:50 +0100 Subject: [PATCH] fix: various bugs and general code improvements --- lib/app.go | 10 ++++++---- lib/download.go | 45 +++++++++++++++++++++++++++------------------ lib/songlink.go | 3 +-- lib/tidal.go | 23 +++++++---------------- lib/utils.go | 12 +++++++++++- main.go | 22 ++++++++++++++++------ 6 files changed, 68 insertions(+), 47 deletions(-) diff --git a/lib/app.go b/lib/app.go index 94171ff..738827d 100644 --- a/lib/app.go +++ b/lib/app.go @@ -1,27 +1,29 @@ package lib type App struct { - UserAgent string // User agent used for scraping requests SelectedTidalApiUrl string Verbose bool SpotifyClient *SpotifyClient - ApiInterval int // How many ms to wait between one call to apis and the other + ApiInterval int // How many ms to wait between one call to apis and the other + NoFallback bool } func NewApp() 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", - Verbose: false, + Verbose: false, ApiInterval: 800, + NoFallback: false, } } func (app *App) Init() error { + app.log("Initializing Tidal") err := app.LoadTidalApis() if err != nil { return err } + app.log("Initializing Spotify") if err := app.InitSpotifyClient(); err != nil { return err } diff --git a/lib/download.go b/lib/download.go index 73a727a..ae66f62 100644 --- a/lib/download.go +++ b/lib/download.go @@ -40,11 +40,20 @@ func (app *App) Download(url string, outputFile string, service string, quality switch urlType { case UrlTypeTrack: - outputFileRune := []rune(outputFile) - lastCharacter := string(outputFileRune[len(outputFileRune)-1:]) - downloadInFolder := lastCharacter == "/" + metadata, err := app.GetTrackMetadata(url) + if err != nil { + return err + } - if err := app.DownloadTrack(url, outputFile, service, quality, downloadInFolder); err != nil { + isDir := IsPathDirectory(outputFile) + if outputFile == "" && !isDir { + outputFile, err = BuildFileName(metadata, "flac") + if err != nil { + return err + } + } + + if err := app.DownloadTrack(url, outputFile, service, quality, isDir, metadata); err != nil { return err } @@ -90,7 +99,7 @@ func (app *App) DownloadPlaylist(url string, outputFile string, service string, fmt.Println("[" + strconv.Itoa(idx+1) + "/" + strconv.Itoa(trackListSize) + "] " + metadata.Data.TrackUnion.Name + " - " + artists) - if err := app.DownloadTrack(url, outputFile+"/", service, quality, true); err != nil { + if err := app.DownloadTrack(url, outputFile+"/", service, quality, true, metadata); err != nil { return err } @@ -111,6 +120,11 @@ func (app *App) GetDownloadUrlOrFallback(askedService string, quality string, so break } + // This could have been implemented in a more clear way + if app.NoFallback { + servicesToTry = []string{servicesToTry[0]} + } + var downloadUrl string var lastError error for idx, service := range servicesToTry { @@ -118,19 +132,19 @@ func (app *App) GetDownloadUrlOrFallback(askedService string, quality string, so app.log("Falling back to " + service) } + songId, err := app.GetIdFromSonglink(songlink) + if err != nil { + lastError = err + continue + } + switch service { case "tidal": if songlink.LinksByPlatform.Tidal == nil { continue } - tidalId, err := app.GetTidalIdFromSonglink(songlink) - if err != nil { - lastError = err - continue - } - - downloadUrl, err = app.GetTidalDownloadUrl(tidalId, quality) + downloadUrl, err = app.GetTidalDownloadUrl(songId, quality) if err != nil { lastError = err continue @@ -147,7 +161,7 @@ func (app *App) GetDownloadUrlOrFallback(askedService string, quality string, so return downloadUrl, nil } -func (app *App) DownloadTrack(url string, outputFile string, service string, quality string, downloadInFolder bool) error { +func (app *App) DownloadTrack(url string, outputFile string, service string, quality string, downloadInFolder bool, metadata TrackMetadata) error { songlink, err := app.ConvertSongUrl(url) if err != nil { return err @@ -158,11 +172,6 @@ func (app *App) DownloadTrack(url string, outputFile string, service string, qua return err } - metadata, err := app.GetTrackMetadata(url) - if err != nil { - return err - } - extension, err := GetFormatFromQuality(quality) if err != nil { return err diff --git a/lib/songlink.go b/lib/songlink.go index ea34c70..6fcb40a 100644 --- a/lib/songlink.go +++ b/lib/songlink.go @@ -20,8 +20,7 @@ type SongLinkResponse struct { } type LinksByPlatform struct { - Deezer *LinkByPlatform `json:"deezer,omitempty"` - Tidal *LinkByPlatform `json:"tidal,omitempty"` + Tidal *LinkByPlatform `json:"tidal,omitempty"` } type LinkByPlatform struct { diff --git a/lib/tidal.go b/lib/tidal.go index 994d02e..ec5adfe 100644 --- a/lib/tidal.go +++ b/lib/tidal.go @@ -10,12 +10,10 @@ import ( "strings" ) -var ErrTidalUrlNotFound = errors.New("Tidal URL not found.") - func (app *App) LoadTidalApis() error { var found bool - for _, url := range app.GetAvailableApis() { + for _, url := range app.GetAvailableTidalApis() { res, err := http.Get(url) if err != nil { continue @@ -35,7 +33,7 @@ func (app *App) LoadTidalApis() error { return nil } -func (app *App) GetAvailableApis() []string { +func (app *App) GetAvailableTidalApis() []string { // TODO: Make this load from a JSON file inside of $HOME/.config/spotiflac-cli/apis.json return []string{ "https://triton.squid.wtf", @@ -64,14 +62,7 @@ type TidalAPIResponseV2 struct { 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) + rawResponse, err := http.Get(url) if err != nil { return "", err } @@ -126,10 +117,10 @@ func (app *App) ParseTidalManifestFromBase64(manifestBase64 string) (TidalManife return result, nil } -func (app *App) GetTidalIdFromSonglink(songlink SongLinkResponse) (string, error) { - if songlink.LinksByPlatform.Tidal == nil { - return "", ErrTidalUrlNotFound +func (app *App) GetIdFromSonglink(songlink SongLinkResponse) (string, error) { + if songlink.LinksByPlatform.Tidal != nil { + return ParseTrackId(songlink.LinksByPlatform.Tidal.Url) } - return ParseTrackId(songlink.LinksByPlatform.Tidal.Url) + return "", errors.New("No link found.") } diff --git a/lib/utils.go b/lib/utils.go index 63cc5eb..7fa86b8 100644 --- a/lib/utils.go +++ b/lib/utils.go @@ -137,7 +137,7 @@ func (app *App) InitSpotifyClient() error { } func SpotifyUriToLink(uri string) (string, error) { - spotifyId := strings.Split(uri, ":") + spotifyId := strings.Split(uri, ":") if len(spotifyId) != 3 { return "", errors.New("Invalid URI parsed.") @@ -145,3 +145,13 @@ func SpotifyUriToLink(uri string) (string, error) { return BASE_SPOTIFY_TRACK_URL + spotifyId[2], nil } + +func IsPathDirectory(path string) bool { + pathRune := []rune(path) + if len(pathRune) == 0 { + return false + } + + lastCharacter := string(pathRune[len(pathRune)-1:]) + return lastCharacter == "/" +} diff --git a/main.go b/main.go index 586b667..696ec50 100644 --- a/main.go +++ b/main.go @@ -15,7 +15,10 @@ func main() { service := "" app := lib.NewApp() - app.Init() + err := app.Init() + if err != nil { + log.Fatal(err) + } cmd := &cli.Command{ Name: "spotiflac-cli", @@ -31,22 +34,29 @@ func main() { &cli.StringFlag{ Name: "output", Aliases: []string{"o"}, - Usage: "set output folder", + Usage: "set output folder/file", + DefaultText: outputFolder, Destination: &outputFolder, }, &cli.StringFlag{ Name: "service", Aliases: []string{"s"}, - Usage: "set service to tidal/amazon/qobuz (FFmpeg is required for amazon and qobuz)", + Usage: "set default service (only tidal is supported at the moment)", Destination: &service, }, &cli.IntFlag{ - Name: "interval", - Aliases: []string{"i"}, - Usage: "interval between api requests in milliseconds", + Name: "interval", + Aliases: []string{"i"}, + Usage: "interval between api requests in milliseconds", DefaultText: strconv.Itoa(app.ApiInterval), Destination: &app.ApiInterval, }, + &cli.BoolFlag{ + Name: "no-fallback", + Usage: "do not fallback in case a source is not found", + DefaultText: strconv.FormatBool(app.NoFallback), + Destination: &app.NoFallback, + }, }, Action: func(ctx context.Context, cmd *cli.Command) error { song_url := cmd.Args().First()