diff --git a/go.mod b/go.mod index 45d6f01..4c1382a 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,9 @@ module music-downloader go 1.25.0 + +require ( + github.com/tetratelabs/wazero v1.11.0 // indirect + go.senan.xyz/taglib v0.11.1 // indirect + golang.org/x/sys v0.41.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..904fd24 --- /dev/null +++ b/go.sum @@ -0,0 +1,6 @@ +github.com/tetratelabs/wazero v1.11.0 h1:+gKemEuKCTevU4d7ZTzlsvgd1uaToIDtlQlmNbwqYhA= +github.com/tetratelabs/wazero v1.11.0/go.mod h1:eV28rsN8Q+xwjogd7f4/Pp4xFxO7uOGbLcD/LzB1wiU= +go.senan.xyz/taglib v0.11.1 h1:S3mO5e3HRRG0Ehw1jLUodYbAJK8TtqdOoNgqkC0D3uU= +go.senan.xyz/taglib v0.11.1/go.mod h1:qyTl978MnGeZ/ny4d/t0ErLXxysA+39X4+SNSCk56Zs= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= diff --git a/main.go b/main.go index bbea59b..60dc262 100644 --- a/main.go +++ b/main.go @@ -11,6 +11,8 @@ import ( "path" "strconv" "sync" + + "go.senan.xyz/taglib" ) func sendJSON(w http.ResponseWriter, data any, status int) { @@ -58,7 +60,12 @@ func main() { return } - if err := os.Mkdir("./music/"+album.Title, 0777); err != nil { + albumCoverImg, err := mClient.AlbumCoverImage(album.CoverID) + if err != nil { + log.Printf("failed to download album cover: %v\n", err) + } + + if err := os.Mkdir("./Music/"+album.Title, 0777); err != nil { http.Error(w, fmt.Sprintf("failed to create album directory: %v", err), 500) return } @@ -107,11 +114,33 @@ func main() { return } - if err := os.WriteFile(path.Join("./music/", album.Title, track.Item.Title+".flac"), data, 0644); err != nil { + fPath := path.Join("./Music/", album.Title, track.Item.Title+".flac") + if err := os.WriteFile(fPath, data, 0644); err != nil { log.Printf("failed to save track file: %v\n", err) return } + artists := []string{} + for _, artist := range album.Artists { + artists = append(artists, artist.Name) + } + + if err := taglib.WriteTags(fPath, map[string][]string{ + taglib.Artist: {album.Artist.Name}, + taglib.Artists: artists, + taglib.AlbumArtist: artists, + taglib.TrackNumber: {strconv.Itoa(track.Item.TrackNumber)}, + taglib.Album: {album.Title}, + taglib.Title: {track.Item.Title}, + taglib.ReleaseDate: {album.ReleaseDate}, + }, 0); err != nil { + log.Printf("failed to add metadata tags to track file: %v\n", err) + } + + if err := taglib.WriteImage(fPath, albumCoverImg); err != nil { + log.Printf("failed to add album cover to track metadata: %v\n", err) + } + response.Lock() response.count++ response.Unlock() diff --git a/monochrome/album.go b/monochrome/album.go index 59dca65..ad02f41 100644 --- a/monochrome/album.go +++ b/monochrome/album.go @@ -1,13 +1,21 @@ package monochrome import ( + "bytes" "encoding/json" "fmt" + "io" "net/http" - "net/url" "strconv" + "strings" ) +type AlbumArtist struct { + ID int `json:"id"` + Name string `json:"name"` + PictureID string `json:"picture"` +} + type AlbumTrack struct { Type string `json:"type"` Item struct { @@ -30,6 +38,8 @@ type AlbumInfo struct { CoverID string `json:"cover"` VibrantColor string `json:"vibrantColor"` Explicit bool `json:"explicit"` + Artist AlbumArtist `json:"artist"` + Artists []AlbumArtist `json:"artists"` Items []AlbumTrack `json:"items"` } @@ -38,25 +48,42 @@ type AlbumInfoResponse struct { } func (c *Client) AlbumInfo(id int) (*AlbumInfo, error) { - req, err := http.NewRequest("GET", c.config.ApiURL+"/album", nil) + req, err := c.makeRequest("GET", "/album", nil, map[string]string{ + "id": strconv.Itoa(id), + }) if err != nil { return nil, fmt.Errorf("failed to create request: %v", err) } - params := url.Values{} - params.Set("id", strconv.Itoa(id)) - req.URL.RawQuery = params.Encode() - - resp, err := http.DefaultClient.Do(req) + data, err := c.do(req) if err != nil { return nil, fmt.Errorf("failed to send request: %v", err) } - defer resp.Body.Close() response := AlbumInfoResponse{} - if err := json.NewDecoder(resp.Body).Decode(&response); err != nil { + if err := json.NewDecoder(bytes.NewReader(data)).Decode(&response); err != nil { return nil, fmt.Errorf("failed to decode json response: %v", err) } return &response.Data, nil } + +func (c *Client) AlbumCoverImage(coverId string) ([]byte, error) { + coverUrl := fmt.Sprintf("%s/%s/640x640.jpg", "https://resources.tidal.com/images", strings.ReplaceAll(coverId, "-", "/")) + resp, err := http.Get(coverUrl) + if err != nil { + return nil, fmt.Errorf("failed to send request: %v", err) + } + defer resp.Body.Close() + + if resp.Header.Get("Content-Type") != "image/jpeg" { + return nil, fmt.Errorf("invalid response type: got %s", resp.Header.Get("Content-Type")) + } + + data, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response: %v", err) + } + + return data, nil +} diff --git a/monochrome/client.go b/monochrome/client.go index 44e4d14..4d5cec6 100644 --- a/monochrome/client.go +++ b/monochrome/client.go @@ -1,5 +1,12 @@ package monochrome +import ( + "fmt" + "io" + "net/http" + "net/url" +) + type ClientConfig struct { ApiURL string } @@ -13,3 +20,41 @@ func NewClient(config ClientConfig) *Client { config: config, } } + +func (c *Client) makeRequest(method string, path string, body io.Reader, params map[string]string) (*http.Request, error) { + req, err := http.NewRequest(method, c.config.ApiURL+path, body) + if err != nil { + return nil, fmt.Errorf("failed to create request: %v", err) + } + + p := url.Values{} + for key, value := range params { + p.Add(key, value) + } + req.URL.RawQuery = p.Encode() + + return req, nil +} + +func (c *Client) do(req *http.Request) ([]byte, error) { + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to send request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode == http.StatusTooManyRequests { + return nil, fmt.Errorf("rate limited") + } + + data, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response: %v", err) + } + + if resp.StatusCode >= 400 { + return data, fmt.Errorf("got error from api, data is filled with response") + } + + return data, nil +} diff --git a/monochrome/search.go b/monochrome/search.go index eda284a..4212d5d 100644 --- a/monochrome/search.go +++ b/monochrome/search.go @@ -1,10 +1,9 @@ package monochrome import ( + "bytes" "encoding/json" "fmt" - "net/http" - "net/url" ) type SearchItemType = string @@ -44,23 +43,20 @@ type SearchResponse struct { } func (c *Client) SearchAlbum(q string) ([]SearchAlbum, error) { - req, err := http.NewRequest("GET", c.config.ApiURL+"/search", nil) + req, err := c.makeRequest("GET", "/search", nil, map[string]string{ + "al": q, + }) if err != nil { return nil, fmt.Errorf("failed to create request: %v", err) } - params := url.Values{} - params.Set("al", q) - req.URL.RawQuery = params.Encode() - - resp, err := http.DefaultClient.Do(req) + data, err := c.do(req) if err != nil { return nil, fmt.Errorf("failed to send request: %v", err) } - defer resp.Body.Close() response := SearchResponse{} - if err := json.NewDecoder(resp.Body).Decode(&response); err != nil { + if err := json.NewDecoder(bytes.NewReader(data)).Decode(&response); err != nil { return nil, fmt.Errorf("failed to decode json response: %v", err) } diff --git a/monochrome/tracks.go b/monochrome/tracks.go index 7abae79..acecc80 100644 --- a/monochrome/tracks.go +++ b/monochrome/tracks.go @@ -5,8 +5,6 @@ import ( "encoding/base64" "encoding/json" "fmt" - "net/http" - "net/url" "strconv" ) @@ -20,24 +18,21 @@ type TrackInfoResponse struct { } func (c *Client) TrackInfo(id int, quality AudioQuality) (*TrackInfo, error) { - req, err := http.NewRequest("GET", c.config.ApiURL+"/track", nil) + req, err := c.makeRequest("GET", "/track", nil, map[string]string{ + "id": strconv.Itoa(id), + "quality": quality, + }) if err != nil { return nil, fmt.Errorf("failed to create request: %v", err) } - params := url.Values{} - params.Set("id", strconv.Itoa(id)) - params.Set("quality", quality) - req.URL.RawQuery = params.Encode() - - resp, err := http.DefaultClient.Do(req) + data, err := c.do(req) if err != nil { return nil, fmt.Errorf("failed to send request: %v", err) } - defer resp.Body.Close() response := TrackInfoResponse{} - if err := json.NewDecoder(resp.Body).Decode(&response); err != nil { + if err := json.NewDecoder(bytes.NewReader(data)).Decode(&response); err != nil { return nil, fmt.Errorf("failed to decode json response: %v", err) }