Skip to content

Commit

Permalink
Add search functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
tomjowitt committed Feb 14, 2024
1 parent 1ea817d commit e09b809
Show file tree
Hide file tree
Showing 10 changed files with 268 additions and 23 deletions.
21 changes: 20 additions & 1 deletion .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ run:
issues-exit-code: 2
modules-download-mode: readonly
allow-parallel-runners: false
show-stats: true

output:
format: github-actions
Expand Down Expand Up @@ -58,14 +57,28 @@ linters:
- gosimple
- govet
- ineffassign
- interfacebloat
- lll
- misspell
- nakedret
- nilerr
- noctx
- paralleltest
- perfsprint
- predeclared
- revive
- rowserrcheck
- sqlclosecheck
- tenv
- thelper
- tparallel
- unconvert
- unparam
- unused
- varnamelen
- wastedassign
- wrapcheck
- wsl

linters-settings:
depguard:
Expand All @@ -74,3 +87,9 @@ linters-settings:
allow:
- $gostd
- github.com/tomjowitt/gotidal

exhaustive:
check:
- switch
- map
default-signifies-exhaustive: true
23 changes: 22 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,24 @@
# gotidal

A Go library for interacting with the Tidal API.
<p align="center" width="100%">
<img width="33%" src="assets/gotidal.png">
</p>

A Go library for interacting with the TIDAL streaming service API.

## Official Documentation

<https://developer.tidal.com/>

## Installation

```bash
go get -u github.com/tomjowitt/gotidal
```

## Usage

## Credits

Logo created with Gopher Konstructor <https://github.com/quasilyte/gopherkon> based on original artwork
by Renee French.
35 changes: 35 additions & 0 deletions album.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package gotidal

type Album struct {
Resource Resource `json:"resource"`
ID string `json:"id"`
Status int `json:"status"`
Message string `json:"message"`
}

type Resource struct {
ID string `json:"id"`
BarcodeID string `json:"barcodeID"`
Title string `json:"title"`
Artists []Artist `json:"artists"`
Duration int `json:"duration"`
ReleaseDate string `json:"releaseDate"`
ImageCover []Image `json:"imageCover"`
VideoCover []Image `json:"videoCover"`
NumberOfVolumes int `json:"numberOfVolumes"`
NumberOfTracks int `json:"numberOfTracks"`
NumberOfVideos int `json:"numberOfVideos"`
Type string `json:"type"`
Copyright string `json:"copyright"`
MediaMetadata MediaMetaData `json:"mediaMetadata"`
Properties Properties `json:"properties"`
TidalURL string `json:"tidalUrl"`
}

type MediaMetaData struct {
Tags []string `json:"tags"`
}

type Properties struct {
Content []string `json:"content"`
}
8 changes: 8 additions & 0 deletions artist.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package gotidal

type Artist struct {
ID string `json:"id"`
Name string `json:"name"`
Picture []Image `json:"picture"`
Main bool `json:"main"`
}
Binary file added assets/gotidal.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
110 changes: 94 additions & 16 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,17 @@ import (
"fmt"
"io"
"net/http"
"net/url"
"reflect"
"strconv"
"strings"
"unicode"
)

const (
ContentType = "application/vnd.tidal.v1+json"
Environment = "https://openapi.tidal.com"
OauthURI = "https://auth.tidal.com/v1/oauth2/token"
contentType = "application/vnd.tidal.v1+json"
environment = "https://openapi.tidal.com"
oauthURI = "https://auth.tidal.com/v1/oauth2/token"
)

