Skip to content

Commit

Permalink
Merge pull request #26 from tomjowitt/9-get-album-items
Browse files Browse the repository at this point in the history
Add ability to retrieve album tracks
  • Loading branch information
tomjowitt authored Feb 24, 2024
2 parents 4d9ef33 + b5585ce commit 9c787f9
Show file tree
Hide file tree
Showing 11 changed files with 1,510 additions and 16 deletions.
11 changes: 11 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,19 @@ linters-settings:
- map
default-signifies-exhaustive: true

gomnd:
ignored-files:
- "examples/*.go"

varnamelen:
ignore-names:
- err
- id
- tt

issues:
exclude-rules:
- path: _test.go
linters:
- cyclop
- funlen
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ An unofficial Go library for interacting with the TIDAL API.
[![Go Reference](https://pkg.go.dev/badge/badge/.svg)](https://pkg.go.dev/github.com/tomjowitt/gotidal)
![GitHub License](https://img.shields.io/github/license/tomjowitt/gotidal)
![GitHub Tag](https://img.shields.io/github/v/tag/tomjowitt/gotidal)
![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/tomjowitt/gotidal/test.yml?label=tests)
![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/tomjowitt/gotidal/lint.yml?label=lint)

## Guidelines

Expand Down
86 changes: 81 additions & 5 deletions album.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ type albumResource struct {
MediaMetaData MediaMetaData `json:"mediaMetadata"`
Properties AlbumProperties `json:"properties"`
TidalURL string `json:"tidalUrl"`
ProviderInfo ProviderInfo `json:"providerInfo"`
}

type MediaMetaData struct {
Expand All @@ -51,17 +52,29 @@ type Track struct {
}

type trackResource struct {
ID string `json:"id"`
Title string `json:"title"`
Version string `json:"version"`
Artists []Artist `json:"artists"`
Album albumResource `json:"album"`
ID string `json:"id"`
Title string `json:"title"`
ISRC string `json:"isrc"`
Copyright string `json:"copyright"`
Version string `json:"version"`
Artists []Artist `json:"artists"`
Album albumResource `json:"album"`
TrackNumber int `json:"trackNumber"`
VolumeNumber int `json:"volumeNumber"`
MediaMetaData MediaMetaData `json:"mediaMetadata"`
Properties AlbumProperties `json:"properties"`
TidalURL string `json:"tidalUrl"`
ProviderInfo ProviderInfo `json:"providerInfo"`
}

type albumResults struct {
Data []Album `json:"data"`
}

type ItemMetaData struct {
Total int `json:"total"`
}

// GetSingleAlbum returns an album that matches an ID.
func (c *Client) GetSingleAlbum(ctx context.Context, id string) (*Album, error) {
if id == "" {
Expand All @@ -83,6 +96,69 @@ func (c *Client) GetSingleAlbum(ctx context.Context, id string) (*Album, error)
return &result, nil
}

// itemsParams defines the request parameters used by the album items API endpoint.
type itemsParams struct {
// Pagination offset (in number of items).
// Example: 0
Offset int `json:"offset"`

// Page size.
// Example: 10
Limit int `json:"limit"`
}

type trackResults struct {
Data []Track `json:"data"`
MetaData ItemMetaData `json:"metadata"`
}

const paginationLimit = 100

// GetAlbumTracks returns a list of album tracks.
//
// The items endpoint is paginated so we set a fairly high limit (100 items) in the hope to catch most cases in
// one round-trip. If the metadata reports a higher total then we make susequent API calls until all the tracks are
// returned.
//
// This endpoint also supports videos but it was hard to find any examples of this.
func (c *Client) GetAlbumTracks(ctx context.Context, id string) ([]Track, error) {
if id == "" {
return nil, ErrMissingRequiredParameters
}

params := itemsParams{
Limit: paginationLimit,
Offset: 0,
}

total := 0
runningTotal := 0

var tracks []Track

for total >= runningTotal {
response, err := c.request(ctx, http.MethodGet, concat("/albums/", id, "/items"), params)
if err != nil {
return nil, fmt.Errorf("failed to connect to the albums endpoint: %w", err)
}

var results trackResults

err = json.Unmarshal(response, &results)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal the albums response body: %w", err)
}

tracks = append(tracks, results.Data...)

params.Offset += params.Limit
runningTotal += params.Limit
total = results.MetaData.Total
}

return tracks, nil
}

// GetAlbumByBarcodeID returns a list of albums that match a barcode ID.
func (c *Client) GetAlbumByBarcodeID(ctx context.Context, barcodeID string) ([]Album, error) {
if barcodeID == "" {
Expand Down
237 changes: 237 additions & 0 deletions album_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
package gotidal

import (
"bytes"
"context"
"fmt"
"io"
"net/http"
"os"
"reflect"
"testing"
)

type mockHTTPClient struct {
FilePath string
StatusCode int
}

func (c *mockHTTPClient) Do(req *http.Request) (*http.Response, error) { // nolint:revive // req is unused
data, err := os.ReadFile(c.FilePath)
if err != nil {
return nil, fmt.Errorf("could not load payload file: %w", err)
}

buffer := bytes.NewBuffer(data)
readCloser := io.NopCloser(buffer)

return &http.Response{
StatusCode: c.StatusCode,
Body: readCloser,
}, nil
}

func TestGetSingleAlbum(t *testing.T) {
t.Parallel()

type args struct {
httpClient HTTPClient
ID string
}

type expected struct {
ID string
Title string
ArtistCount int
ArtistID string
ArtistName string
ArtistPictureCount int
IsMain bool
Duration int
ReleaseDate string
CoverImageCount int
VideoCoverCount int
Volumes int
Tracks int
Videos int
Type string
Copyright string
MetadataTags []string
TidalURL string
}

tests := []struct {
name string
args args
expected expected
wantErr bool
}{
{
"Single album parses correctly",
args{
httpClient: &mockHTTPClient{FilePath: "testdata/single-album.json", StatusCode: http.StatusOK},
ID: "51584178",
},
expected{
ID: "51584178",
Title: "Power Corruption and Lies",
ArtistCount: 1,
ArtistID: "11950",
ArtistName: "New Order",
ArtistPictureCount: 10,
IsMain: true,
Duration: 2555,
ReleaseDate: "1983-01-01",
CoverImageCount: 7,
VideoCoverCount: 0,
Volumes: 1,
Tracks: 8,
Videos: 0,
Type: "ALBUM",
Copyright: "© 2015 Warner Records 90 Ltd",
MetadataTags: []string{"LOSSLESS", "MQA"},
TidalURL: "https://tidal.com/browse/album/51584178",
},
false,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

client := &Client{httpClient: tt.args.httpClient}

album, err := client.GetSingleAlbum(context.Background(), tt.args.ID)
if (err != nil) != tt.wantErr {
t.Errorf("Client.GetSingleAlbum() error = %v, wantErr %v", err, tt.wantErr)
return
}

if album.ID != tt.expected.ID {
t.Errorf("Album ID error. Want %v, Got %v", tt.expected.ID, album.ID)
}

if album.Title != tt.expected.Title {
t.Errorf("Album Title error. Want %v, Got %v", tt.expected.Title, album.Title)
}

if len(album.Artists) != tt.expected.ArtistCount {
t.Errorf("Album Artists error. Want %v, Got %v", tt.expected.ArtistCount, len(album.Artists))
}

if album.Artists[0].ID != tt.expected.ArtistID {
t.Errorf("Album Artist ID error. Want %v, Got %v", tt.expected.ArtistID, album.Artists[0].ID)
}

if album.Artists[0].Name != tt.expected.ArtistName {
t.Errorf("Album Artist Name error. Want %v, Got %v", tt.expected.ArtistName, album.Artists[0].Name)
}

picCount := len(album.Artists[0].Picture)
if picCount != tt.expected.ArtistPictureCount {
t.Errorf("Album Artist Picture error. Want %v, Got %v", tt.expected.ArtistPictureCount, picCount)
}

if album.Artists[0].Main != tt.expected.IsMain {
t.Errorf("Album Artist Main error. Want %v, Got %v", tt.expected.IsMain, album.Artists[0].Main)
}

if album.Duration != tt.expected.Duration {
t.Errorf("Album Duration error. Want %v, Got %v", tt.expected.Duration, album.Duration)
}

if album.ReleaseDate != tt.expected.ReleaseDate {
t.Errorf("Album ReleaseDate error. Want %v, Got %v", tt.expected.ReleaseDate, album.ReleaseDate)
}

coverCount := len(album.ImageCover)
if coverCount != tt.expected.CoverImageCount {
t.Errorf("Album Cover error. Want %v, Got %v", tt.expected.CoverImageCount, coverCount)
}

videoCount := len(album.VideoCover)
if videoCount != tt.expected.VideoCoverCount {
t.Errorf("Album Video Cover error. Want %v, Got %v", tt.expected.VideoCoverCount, videoCount)
}

if album.NumberOfVolumes != tt.expected.Volumes {
t.Errorf("Album Volumes error. Want %v, Got %v", tt.expected.Volumes, album.NumberOfVolumes)
}

if album.NumberOfTracks != tt.expected.Tracks {
t.Errorf("Album Tracks error. Want %v, Got %v", tt.expected.Tracks, album.NumberOfTracks)
}

if album.NumberOfVideos != tt.expected.Videos {
t.Errorf("Album Videos error. Want %v, Got %v", tt.expected.Videos, album.NumberOfVideos)
}

if album.Type != tt.expected.Type {
t.Errorf("Album Type error. Want %v, Got %v", tt.expected.Type, album.Type)
}

if album.Copyright != tt.expected.Copyright {
t.Errorf("Album Copyright error. Want %v, Got %v", tt.expected.Copyright, album.Copyright)
}

if !reflect.DeepEqual(album.MediaMetaData.Tags, tt.expected.MetadataTags) {
t.Errorf("Album Metadata error. Want %v, Got %v", tt.expected.MetadataTags, album.MediaMetaData.Tags)
}

if album.TidalURL != tt.expected.TidalURL {
t.Errorf("Album TidalURL error. Want %v, Got %v", tt.expected.TidalURL, album.TidalURL)
}
})
}
}

func TestGetAlbumTracks(t *testing.T) {
t.Parallel()

type args struct {
httpClient HTTPClient
id string
}

type expected struct {
trackCount int
}

tests := []struct {
name string
args args
expected expected
}{
{
"Count of album tracks",
args{
httpClient: &mockHTTPClient{FilePath: "testdata/album-items.json", StatusCode: http.StatusOK},
id: "51584178",
},
expected{
trackCount: 8,
},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

c := &Client{
httpClient: tt.args.httpClient,
}

tracks, err := c.GetAlbumTracks(context.Background(), tt.args.id)
if err != nil {
t.Errorf("Client.GetAlbumTracks() error = %v", err)
return
}

if len(tracks) != tt.expected.trackCount {
t.Errorf("Client.GetAlbumTracks() track count = %v, want %v", len(tracks), tt.expected.trackCount)
}
})
}
}
Loading

0 comments on commit 9c787f9

Please sign in to comment.