25 Commits
v0.1 ... v0.2.1

Author SHA1 Message Date
Artem Piskun
77c264c993 DSCGS-31 Search fix (#32) 2019-10-29 13:34:43 +03:00
Abe Massry
84a3ecd4c5 add cover_image to search results (#30) 2019-09-02 18:15:44 +03:00
Abe Massry
f9bfcf8fba Update search example for current code behavior (#29) 2019-08-28 12:53:11 +03:00
irlndts
664896b3dd Golangci-lint fix 2019-06-18 15:08:55 +03:00
Artem Piskun
3c48a3e6d4 Merge pull request #28 from irlndts/RemoveVendor 2019-06-06 15:24:00 +03:00
irlndts
271bbe8b83 Travis version updated 2019-06-06 15:20:17 +03:00
irlndts
35dfbe02d1 Vendor removed, some cleanups 2019-06-06 15:18:10 +03:00
irlndts
618b882634 Use go mod 2018-12-11 16:47:31 +03:00
Artem Piskun
a4b6e4193a Merge pull request #27 from pluyckx/master
Add 'Artists' field to 'Track'
2018-04-30 21:27:27 +03:00
Philip Luyckx
e1e0446c29 Add 'Artists' field to 'Track'
Some tracks contain different artists than specified in the release. In
this case there is a field 'artists' in the JSON response. This field is
added to the 'Track' struct. When the field 'artists' is available in the
JSON response, the length of 'Artists' is not 0. Otherwise the 'Artists'
from the release should be used.

Signed-off-by: Philip Luyckx <philip.luyckx@gmail.com>
2018-04-30 10:37:04 +02:00
irlndts
dd501a3f65 fix 2018-04-10 15:39:54 +03:00
irlndts
110a37661a goreportcard 2018-03-28 20:18:24 +03:00
irlndts
3775bc39c7 travis pic 2018-03-28 20:13:31 +03:00
irlndts
e9ad282056 added travis-ci.yml 2018-03-28 20:11:47 +03:00
Artem Piskun
31a2e3b959 Merge pull request #26 from irlndts/APIErrors 2018-03-24 16:19:05 +03:00
irlndts
8e69f9334b Added several tests 2018-03-24 16:17:35 +03:00
irlndts
788518e3e2 Added several tests 2018-03-24 16:14:54 +03:00
irlndts
0a3a17b69d Lint errors fixed 2018-03-24 13:46:20 +03:00
Artem Piskun
ebfd052f67 Merge pull request #24 from irlndts/DSCGS-23
Move services to database
2018-03-21 21:18:54 +03:00
irlndts
c526ccd692 Move services to database 2018-03-21 21:17:40 +03:00
Artem Piskun
91d056b186 Merge pull request #22 from irlndts/DSCGS-20
Fix govet errors
2018-03-21 20:58:14 +03:00
irlndts
441de35813 Fix govet errors 2018-03-21 20:57:39 +03:00
Artem Piskun
872ff94673 Merge pull request #21 from irlndts/DSCGS-19
DSCGS-19 fix lint
2018-03-20 13:57:39 +03:00
irlndts
afd778f11c Cleanup 2018-03-20 13:56:17 +03:00
irlndts
dd29f70850 DSCGS-19 Fix golint errors 2018-03-17 23:12:25 +03:00
20 changed files with 410 additions and 1048 deletions

3
.travis.yml Normal file
View File

@@ -0,0 +1,3 @@
language: go
go:
- "1.12.5"

View File

@@ -1,17 +1,20 @@
# REST API 2.0 Discogs.com client
[![Build Status](https://travis-ci.org/irlndts/go-discogs.svg?branch=master)](https://travis-ci.org/irlndts/go-discogs)[![Go Report Card](https://goreportcard.com/badge/github.com/irlndts/go-discogs)](https://goreportcard.com/report/github.com/irlndts/go-discogs)
go-discogs is a Go client library for the [Discogs API](https://www.discogs.com/developers/). Check the usage section to see how to access the Discogs API.
### Feauteres
* Database
* [Releases](#releases)
* Release Rating
* Master Releases
* Release Versions
* Master Versions
* Artists
* Artist Releases
* Label
* All Label Releases
* [Search](#search)
* [Search](#search)
Install
--------
@@ -37,7 +40,7 @@ client, err := discogs.NewClient(&discogs.Options{
#### Releases
```go
release, _ := client.Release.Release(9893847)
release, _ := client.Database.Release(9893847)
fmt.Println(release.Artists[0].Name, " - ", release.Title)
// St. Petersburg Ska-Jazz Review - Elephant Riddim
```
@@ -76,7 +79,7 @@ type SearchRequest struct {
Example
```go
request := discogs.SearchRequest{Artist: "reggaenauts", ReleaseTitle: "river rock", Page: 0, PerPage: 1}
search, _ := client.Search(request)
search, _ := client.Search.Search(request)
for _, r := range search.Results {
fmt.Println(r.Title)

View File

@@ -1,52 +0,0 @@
package discogs
import "strconv"
// ArtistService ...
type ArtistService struct {
url string
}
func newArtistService(url string) *ArtistService {
return &ArtistService{
url: url,
}
}
// Artist ...
type Artist struct {
Namevariations []string `json:"namevariations"`
Profile string `json:"profile"`
Releases_url string `json:"releases_url"`
Resource_url string `json:"resource_url"`
Uri string `json:"uri"`
Urls []string `json:"urls"`
Data_quality string `json:"data_quality"`
Id int `json:"id"`
Images []Image `json:"images"`
Members []Member `json:"members"`
}
// Artist represents a person in the discogs database
func (s *ArtistService) Artist(artistID int) (*Artist, error) {
var artist *Artist
if err := request(s.url+strconv.Itoa(artistID), nil, &artist); err != nil {
return nil, err
}
return artist, nil
}
// ArtistReleases ...
type ArtistReleases struct {
Pagination Page `json:"pagination"`
Releases []ReleaseSource `json:"releases"`
}
// Releases returns a list of releases and masters associated with the artist.
func (s *ArtistService) Releases(artistID int, pagination *Pagination) (*ArtistReleases, error) {
var releases *ArtistReleases
if err := request(s.url+strconv.Itoa(artistID)+"/releases", pagination.toParams(), &releases); err != nil {
return nil, err
}
return releases, nil
}

196
database.go Normal file
View File

@@ -0,0 +1,196 @@
package discogs
import (
"net/url"
"strconv"
)
const (
releasesURI = "/releases/"
artistsURI = "/artists/"
labelsURI = "/labels/"
mastersURI = "/masters/"
)
// DatabaseService ...
type DatabaseService struct {
url string
currency string
}
func newDatabaseService(url string, currency string) *DatabaseService {
return &DatabaseService{
url: url,
currency: currency,
}
}
// Release serves relesase response from discogs
type Release struct {
Title string `json:"title"`
ID int `json:"id"`
Artists []ArtistSource `json:"artists"`
DataQuality string `json:"data_quality"`
Thumb string `json:"thumb"`
Community Community `json:"community"`
Companies []Company `json:"companies"`
Country string `json:"country"`
DateAdded string `json:"date_added"`
DateChanged string `json:"date_changed"`
EstimatedWeight int `json:"estimated_weight"`
ExtraArtists []ArtistSource `json:"extraartists"`
FormatQuantity int `json:"format_quantity"`
Formats []Format `json:"formats"`
Genres []string `json:"genres"`
Identifiers []Identifier `json:"identifiers"`
Images []Image `json:"images"`
Labels []LabelSource `json:"labels"`
LowestPrice float64 `json:"lowest_price"`
MasterID int `json:"master_id"`
MasterURL string `json:"master_url"`
Notes string `json:"notes,omitempty"`
NumForSale int `json:"num_for_sale,omitempty"`
Released string `json:"released"`
ReleasedFormatted string `json:"released_formatted"`
ResourceURL string `json:"resource_url"`
// Series
Status string `json:"status"`
Styles []string `json:"styles"`
Tracklist []Track `json:"tracklist"`
URI string `json:"uri"`
Videos []Video `json:"videos"`
Year int `json:"year"`
}
// Release returns release by release's ID
func (s *DatabaseService) Release(releaseID int) (*Release, error) {
params := url.Values{}
params.Set("curr_abbr", s.currency)
var release *Release
err := request(s.url+releasesURI+strconv.Itoa(releaseID), params, &release)
return release, err
}
// ReleaseRating serves response for community release rating request
type ReleaseRating struct {
ID int `json:"release_id"`
Rating Rating `json:"rating"`
}
// ReleaseRating retruns community release rating
func (s *DatabaseService) ReleaseRating(releaseID int) (*ReleaseRating, error) {
var rating *ReleaseRating
err := request(s.url+releasesURI+strconv.Itoa(releaseID)+"/rating", nil, &rating)
return rating, err
}
// Artist ...
type Artist struct {
Namevariations []string `json:"namevariations"`
Profile string `json:"profile"`
ReleasesURL string `json:"releases_url"`
ResourceURL string `json:"resource_url"`
URI string `json:"uri"`
URLs []string `json:"urls"`
DataQuality string `json:"data_quality"`
ID int `json:"id"`
Images []Image `json:"images"`
Members []Member `json:"members"`
}
// Artist represents a person in the discogs database
func (s *DatabaseService) Artist(artistID int) (*Artist, error) {
var artist *Artist
err := request(s.url+artistsURI+strconv.Itoa(artistID), nil, &artist)
return artist, err
}
// ArtistReleases ...
type ArtistReleases struct {
Pagination Page `json:"pagination"`
Releases []ReleaseSource `json:"releases"`
}
// ArtistReleases returns a list of releases and masters associated with the artist.
func (s *DatabaseService) ArtistReleases(artistID int, pagination *Pagination) (*ArtistReleases, error) {
var releases *ArtistReleases
err := request(s.url+artistsURI+strconv.Itoa(artistID)+"/releases", pagination.params(), &releases)
return releases, err
}
// Label resource represents a label, company, recording studio, location,
// or other entity involved with artists and releases.
type Label struct {
Profile string `json:"profile"`
ReleasesURL string `json:"releases_url"`
Name string `json:"name"`
ContactInfo string `json:"contact_info"`
URI string `json:"uri"`
Sublabels []Sublable `json:"sublabels"`
URLs []string `json:"urls"`
Images []Image `json:"images"`
ResourceURL string `json:"resource_url"`
ID int `json:"id"`
DataQuality string `json:"data_quality"`
}
// Label returns a label.
func (s *DatabaseService) Label(labelID int) (*Label, error) {
var label *Label
err := request(s.url+labelsURI+strconv.Itoa(labelID), nil, &label)
return label, err
}
// LabelReleases is a list of Releases associated with the label.
type LabelReleases struct {
Pagination Page `json:"pagination"`
Releases []ReleaseSource `json:"releases"`
}
// LabelReleases returns a list of Releases associated with the label.
func (s *DatabaseService) LabelReleases(labelID int, pagination *Pagination) (*LabelReleases, error) {
var releases *LabelReleases
err := request(s.url+labelsURI+strconv.Itoa(labelID)+"/releases", pagination.params(), &releases)
return releases, err
}
// Master resource represents a set of similar releases.
// Masters (also known as `master releases`) have a `main release` which is often the chronologically earliest.
type Master struct {
Styles []string `json:"styles"`
Genres []string `json:"genres"`
Videos []Video `json:"videos"`
Title string `json:"title"`
MainRelease int `json:"main_release"`
MainReleaseURL string `json:"main_release_url"`
URI string `json:"uri"`
Artists []Artist `json:"artists"`
VersionURL string `json:"version_url"`
Year int `json:"year"`
Images []Image `json:"images"`
ResourceURL string `json:"resource_url"`
Tracklist []Track `json:"tracklist"`
ID int `json:"id"`
DataQuality string `json:"data_quality"`
}
// Master returns a master release
func (s *DatabaseService) Master(masterID int) (*Master, error) {
var master *Master
err := request(s.url+mastersURI+strconv.Itoa(masterID), nil, &master)
return master, err
}
// MasterVersions retrieves a list of all releases that are versions of this master.
type MasterVersions struct {
Pagination Page `json:"pagination"`
Versions []Version `json:"versions"`
}
// MasterVersions retrieves a list of all Releases that are versions of this master
func (s *DatabaseService) MasterVersions(masterID int, pagination *Pagination) (*MasterVersions, error) {
var versions *MasterVersions
err := request(s.url+mastersURI+strconv.Itoa(masterID)+"/versions", pagination.params(), &versions)
return versions, err
}

View File

@@ -9,17 +9,18 @@ import (
func ReleaseServer(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
io.WriteString(w, `{"title":"Elephant Riddim"}`)
if _, err := io.WriteString(w, `{"title":"Elephant Riddim"}`); err != nil {
panic(err)
}
}
func TestReleaseServiceRelease(t *testing.T) {
expectedTitle := "Elephant Riddim"
ts := httptest.NewServer(http.HandlerFunc(ReleaseServer))
defer ts.Close()
expectedTitle := "Elephant Riddim"
d := initDiscogsClient(t, &Options{URL: ts.URL})
release, err := d.Release.Release(8138518)
release, err := d.Database.Release(8138518)
if err != nil {
t.Fatalf("failed to get release: %s", err)
}

View File

@@ -9,9 +9,10 @@ import (
)
const (
discogsAPI = "https://api.discogs.com/"
discogsAPI = "https://api.discogs.com"
)
// Options is a set of options to use discogs API client
type Options struct {
URL string
Currency string
@@ -21,11 +22,8 @@ type Options struct {
// Client is a Discogs client for making Discogs API requests.
type Client struct {
Release *ReleaseService
Master *MasterService
Artist *ArtistService
Label *LabelService
Search *SearchService
Database *DatabaseService
Search *SearchService
}
var header *http.Header
@@ -35,7 +33,7 @@ func NewClient(o *Options) (*Client, error) {
header = &http.Header{}
if o == nil || o.UserAgent == "" {
return nil, fmt.Errorf("failed to set user-agent")
return nil, ErrUserAgentInvalid
}
header.Add("User-Agent", o.UserAgent)
@@ -55,11 +53,8 @@ func NewClient(o *Options) (*Client, error) {
}
return &Client{
Release: newReleaseService(o.URL+"releases/", cur),
Artist: newArtistService(o.URL + "artists/"),
Label: newLabelService(o.URL + "labels/"),
Master: newMasterService(o.URL + "masters/"),
Search: newSearchService(o.URL + "database/search"),
Database: newDatabaseService(o.URL, cur),
Search: newSearchService(o.URL + "/database/search"),
}, nil
}
@@ -73,12 +68,11 @@ func currency(c string) (string, error) {
case "":
return "USD", nil
default:
return "", fmt.Errorf("%v\n", "Invalid currency abbreviation.")
return "", ErrCurrencyNotSupported
}
}
func request(path string, params url.Values, resp interface{}) error {
fmt.Println(path + "?" + params.Encode())
r, err := http.NewRequest("GET", path+"?"+params.Encode(), nil)
if err != nil {
return err
@@ -92,6 +86,15 @@ func request(path string, params url.Values, resp interface{}) error {
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
switch response.StatusCode {
case http.StatusUnauthorized:
return ErrUnauthorized
default:
return fmt.Errorf("unknown error: %s", response.Status)
}
}
body, err := ioutil.ReadAll(response.Body)
if err != nil {
return err

View File

@@ -29,3 +29,65 @@ func initDiscogsClient(t *testing.T, options *Options) *Client {
return client
}
func TestNewClient(t *testing.T) {
tests := map[string]struct {
options *Options
err error
}{
"normal": {&Options{
UserAgent: testUserAgent,
Currency: "USD",
Token: "some token",
}, nil},
"incorrect user-agent": {&Options{
UserAgent: "",
Currency: "USD",
}, ErrUserAgentInvalid},
"incorrect currency": {&Options{
UserAgent: testUserAgent,
Currency: "RUR",
}, ErrCurrencyNotSupported},
}
for name := range tests {
tt := tests[name]
t.Run(name, func(t *testing.T) {
if _, err := NewClient(tt.options); err != tt.err {
t.Errorf("err got=%s; want=%s", err, tt.err)
}
})
}
}
func TestCurrency(t *testing.T) {
tests := []struct {
currency string
want string
err error
}{
{currency: "", want: "USD", err: nil},
{currency: "USD", want: "USD", err: nil},
{currency: "GBP", want: "GBP", err: nil},
{currency: "EUR", want: "EUR", err: nil},
{currency: "CAD", want: "CAD", err: nil},
{currency: "AUD", want: "AUD", err: nil},
{currency: "JPY", want: "JPY", err: nil},
{currency: "CHF", want: "CHF", err: nil},
{currency: "MXN", want: "MXN", err: nil},
{currency: "BRL", want: "BRL", err: nil},
{currency: "NZD", want: "NZD", err: nil},
{currency: "SEK", want: "SEK", err: nil},
{currency: "ZAR", want: "ZAR", err: nil},
{currency: "RUR", want: "", err: ErrCurrencyNotSupported},
}
for i, tt := range tests {
cur, err := currency(tt.currency)
if err != tt.err {
t.Errorf("#%d err got=%s; want=%s", i, err, tt.err)
}
if cur != tt.want {
t.Errorf("#%d currency got=%s; want=%s", i, cur, tt.want)
}
}
}

View File

@@ -2,39 +2,21 @@ package discogs
import (
"fmt"
"strings"
)
// APIError represents a Discogs API Error response
type APIError struct {
Message string `json:"message"`
// Error represents a Discogs API error
type Error struct {
Message string
}
// Error ...
func (e APIError) Error() string {
if e.Message != "" {
return fmt.Sprintf("discogs: %v", e.Message)
}
return ""
func (e *Error) Error() string {
return fmt.Sprintf("discogs error: %s", strings.ToLower(e.Message))
}
// Empty returns true if empty. Otherwise, at least 1 error message/code is
// present and false is returned.
func (e APIError) Empty() bool {
if e.Message == "" {
return true
}
return false
}
// relevantError returns any non-nil http-related error (creating the request,
// getting the response, decoding) if any. If the decoded apiError is non-zero
// the apiError is returned. Otherwise, no errors occurred, returns nil.
func relevantError(httpError error, apiError APIError) error {
if httpError != nil {
return httpError
}
if apiError.Empty() {
return nil
}
return apiError
}
// APIErrors
var (
ErrUnauthorized = &Error{"authentication required"}
ErrCurrencyNotSupported = &Error{"currency does not supported"}
ErrUserAgentInvalid = &Error{"invalid user-agent"}
)

View File

@@ -9,7 +9,7 @@ import (
func main() {
d, err := discogs.NewClient(&discogs.Options{
UserAgent: "TestDiscogsClient/0.0.1 +http://example.com",
Currency: "EUR",
Currency: "USD",
Token: "",
})
if err != nil {
@@ -17,9 +17,7 @@ func main() {
return
}
release, err := d.Search.Search(discogs.SearchRequest{
Q: "Ska-Jazz Review",
})
release, err := d.Search.Search(discogs.SearchRequest{Q: "middle", PerPage: 3})
if err != nil {
fmt.Println(err)
return

3
go.mod Normal file
View File

@@ -0,0 +1,3 @@
module github.com/irlndts/go-discogs
go 1.12

0
go.sum Normal file
View File

View File

@@ -1,56 +0,0 @@
package discogs
import (
"strconv"
)
// LabelService ...
type LabelService struct {
url string
}
func newLabelService(url string) *LabelService {
return &LabelService{
url: url,
}
}
// Label resource represents a label, company, recording studio, location,
// or other entity involved with artists and releases.
type Label struct {
Profile string `json:"profile"`
ReleasesURL string `json:"releases_url"`
Name string `json:"name"`
ContactInfo string `json:"contact_info"`
URI string `json:"uri"`
Sublabels []Sublable `json:"sublabels"`
URLs []string `json:"urls"`
Images []Image `json:"images"`
ResourceURL string `json:"resource_url"`
ID int `json:"id"`
DataQuality string `json:"data_quality"`
}
// Label returns a label.
func (s *LabelService) Label(labelID int) (*Label, error) {
var label *Label
if err := request(s.url+strconv.Itoa(labelID), nil, &label); err != nil {
return nil, err
}
return label, nil
}
// LabelReleases is a list of Releases associated with the label.
type LabelReleases struct {
Pagination Page `json:"pagination"`
Releases []ReleaseSource `json:"releases"`
}
// Releases returns a list of Releases associated with the label.
func (s *LabelService) Releases(labelID int, pagination *Pagination) (*LabelReleases, error) {
var releases *LabelReleases
if err := request(s.url+strconv.Itoa(labelID)+"/releases", pagination.toParams(), &releases); err != nil {
return nil, err
}
return releases, nil
}

View File

@@ -1,59 +0,0 @@
package discogs
import (
"strconv"
)
type MasterService struct {
url string
}
func newMasterService(url string) *MasterService {
return &MasterService{
url: url,
}
}
// Master resource represents a set of similar releases.
// Masters (also known as `master releases`) have a `main release` which is often the chronologically earliest.
type Master struct {
Styles []string `json:"styles"`
Genres []string `json:"genres"`
Videos []Video `json:"videos"`
Title string `json:"title"`
Main_release int `json:"main_release"`
Main_release_url string `json:"main_release_url"`
Uri string `json:"uri"`
Artists []Artist `json:"artists"`
Version_url string `json:"version_url"`
Year int `json:"year"`
Images []Image `json:"images"`
Resource_url string `json:"resource_url"`
Tracklist []Track `json:"tracklist"`
Id int `json:"id"`
Data_quality string `json:"data_quality"`
}
// Master returns a master release
func (s *MasterService) Master(masterID int) (*Master, error) {
var master *Master
if err := request(s.url+strconv.Itoa(masterID), nil, &master); err != nil {
return nil, err
}
return master, nil
}
// MasterVersions retrieves a list of all releases that are versions of this master.
type MasterVersions struct {
Pagination Page `json:"pagination"`
Versions []Version `json:"versions"`
}
// Versions retrieves a list of all Releases that are versions of this master
func (s *MasterService) Versions(masterID int, pagination *Pagination) (*MasterVersions, error) {
var versions *MasterVersions
if err := request(s.url+strconv.Itoa(masterID)+"/versions", pagination.toParams(), &versions); err != nil {
return nil, err
}
return versions, nil
}

View File

@@ -19,7 +19,7 @@ type ArtistSource struct {
Anv string `json:"anv"`
ID int `json:"id"`
Join string `json:"join"`
Name string `json:"name:`
Name string `json:"name"`
ResourceURL string `json:"resource_url"`
Role string `json:"role"`
Tracks string `json:"tracks"`
@@ -42,6 +42,7 @@ type Track struct {
Title string `json:"title"`
Type string `json:"type_"`
Extraartists []ArtistSource `json:"extraartists"`
Artists []ArtistSource `json:"artists"`
}
// LabelSource ...
@@ -53,7 +54,7 @@ type LabelSource struct {
ResourceURL string `json:"resource_url"`
}
// Itentifier ...
// Identifier ...
type Identifier struct {
Type string `json:"type"`
Value string `json:"value"`
@@ -174,23 +175,15 @@ type Pagination struct {
}
// toParams converts pagaination params to request values
func (p *Pagination) toParams() url.Values {
func (p *Pagination) params() url.Values {
if p == nil {
return nil
}
params := url.Values{}
if p.Sort != "" {
params.Set("sort", p.Sort)
}
if p.SortOrder != "" {
params.Set("sort_order", p.SortOrder)
}
if p.Page != 0 {
params.Set("page", strconv.Itoa(p.Page))
}
if p.PerPage != 0 {
params.Set("per_page", strconv.Itoa(p.PerPage))
}
params.Set("sort", p.Sort)
params.Set("sort_order", p.SortOrder)
params.Set("page", strconv.Itoa(p.Page))
params.Set("per_page", strconv.Itoa(p.PerPage))
return params
}

View File

@@ -1,90 +0,0 @@
package discogs
import (
"net/url"
"strconv"
)
// ReleaseService ...
type ReleaseService struct {
url string
currency string
}
func newReleaseService(url string, currency string) *ReleaseService {
return &ReleaseService{
url: url,
currency: currency,
}
}
// ReqRelease serves release request
type ReqRelease struct {
CurrAbbr string
}
// Release serves relesase response from discogs
type Release struct {
Title string `json:"title"`
ID int `json:"id"`
Artists []ArtistSource `json:"artists"`
DataQuality string `json:"data_quality"`
Thumb string `json:"thumb"`
Community Community `json:"community"`
Companies []Company `json:"companies"`
Country string `json:"country"`
DateAdded string `json:"date_added"`
DateChanged string `json:"date_changed"`
EstimatedWeight int `json:"estimated_weight"`
ExtraArtists []ArtistSource `json:"extraartists"`
FormatQuantity int `json:"format_quantity"`
Formats []Format `json:"formats"`
Genres []string `json:"genres"`
Identifiers []Identifier `json:"identifiers"`
Images []Image `json:"images"`
Labels []LabelSource `json:"labels"`
LowestPrice float64 `json:"lowest_price"`
MasterID int `json:"master_id"`
MasterURL string `json:"master_url"`
Notes string `json:"notes,omitempty"`
NumForSale int `json:"num_for_sale,omitempty"`
Released string `json:"released"`
ReleasedFormatted string `json:"released_formatted"`
ResourceURL string `json:"resource_url"`
// Series
Status string `json:"status"`
Styles []string `json:"styles"`
Tracklist []Track `json:"tracklist"`
URI string `json:"uri"`
Videos []Video `json:"videos"`
Year int `json:"year"`
}
// Release returns release by release's ID
func (s *ReleaseService) Release(releaseID int) (*Release, error) {
params := url.Values{}
params.Set("CurrAbbr", s.currency)
var release *Release
if err := request(s.url+strconv.Itoa(releaseID), params, &release); err != nil {
return nil, err
}
return release, nil
}
// ReleaseRating serves response for community release rating request
type ReleaseRating struct {
ID int `json:"release_id"`
Rating Rating `json:"rating"`
}
// Ratings retruns community release rating
func (s *ReleaseService) Rating(releaseID int) (*ReleaseRating, error) {
var rating *ReleaseRating
if err := request(s.url+strconv.Itoa(releaseID)+"/rating", nil, &rating); err != nil {
return nil, err
}
return rating, nil
}

127
search.go
View File

@@ -1,7 +1,8 @@
package discogs
import (
"github.com/google/go-querystring/query"
"net/url"
"strconv"
)
// SearchService ...
@@ -15,29 +16,98 @@ func newSearchService(url string) *SearchService {
}
}
// SerachRequest describes search request
// SearchRequest describes search request
type SearchRequest struct {
Q string `url:"q,omitempty"` // search query
Type string `url:"type,omitempty"` // one of release, master, artist, label
Title string `url:"title,omitempty"` // search by combined “Artist Name - Release Title” title field
ReleaseTitle string `url:"release_title,omitempty"` // search release titles
Credit string `url:"credit,omitempty"` // search release credits
Artist string `url:"artist,omitempty"` // search artist names
Anv string `url:"anv,omitempty"` // search artist ANV
Label string `url:"label,omitempty"` // search label names
Genre string `url:"genre,omitempty"` // search genres
Style string `url:"style,omitempty"` // search styles
Country string `url:"country,omitempty"` // search release country
Year string `url:"year,omitempty"` // search release year
Format string `url:"format,omitempty"` // search formats
Catno string `url:"catno,omitempty"` // search catalog number
Barcode string `url:"barcode,omitempty"` // search barcodes
Track string `url:"track,omitempty"` // search track titles
Submitter string `url:"submitter,omitempty"` // search submitter username
Contributer string `url:"contributer,omitempty"` // search contributor usernames
Q string // search query
Type string // one of release, master, artist, label
Title string // search by combined “Artist Name - Release Title” title field
ReleaseTitle string // search release titles
Credit string // search release credits
Artist string // search artist names
Anv string // search artist ANV
Label string // search label names
Genre string // search genres
Style string // search styles
Country string // search release country
Year string // search release year
Format string // search formats
Catno string // search catalog number
Barcode string // search barcodes
Track string // search track titles
Submitter string // search submitter username
Contributor string // search contributor usernames
Page int `url:"page,omitempty"`
PerPage int `url:"per_page,omitempty"`
Page int
PerPage int
}
func (r *SearchRequest) params() url.Values {
if r == nil {
return nil
}
params := url.Values{}
if r.Q != "" {
params.Set("q", r.Q)
}
if r.Type != "" {
params.Set("type", r.Type)
}
if r.Title != "" {
params.Set("title", r.Title)
}
if r.ReleaseTitle != "" {
params.Set("release_title", r.ReleaseTitle)
}
if r.Credit != "" {
params.Set("credit", r.Credit)
}
if r.Artist != "" {
params.Set("artist", r.Artist)
}
if r.Anv != "" {
params.Set("anv", r.Anv)
}
if r.Label != "" {
params.Set("label", r.Label)
}
if r.Genre != "" {
params.Set("genre", r.Genre)
}
if r.Style != "" {
params.Set("style", r.Style)
}
if r.Country != "" {
params.Set("country", r.Country)
}
if r.Year != "" {
params.Set("year", r.Year)
}
if r.Format != "" {
params.Set("format", r.Format)
}
if r.Catno != "" {
params.Set("catno", r.Catno)
}
if r.Barcode != "" {
params.Set("barcode", r.Barcode)
}
if r.Track != "" {
params.Set("track", r.Track)
}
if r.Submitter != "" {
params.Set("submitter", r.Submitter)
}
if r.Contributor != "" {
params.Set("contributor", r.Contributor)
}
params.Set("page", strconv.Itoa(r.Page))
if r.PerPage != 0 {
params.Set("per_page", strconv.Itoa(r.PerPage))
}
return params
}
// Search describes search response
@@ -50,6 +120,7 @@ type Search struct {
type Result struct {
Style []string `json:"style,omitempty"`
Thumb string `json:"thumb,omitempty"`
CoverImage string `json:"cover_image,omitempty"`
Title string `json:"title,omitempty"`
Country string `json:"country,omitempty"`
Format []string `json:"format,omitempty"`
@@ -68,16 +139,8 @@ type Result struct {
// Issue a search query to our database. This endpoint accepts pagination parameters.
// Authentication (as any user) is required.
// https://www.discogs.com/developers/#page:database,header:database-search
// TODO(irlndts): improve params to pass
func (s *SearchService) Search(req SearchRequest) (*Search, error) {
params, err := query.Values(req)
if err != nil {
return nil, err
}
var search *Search
if err := request(s.url, params, &search); err != nil {
return nil, err
}
return search, nil
err := request(s.url, req.params(), &search)
return search, err
}

View File

@@ -1,27 +0,0 @@
Copyright (c) 2013 Google. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@@ -1,320 +0,0 @@
// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package query implements encoding of structs into URL query parameters.
//
// As a simple example:
//
// type Options struct {
// Query string `url:"q"`
// ShowAll bool `url:"all"`
// Page int `url:"page"`
// }
//
// opt := Options{ "foo", true, 2 }
// v, _ := query.Values(opt)
// fmt.Print(v.Encode()) // will output: "q=foo&all=true&page=2"
//
// The exact mapping between Go values and url.Values is described in the
// documentation for the Values() function.
package query
import (
"bytes"
"fmt"
"net/url"
"reflect"
"strconv"
"strings"
"time"
)
var timeType = reflect.TypeOf(time.Time{})
var encoderType = reflect.TypeOf(new(Encoder)).Elem()
// Encoder is an interface implemented by any type that wishes to encode
// itself into URL values in a non-standard way.
type Encoder interface {
EncodeValues(key string, v *url.Values) error
}
// Values returns the url.Values encoding of v.
//
// Values expects to be passed a struct, and traverses it recursively using the
// following encoding rules.
//
// Each exported struct field is encoded as a URL parameter unless
//
// - the field's tag is "-", or
// - the field is empty and its tag specifies the "omitempty" option
//
// The empty values are false, 0, any nil pointer or interface value, any array
// slice, map, or string of length zero, and any time.Time that returns true
// for IsZero().
//
// The URL parameter name defaults to the struct field name but can be
// specified in the struct field's tag value. The "url" key in the struct
// field's tag value is the key name, followed by an optional comma and
// options. For example:
//
// // Field is ignored by this package.
// Field int `url:"-"`
//
// // Field appears as URL parameter "myName".
// Field int `url:"myName"`
//
// // Field appears as URL parameter "myName" and the field is omitted if
// // its value is empty
// Field int `url:"myName,omitempty"`
//
// // Field appears as URL parameter "Field" (the default), but the field
// // is skipped if empty. Note the leading comma.
// Field int `url:",omitempty"`
//
// For encoding individual field values, the following type-dependent rules
// apply:
//
// Boolean values default to encoding as the strings "true" or "false".
// Including the "int" option signals that the field should be encoded as the
// strings "1" or "0".
//
// time.Time values default to encoding as RFC3339 timestamps. Including the
// "unix" option signals that the field should be encoded as a Unix time (see
// time.Unix())
//
// Slice and Array values default to encoding as multiple URL values of the
// same name. Including the "comma" option signals that the field should be
// encoded as a single comma-delimited value. Including the "space" option
// similarly encodes the value as a single space-delimited string. Including
// the "semicolon" option will encode the value as a semicolon-delimited string.
// Including the "brackets" option signals that the multiple URL values should
// have "[]" appended to the value name. "numbered" will append a number to
// the end of each incidence of the value name, example:
// name0=value0&name1=value1, etc.
//
// Anonymous struct fields are usually encoded as if their inner exported
// fields were fields in the outer struct, subject to the standard Go
// visibility rules. An anonymous struct field with a name given in its URL
// tag is treated as having that name, rather than being anonymous.
//
// Non-nil pointer values are encoded as the value pointed to.
//
// Nested structs are encoded including parent fields in value names for
// scoping. e.g:
//
// "user[name]=acme&user[addr][postcode]=1234&user[addr][city]=SFO"
//
// All other values are encoded using their default string representation.
//
// Multiple fields that encode to the same URL parameter name will be included
// as multiple URL values of the same name.
func Values(v interface{}) (url.Values, error) {
values := make(url.Values)
val := reflect.ValueOf(v)
for val.Kind() == reflect.Ptr {
if val.IsNil() {
return values, nil
}
val = val.Elem()
}
if v == nil {
return values, nil
}
if val.Kind() != reflect.Struct {
return nil, fmt.Errorf("query: Values() expects struct input. Got %v", val.Kind())
}
err := reflectValue(values, val, "")
return values, err
}
// reflectValue populates the values parameter from the struct fields in val.
// Embedded structs are followed recursively (using the rules defined in the
// Values function documentation) breadth-first.
func reflectValue(values url.Values, val reflect.Value, scope string) error {
var embedded []reflect.Value
typ := val.Type()
for i := 0; i < typ.NumField(); i++ {
sf := typ.Field(i)
if sf.PkgPath != "" && !sf.Anonymous { // unexported
continue
}
sv := val.Field(i)
tag := sf.Tag.Get("url")
if tag == "-" {
continue
}
name, opts := parseTag(tag)
if name == "" {
if sf.Anonymous && sv.Kind() == reflect.Struct {
// save embedded struct for later processing
embedded = append(embedded, sv)
continue
}
name = sf.Name
}
if scope != "" {
name = scope + "[" + name + "]"
}
if opts.Contains("omitempty") && isEmptyValue(sv) {
continue
}
if sv.Type().Implements(encoderType) {
if !reflect.Indirect(sv).IsValid() {
sv = reflect.New(sv.Type().Elem())
}
m := sv.Interface().(Encoder)
if err := m.EncodeValues(name, &values); err != nil {
return err
}
continue
}
if sv.Kind() == reflect.Slice || sv.Kind() == reflect.Array {
var del byte
if opts.Contains("comma") {
del = ','
} else if opts.Contains("space") {
del = ' '
} else if opts.Contains("semicolon") {
del = ';'
} else if opts.Contains("brackets") {
name = name + "[]"
}
if del != 0 {
s := new(bytes.Buffer)
first := true
for i := 0; i < sv.Len(); i++ {
if first {
first = false
} else {
s.WriteByte(del)
}
s.WriteString(valueString(sv.Index(i), opts))
}
values.Add(name, s.String())
} else {
for i := 0; i < sv.Len(); i++ {
k := name
if opts.Contains("numbered") {
k = fmt.Sprintf("%s%d", name, i)
}
values.Add(k, valueString(sv.Index(i), opts))
}
}
continue
}
for sv.Kind() == reflect.Ptr {
if sv.IsNil() {
break
}
sv = sv.Elem()
}
if sv.Type() == timeType {
values.Add(name, valueString(sv, opts))
continue
}
if sv.Kind() == reflect.Struct {
reflectValue(values, sv, name)
continue
}
values.Add(name, valueString(sv, opts))
}
for _, f := range embedded {
if err := reflectValue(values, f, scope); err != nil {
return err
}
}
return nil
}
// valueString returns the string representation of a value.
func valueString(v reflect.Value, opts tagOptions) string {
for v.Kind() == reflect.Ptr {
if v.IsNil() {
return ""
}
v = v.Elem()
}
if v.Kind() == reflect.Bool && opts.Contains("int") {
if v.Bool() {
return "1"
}
return "0"
}
if v.Type() == timeType {
t := v.Interface().(time.Time)
if opts.Contains("unix") {
return strconv.FormatInt(t.Unix(), 10)
}
return t.Format(time.RFC3339)
}
return fmt.Sprint(v.Interface())
}
// isEmptyValue checks if a value should be considered empty for the purposes
// of omitting fields with the "omitempty" option.
func isEmptyValue(v reflect.Value) bool {
switch v.Kind() {
case reflect.Array, reflect.Map, reflect.Slice, reflect.String:
return v.Len() == 0
case reflect.Bool:
return !v.Bool()
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return v.Int() == 0
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
return v.Uint() == 0
case reflect.Float32, reflect.Float64:
return v.Float() == 0
case reflect.Interface, reflect.Ptr:
return v.IsNil()
}
if v.Type() == timeType {
return v.Interface().(time.Time).IsZero()
}
return false
}
// tagOptions is the string following a comma in a struct field's "url" tag, or
// the empty string. It does not include the leading comma.
type tagOptions []string
// parseTag splits a struct field's url tag into its name and comma-separated
// options.
func parseTag(tag string) (string, tagOptions) {
s := strings.Split(tag, ",")
return s[0], s[1:]
}
// Contains checks whether the tagOptions contains the specified option.
func (o tagOptions) Contains(option string) bool {
for _, s := range o {
if s == option {
return true
}
}
return false
}

View File

@@ -1,328 +0,0 @@
// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package query
import (
"fmt"
"net/url"
"reflect"
"testing"
"time"
)
type Nested struct {
A SubNested `url:"a"`
B *SubNested `url:"b"`
Ptr *SubNested `url:"ptr,omitempty"`
}
type SubNested struct {
Value string `url:"value"`
}
func TestValues_types(t *testing.T) {
str := "string"
strPtr := &str
timeVal := time.Date(2000, 1, 1, 12, 34, 56, 0, time.UTC)
tests := []struct {
in interface{}
want url.Values
}{
{
// basic primitives
struct {
A string
B int
C uint
D float32
E bool
}{},
url.Values{
"A": {""},
"B": {"0"},
"C": {"0"},
"D": {"0"},
"E": {"false"},
},
},
{
// pointers
struct {
A *string
B *int
C **string
D *time.Time
}{
A: strPtr,
C: &strPtr,
D: &timeVal,
},
url.Values{
"A": {str},
"B": {""},
"C": {str},
"D": {"2000-01-01T12:34:56Z"},
},
},
{
// slices and arrays
struct {
A []string
B []string `url:",comma"`
C []string `url:",space"`
D [2]string
E [2]string `url:",comma"`
F [2]string `url:",space"`
G []*string `url:",space"`
H []bool `url:",int,space"`
I []string `url:",brackets"`
J []string `url:",semicolon"`
K []string `url:",numbered"`
}{
A: []string{"a", "b"},
B: []string{"a", "b"},
C: []string{"a", "b"},
D: [2]string{"a", "b"},
E: [2]string{"a", "b"},
F: [2]string{"a", "b"},
G: []*string{&str, &str},
H: []bool{true, false},
I: []string{"a", "b"},
J: []string{"a", "b"},
K: []string{"a", "b"},
},
url.Values{
"A": {"a", "b"},
"B": {"a,b"},
"C": {"a b"},
"D": {"a", "b"},
"E": {"a,b"},
"F": {"a b"},
"G": {"string string"},
"H": {"1 0"},
"I[]": {"a", "b"},
"J": {"a;b"},
"K0": {"a"},
"K1": {"b"},
},
},
{
// other types
struct {
A time.Time
B time.Time `url:",unix"`
C bool `url:",int"`
D bool `url:",int"`
}{
A: time.Date(2000, 1, 1, 12, 34, 56, 0, time.UTC),
B: time.Date(2000, 1, 1, 12, 34, 56, 0, time.UTC),
C: true,
D: false,
},
url.Values{
"A": {"2000-01-01T12:34:56Z"},
"B": {"946730096"},
"C": {"1"},
"D": {"0"},
},
},
{
struct {
Nest Nested `url:"nest"`
}{
Nested{
A: SubNested{
Value: "that",
},
},
},
url.Values{
"nest[a][value]": {"that"},
"nest[b]": {""},
},
},
{
struct {
Nest Nested `url:"nest"`
}{
Nested{
Ptr: &SubNested{
Value: "that",
},
},
},
url.Values{
"nest[a][value]": {""},
"nest[b]": {""},
"nest[ptr][value]": {"that"},
},
},
{
nil,
url.Values{},
},
}
for i, tt := range tests {
v, err := Values(tt.in)
if err != nil {
t.Errorf("%d. Values(%q) returned error: %v", i, tt.in, err)
}
if !reflect.DeepEqual(tt.want, v) {
t.Errorf("%d. Values(%q) returned %v, want %v", i, tt.in, v, tt.want)
}
}
}
func TestValues_omitEmpty(t *testing.T) {
str := ""
s := struct {
a string
A string
B string `url:",omitempty"`
C string `url:"-"`
D string `url:"omitempty"` // actually named omitempty, not an option
E *string `url:",omitempty"`
}{E: &str}
v, err := Values(s)
if err != nil {
t.Errorf("Values(%q) returned error: %v", s, err)
}
want := url.Values{
"A": {""},
"omitempty": {""},
"E": {""}, // E is included because the pointer is not empty, even though the string being pointed to is
}
if !reflect.DeepEqual(want, v) {
t.Errorf("Values(%q) returned %v, want %v", s, v, want)
}
}
type A struct {
B
}
type B struct {
C string
}
type D struct {
B
C string
}
type e struct {
B
C string
}
type F struct {
e
}
func TestValues_embeddedStructs(t *testing.T) {
tests := []struct {
in interface{}
want url.Values
}{
{
A{B{C: "foo"}},
url.Values{"C": {"foo"}},
},
{
D{B: B{C: "bar"}, C: "foo"},
url.Values{"C": {"foo", "bar"}},
},
{
F{e{B: B{C: "bar"}, C: "foo"}}, // With unexported embed
url.Values{"C": {"foo", "bar"}},
},
}
for i, tt := range tests {
v, err := Values(tt.in)
if err != nil {
t.Errorf("%d. Values(%q) returned error: %v", i, tt.in, err)
}
if !reflect.DeepEqual(tt.want, v) {
t.Errorf("%d. Values(%q) returned %v, want %v", i, tt.in, v, tt.want)
}
}
}
func TestValues_invalidInput(t *testing.T) {
_, err := Values("")
if err == nil {
t.Errorf("expected Values() to return an error on invalid input")
}
}
type EncodedArgs []string
func (m EncodedArgs) EncodeValues(key string, v *url.Values) error {
for i, arg := range m {
v.Set(fmt.Sprintf("%s.%d", key, i), arg)
}
return nil
}
func TestValues_Marshaler(t *testing.T) {
s := struct {
Args EncodedArgs `url:"arg"`
}{[]string{"a", "b", "c"}}
v, err := Values(s)
if err != nil {
t.Errorf("Values(%q) returned error: %v", s, err)
}
want := url.Values{
"arg.0": {"a"},
"arg.1": {"b"},
"arg.2": {"c"},
}
if !reflect.DeepEqual(want, v) {
t.Errorf("Values(%q) returned %v, want %v", s, v, want)
}
}
func TestValues_MarshalerWithNilPointer(t *testing.T) {
s := struct {
Args *EncodedArgs `url:"arg"`
}{}
v, err := Values(s)
if err != nil {
t.Errorf("Values(%q) returned error: %v", s, err)
}
want := url.Values{}
if !reflect.DeepEqual(want, v) {
t.Errorf("Values(%q) returned %v, want %v", s, v, want)
}
}
func TestTagParsing(t *testing.T) {
name, opts := parseTag("field,foobar,foo")
if name != "field" {
t.Fatalf("name = %q, want field", name)
}
for _, tt := range []struct {
opt string
want bool
}{
{"foobar", true},
{"foo", true},
{"bar", false},
{"field", false},
} {
if opts.Contains(tt.opt) != tt.want {
t.Errorf("Contains(%q) = %v", tt.opt, !tt.want)
}
}
}

13
vendor/vendor.json vendored
View File

@@ -1,13 +0,0 @@
{
"comment": "",
"ignore": "",
"package": [
{
"checksumSHA1": "aDjb3mG2PnOgRgQXVXBxzPNc9I0=",
"path": "github.com/google/go-querystring/query",
"revision": "53e6ce116135b80d037921a7fdd5138cf32d7a8a",
"revisionTime": "2017-01-11T10:11:55Z"
}
],
"rootPath": "github.com/irlndts/go-discogs"
}