var ErrUnexpectedResponseCode = errors.New("returned an unexpected status code")
Expand All @@ -27,19 +32,20 @@ type Client struct {

func NewClient(clientID string, clientSecret string) (*Client, error) {
ctx := context.Background()

token, err := getAccessToken(ctx, clientID, clientSecret)
if err != nil {
return nil, err
}

return &Client{
ContentType: ContentType,
Environment: Environment,
ContentType: contentType,
Environment: environment,
Token: token,
}, nil
}

type AuthResponse struct {
type authResponse struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
ExpiresIn int `json:"expires_in"`
Expand All @@ -48,35 +54,107 @@ type AuthResponse struct {
func getAccessToken(ctx context.Context, clientID string, clientSecret string) (string, error) {
basicAuth := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", clientID, clientSecret)))

client := http.Client{}
requestBody := []byte(`grant_type=client_credentials`)
req, err := http.NewRequestWithContext(ctx, "POST", OauthURI, bytes.NewBuffer(requestBody))

req, err := http.NewRequestWithContext(ctx, http.MethodPost, oauthURI, bytes.NewBuffer(requestBody))
if err != nil {
return "", fmt.Errorf("failed to create OAuth request: %w", err)
}

req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("Authorization", fmt.Sprintf("Basic %s", basicAuth))

Check failure on line 65 in client.go

View workflow job for this annotation

GitHub Actions / lint

fmt.Sprintf can be replaced with string addition (perfsprint)

responseBody, err := processRequest(req)
if err != nil {
return "", fmt.Errorf("failed to process the request: %w", err)
}

var authResponse authResponse

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

return authResponse.AccessToken, nil
}

func processRequest(req *http.Request) ([]byte, error) {
client := http.Client{}

response, err := client.Do(req)
if err != nil {
return "", fmt.Errorf("failed to process OAuth request: %w", err)
return nil, fmt.Errorf("failed to process request: %w", err)
}
defer response.Body.Close()

if response.StatusCode != http.StatusOK {
return "", fmt.Errorf("%w: %d", ErrUnexpectedResponseCode, response.StatusCode)
if response.StatusCode != http.StatusOK && response.StatusCode != http.StatusMultiStatus {
return nil, fmt.Errorf("%w: %d", ErrUnexpectedResponseCode, response.StatusCode)
}

responseBody, err := io.ReadAll(response.Body)
if err != nil {
return "", fmt.Errorf("failed to read the OAuth response body: %w", err)
return nil, fmt.Errorf("failed to read the OAuth response body: %w", err)
}

var authResponse AuthResponse
err = json.Unmarshal(responseBody, &authResponse)
return responseBody, nil
}

func (c *Client) Request(ctx context.Context, method string, path string, params any) ([]byte, error) {
uri := fmt.Sprintf("%s%s?%s", c.Environment, path, toURLParams(params))

req, err := http.NewRequestWithContext(ctx, method, uri, nil)
if err != nil {
return "", fmt.Errorf("failed to unmarshal the OAuth response body: %w", err)
return nil, fmt.Errorf("failed to create request for %s: %w", uri, err)
}

return authResponse.AccessToken, nil
req.Header.Set("Content-Type", c.ContentType)
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.Token))

Check failure on line 112 in client.go

View workflow job for this annotation

GitHub Actions / lint

fmt.Sprintf can be replaced with string addition (perfsprint)
req.Header.Set("accept", c.ContentType)

return processRequest(req)
}

func toURLParams(s interface{}) string {
var params []string

v := reflect.ValueOf(s)
t := v.Type()

for i := 0; i < v.NumField(); i++ {
field := t.Field(i)
value := v.Field(i)

if value.IsValid() {
var paramValue string

switch value.Kind() {
case reflect.String:
paramValue = value.String()
case reflect.Int:
paramValue = strconv.FormatInt(value.Int(), 10)
default:
continue
}

if paramValue != "" && paramValue != "0" {
paramName := url.QueryEscape(lowercaseFirstLetter(field.Name))
paramValue = url.QueryEscape(paramValue)
params = append(params, fmt.Sprintf("%s=%s", paramName, paramValue))
}
}
}

return strings.Join(params, "&")
}

func lowercaseFirstLetter(str string) string {
if len(str) == 0 {
return str
}

firstChar := []rune(str)[0]
lowerFirstChar := unicode.ToLower(firstChar)

return string(lowerFirstChar) + str[1:]
}
21 changes: 18 additions & 3 deletions examples/search/main.go
Original file line number Diff line number Diff line change
@@ -1,20 +1,35 @@
package main

import (
"context"
"log"
"os"

"github.com/tomjowitt/gotidal"
)

func main() {
clientId := os.Getenv("TIDAL_CLIENT_ID")
ctx := context.Background()

clientID := os.Getenv("TIDAL_CLIENT_ID")
clientSecret := os.Getenv("TIDAL_CLIENT_SECRET")

client, err := gotidal.NewClient(clientId, clientSecret)
client, err := gotidal.NewClient(clientID, clientSecret)
if err != nil {
log.Fatal(err)
}

params := gotidal.SearchParams{
Query: "Black Flag",
CountryCode: "AU",
}

results, err := client.Search(ctx, params)
if err != nil {
log.Fatal(err)
}

client.Search("searchQuery")
for _, album := range results.Albums {
log.Printf("%s - %s", album.Resource.Title, album.Resource.Artists[0].Name)
}
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module github.com/tomjowitt/gotidal

go 1.21.6
go 1.22.0
7 changes: 7 additions & 0 deletions image.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package gotidal

type Image struct {
URL string `json:"url"`
Width int `json:"width"`
Height int `json:"height"`
}
Loading

0 comments on commit e09b809

Please sign in to comment.