├── .gitignore ├── LICENSE ├── README.md └── soundcloud ├── apierror.go ├── apps.go ├── comments.go ├── config_test.go ├── config_test.go.template ├── core.go ├── endpoints.go ├── example_test.go.old ├── groups.go ├── helpers.go ├── helpers_test.go ├── me.go ├── me_test.go ├── oauth.go ├── oembed └── oembed.go ├── playlists.go ├── resolve.go ├── resolve_test.go ├── tracks.go ├── tracks_test.go ├── types.go ├── users.go └── users_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | 24 | instagram/config_test.go 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Jon Eisen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # golang-soundcloud 2 | 3 | A (_incomplete_) [SoundCloud](http://soundcloud.com) [API](http://developers.soundcloud.com) wrapper. 4 | 5 | ## Features 6 | 7 | Implemented: 8 | 9 | - Most GET requests are implemented 10 | - Both authenticated and unauthenticated requests can be made 11 | - Refreshes tokens 12 | - No `interface{}` data types! 13 | 14 | Todo: 15 | 16 | - The full set of GET requests 17 | - Full Authentication 18 | - POST / PUT / DELETE requests 19 | 20 | ## Contributing 21 | 22 | This API wrapper is a good start, but is no where near completion. If you're using Go and SoundCloud, please consider contributing by implementing more methods (see [Issues](https://github.com/yanatan16/golang-soundcloud/issues)) and making a pull request. As for style, just use `go fmt` before you pull! 23 | 24 | ## Documentation 25 | 26 | [Documentation on godoc.org](http://godoc.org/github.com/yanatan16/golang-soundcloud/soundcloud) 27 | 28 | ## Install 29 | 30 | ``` 31 | go get github.com/yanatan16/golang-soundcloud/soundcloud 32 | ``` 33 | 34 | ## Creation 35 | 36 | ```go 37 | import ( 38 | "github.com/yanatan16/golang-soundcloud/soundcloud" 39 | ) 40 | 41 | unauthenticatedApi := &soundcloud.Api{ 42 | ClientId: "my-client-id", 43 | } 44 | 45 | authenticatedApi := &soundcloud.Api{ 46 | ClientId: "my-client-id", 47 | ClientSecret: "my-client-secret", 48 | AccessToken: "my-access-token", 49 | RefreshToken: "my-refresh-token", 50 | } 51 | ``` 52 | 53 | ## Usage 54 | 55 | ```go 56 | import ( 57 | "fmt" 58 | "github.com/yanatan16/golang-soundcloud/soundcloud" 59 | "net/url" 60 | ) 61 | 62 | func DoSomeSoundCloudApiStuff(accessToken string) { 63 | api := New("", accessToken) 64 | 65 | var myId string 66 | 67 | // Get yourself! 68 | if me, err := api.Me().Get(); err != nil { 69 | panic(err) 70 | } else { 71 | fmt.Printf("My userid is %s, username is %s, and I have %d followers\n", me.Id, me.Username, me.FollowerCount) 72 | } 73 | } 74 | ``` 75 | 76 | ## Tests 77 | 78 | To run the tests, you'll need at least a `ClientId` (which you can get from [here](http://soundcloud.com/developer/clients/manage/)), and preferably an authenticated users' `AccessToken`, which is a bit harder to get (involves authenticating for an app and getting that auth token) 79 | 80 | First, fill in `config_test.go.example` and save it as `config_test.go`. Then run `go test` 81 | 82 | ## Notes 83 | 84 | - Certain methods require an access token so check the official documentation before using an unauthenticated `Api`. This package will use it if it is given. 85 | 86 | ## License 87 | 88 | MIT-style. See LICENSE file. -------------------------------------------------------------------------------- /soundcloud/apierror.go: -------------------------------------------------------------------------------- 1 | package soundcloud 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | ) 7 | 8 | type ApiError struct { 9 | Repr string 10 | URL string 11 | Code int 12 | Message string 13 | } 14 | 15 | type errorResponse struct { 16 | Error interface{} 17 | } 18 | 19 | func newApiError(req *http.Request, resp *http.Response) *ApiError { 20 | errResp := new(errorResponse) 21 | err := decodeResponse(resp.Body, errResp) 22 | if err != nil { 23 | errResp.Error = "" 24 | } 25 | 26 | repr := fmt.Sprintf("Bad response code %d", resp.StatusCode) 27 | return &ApiError{Repr: repr, Code: resp.StatusCode, URL: req.URL.String(), Message: fmt.Sprintf("%v", errResp.Error)} 28 | } 29 | 30 | func newApiErrorStack(err error, req *http.Request, resp *http.Response) *ApiError { 31 | ae := newApiError(req, resp) 32 | if err != nil { 33 | ae.Repr = err.Error() 34 | } 35 | return ae 36 | } 37 | 38 | func (ae *ApiError) Error() string { 39 | return fmt.Sprintf("%s on request to %s: %s", ae.Repr, ae.URL, ae.Message) 40 | } 41 | -------------------------------------------------------------------------------- /soundcloud/apps.go: -------------------------------------------------------------------------------- 1 | package soundcloud 2 | 3 | import ( 4 | "net/url" 5 | ) 6 | 7 | type AppApi struct { 8 | appEndpoint 9 | } 10 | 11 | func (api *Api) Apps(params url.Values) ([]*App, error) { 12 | ret := make([]*App, 0) 13 | err := api.get("/apps", params, &ret) 14 | return ret, err 15 | } 16 | 17 | func (api *Api) App(id uint64) *AppApi { 18 | return &AppApi{*api.newAppEndpoint("apps", id)} 19 | } 20 | 21 | func (a *AppApi) Tracks(params url.Values) ([]*Track, error) { 22 | ret := make([]*Track, 0) 23 | err := a.api.get(a.base+"/tracks", params, &ret) 24 | return ret, err 25 | } 26 | -------------------------------------------------------------------------------- /soundcloud/comments.go: -------------------------------------------------------------------------------- 1 | package soundcloud 2 | 3 | import () 4 | 5 | type CommentApi struct { 6 | commentEndpoint 7 | } 8 | 9 | func (api *Api) Comment(id uint64) *CommentApi { 10 | return &CommentApi{*api.newCommentEndpoint("comments", id)} 11 | } 12 | 13 | // No idea how these endpoints works 14 | // func (t *PlaylistApi) SharedToUsers() (*usersEndpoint) { 15 | // func (t *PlaylistApi) SharedToEmails() (*emailsEndpoint) { 16 | // func (t *PlaylistApi) SecretToken() (*tokenEndpoint) 17 | -------------------------------------------------------------------------------- /soundcloud/config_test.go: -------------------------------------------------------------------------------- 1 | package soundcloud 2 | 3 | // Copy this to config_test.go (its ignored by git) 4 | // And fill in your test clientID, clientSecret, authorization token and secret 5 | // You can get the client information from http://developers.soundcloud.com/you/apps 6 | // And you'll have to authorize with that client to get a token 7 | // You don't NEED access tokens per se but some requests require it. 8 | 9 | var TestConfig map[string]interface{} = map[string]interface{}{ 10 | "client_id": "f6212fc8e1e90501e7cece44f78b8f43", 11 | "client_secret": "45d1d26f8399fc4c19abcf14d3830ff2", 12 | "access_token": "1-64086-73163832-dc46009aebdb123", 13 | "refresh_token": "7b66f2a4a11add6d62fe25dd9d8ee785", 14 | "my_id": uint64(73163832), 15 | } 16 | -------------------------------------------------------------------------------- /soundcloud/config_test.go.template: -------------------------------------------------------------------------------- 1 | package instagram 2 | 3 | // Copy this to config_test.go (its ignored by git) 4 | // And fill in your test clientID, clientSecret, authorization token and secret 5 | // You can get the client information from http://developers.soundcloud.com/you/apps 6 | // And you'll have to authorize with that client to get a token 7 | // You don't NEED access tokens per se but some requests require it. 8 | 9 | var TestConfig map[string]string = map[string]string{ 10 | "client_id": "", 11 | "client_secret": "", 12 | "access_token": "", 13 | "refresh_token": "", 14 | "my_id": uint64(1234567), // The authenticated user's ID 15 | } -------------------------------------------------------------------------------- /soundcloud/core.go: -------------------------------------------------------------------------------- 1 | // Package soundcloud provides a minimialist soundcloud API wrapper. 2 | package soundcloud 3 | 4 | import ( 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "net/http" 10 | "net/url" 11 | "strings" 12 | ) 13 | 14 | var ( 15 | baseUrl = "https://api.soundcloud.com" 16 | client *http.Client = &http.Client{ 17 | CheckRedirect: noRedirects, 18 | } 19 | ) 20 | 21 | func noRedirects(req *http.Request, via []*http.Request) error { 22 | return errors.New("No redirects!") 23 | } 24 | 25 | type Api struct { 26 | ClientId string 27 | ClientSecret string 28 | AccessToken string 29 | RefreshToken string 30 | } 31 | 32 | func (api *Api) Authenticated() bool { 33 | return api.AccessToken != "" 34 | } 35 | 36 | // -- Implementation of request -- 37 | 38 | func buildGetRequest(urlStr string, params url.Values) (*http.Request, error) { 39 | u, err := url.Parse(urlStr) 40 | if err != nil { 41 | return nil, err 42 | } 43 | 44 | // If we are getting, then we can't merge query params 45 | if params != nil { 46 | if u.RawQuery != "" { 47 | return nil, fmt.Errorf("Cannot merge query params in urlStr and params") 48 | } 49 | u.RawQuery = params.Encode() 50 | } 51 | 52 | u.Path += ".json" 53 | return http.NewRequest("GET", u.String(), nil) 54 | } 55 | 56 | func buildNonGetRequest(method, urlStr string, params url.Values) (*http.Request, error) { 57 | u, err := url.Parse(urlStr) 58 | if err != nil { 59 | return nil, err 60 | } 61 | 62 | // If we are getting, then we can't merge query params 63 | if u.RawQuery != "" { 64 | if params != nil { 65 | return nil, fmt.Errorf("Cannot merge query params in urlStr and params") 66 | } 67 | params = u.Query() 68 | u.RawQuery = "" 69 | } 70 | 71 | if u.Path != "/oauth2/token" { 72 | u.Path += ".json" 73 | } 74 | return http.NewRequest(method, u.String(), strings.NewReader(params.Encode())) 75 | } 76 | 77 | func (api *Api) extendParams(p url.Values, auth ...bool) (url.Values, error) { 78 | if p == nil { 79 | p = url.Values{} 80 | } 81 | 82 | if len(auth) > 0 && auth[0] { 83 | if api.AccessToken != "" { 84 | p.Set("oauth_token", api.AccessToken) 85 | } else { 86 | return p, errors.New("Access token required to use this endpoint") 87 | } 88 | } else { 89 | p.Set("client_id", api.ClientId) 90 | } 91 | return p, nil 92 | } 93 | 94 | func (api *Api) get(path string, params url.Values, r interface{}, auth ...bool) error { 95 | params, err := api.extendParams(params, auth...) 96 | if err != nil { 97 | return err 98 | } 99 | 100 | req, err := buildGetRequest(urlify(path), params) 101 | if err != nil { 102 | return err 103 | } 104 | return api.do(req, r) 105 | } 106 | 107 | func (api *Api) post(path string, params url.Values, r interface{}, auth ...bool) error { 108 | return api.nonget("POST", path, params, r) 109 | } 110 | 111 | func (api *Api) put(path string, params url.Values, r interface{}, auth ...bool) error { 112 | return api.nonget("PUT", path, params, r) 113 | } 114 | 115 | func (api *Api) delete(path string, params url.Values, r interface{}, auth ...bool) error { 116 | return api.nonget("DELETE", path, params, r) 117 | } 118 | 119 | func (api *Api) nonget(method, path string, params url.Values, r interface{}, auth ...bool) error { 120 | params, err := api.extendParams(params, auth...) 121 | if err != nil { 122 | return err 123 | } 124 | 125 | req, err := buildNonGetRequest(method, urlify(path), params) 126 | if err != nil { 127 | return err 128 | } 129 | return api.do(req, r) 130 | } 131 | 132 | func (api *Api) do(req *http.Request, r interface{}) error { 133 | resp, err := client.Do(req) 134 | if urlerr, ok := err.(*url.Error); ok && urlerr.Err.Error() == "No redirects!" { 135 | err = nil 136 | } else if err != nil { 137 | return err 138 | } 139 | defer resp.Body.Close() 140 | 141 | if resp.StatusCode > 399 { 142 | return newApiError(req, resp) 143 | } 144 | 145 | if err := decodeResponse(resp.Body, r); err != nil { 146 | return newApiErrorStack(err, req, resp) 147 | } 148 | return nil 149 | } 150 | 151 | func decodeResponse(body io.Reader, to interface{}) error { 152 | err := json.NewDecoder(body).Decode(to) 153 | 154 | if err != nil { 155 | return fmt.Errorf("soundcloud: error decoding body: %s", err.Error()) 156 | } 157 | return nil 158 | } 159 | 160 | func urlify(path string) string { 161 | return baseUrl + path 162 | } 163 | 164 | func ensureParams(v url.Values) url.Values { 165 | if v == nil { 166 | return url.Values{} 167 | } 168 | return v 169 | } 170 | -------------------------------------------------------------------------------- /soundcloud/endpoints.go: -------------------------------------------------------------------------------- 1 | package soundcloud 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | ) 7 | 8 | type endpoint struct { 9 | api *Api 10 | base string 11 | authReq bool 12 | } 13 | 14 | // Creates a new endpoint at the join of all dirs, where dir can be string or uint64 (ids). 15 | // Last param can be bool, which would make an authentication required endpoint 16 | func (api *Api) newEndpoint(dirs ...interface{}) endpoint { 17 | path := "" 18 | authReq := false 19 | for i, dir := range dirs { 20 | if sdir, ok := dir.(string); ok { 21 | path += "/" + sdir 22 | continue 23 | } 24 | if idir, ok := dir.(uint64); ok { 25 | path += fmt.Sprintf("/%d", idir) 26 | continue 27 | } 28 | if bdir, ok := dir.(bool); ok && i+1 == len(dirs) { 29 | authReq = bdir 30 | continue 31 | } 32 | panic("Only strings and uint64 accepted; last can be bool") 33 | } 34 | return endpoint{api, path, authReq} 35 | } 36 | 37 | type userEndpoint struct { 38 | endpoint 39 | } 40 | 41 | func (api *Api) newUserEndpoint(dirs ...interface{}) *userEndpoint { 42 | return &userEndpoint{api.newEndpoint(dirs...)} 43 | } 44 | 45 | func (u *userEndpoint) Get(params url.Values) (*User, error) { 46 | ret := new(User) 47 | err := u.api.get(u.base, params, ret, u.authReq) 48 | return ret, err 49 | } 50 | 51 | // func (u *userEndpoint) Put(params url.Values) (*User, error) { 52 | // ret := new(User) 53 | // err := u.api.put(u.base, params, ret) 54 | // return ret, err 55 | // } 56 | 57 | // func (u *userEndpoint) Delete(params url.Values) (*User, error) { 58 | // ret := new(User) 59 | // err := u.api.delete(u.base, params, ret) 60 | // return ret, err 61 | // } 62 | 63 | type trackEndpoint struct { 64 | endpoint 65 | } 66 | 67 | func (api *Api) newTrackEndpoint(dirs ...interface{}) *trackEndpoint { 68 | return &trackEndpoint{api.newEndpoint(dirs...)} 69 | } 70 | 71 | func (t *trackEndpoint) Get(params url.Values) (*Track, error) { 72 | ret := new(Track) 73 | err := t.api.get(t.base, params, ret) 74 | return ret, err 75 | } 76 | 77 | type commentEndpoint struct { 78 | endpoint 79 | } 80 | 81 | func (api *Api) newCommentEndpoint(dirs ...interface{}) *commentEndpoint { 82 | return &commentEndpoint{api.newEndpoint(dirs...)} 83 | } 84 | 85 | func (t *commentEndpoint) Get(params url.Values) (*Comment, error) { 86 | ret := new(Comment) 87 | err := t.api.get(t.base, params, ret) 88 | return ret, err 89 | } 90 | 91 | type playlistEndpoint struct { 92 | endpoint 93 | } 94 | 95 | func (api *Api) newPlaylistEndpoint(dirs ...interface{}) *playlistEndpoint { 96 | return &playlistEndpoint{api.newEndpoint(dirs...)} 97 | } 98 | 99 | func (t *playlistEndpoint) Get(params url.Values) (*Playlist, error) { 100 | ret := new(Playlist) 101 | err := t.api.get(t.base, params, ret) 102 | return ret, err 103 | } 104 | 105 | type groupEndpoint struct { 106 | endpoint 107 | } 108 | 109 | func (api *Api) newGroupEndpoint(dirs ...interface{}) *groupEndpoint { 110 | return &groupEndpoint{api.newEndpoint(dirs...)} 111 | } 112 | 113 | func (t *groupEndpoint) Get(params url.Values) (*Group, error) { 114 | ret := new(Group) 115 | err := t.api.get(t.base, params, ret) 116 | return ret, err 117 | } 118 | 119 | type appEndpoint struct { 120 | endpoint 121 | } 122 | 123 | func (api *Api) newAppEndpoint(dirs ...interface{}) *appEndpoint { 124 | return &appEndpoint{api.newEndpoint(dirs...)} 125 | } 126 | 127 | func (t *appEndpoint) Get(params url.Values) (*App, error) { 128 | ret := new(App) 129 | err := t.api.get(t.base, params, ret) 130 | return ret, err 131 | } 132 | -------------------------------------------------------------------------------- /soundcloud/example_test.go.old: -------------------------------------------------------------------------------- 1 | package soundcloud 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | ) 7 | 8 | // ExampleNew sets up the whole soundcloud API 9 | func ExampleNew() { 10 | apiWithoutAuthenticatedUser := New("YOUR_CLIENT_ID", "") 11 | if _, err := apiWithoutAuthenticatedUser.Users(nil); err != nil { 12 | panic(err) 13 | } 14 | fmt.Println("Successfully created soundcloud.Api without user credentials") 15 | 16 | apiAuthenticatedUser := New("", "access_token") 17 | if ok, err := apiAuthenticatedUser.VerifyCredentials(); !ok { 18 | panic(err) 19 | return 20 | } 21 | fmt.Println("Successfully created soundcloud.Api with user credentials") 22 | } 23 | 24 | // ExampleApi_GetUser shows how to get a user object 25 | func ExampleApi_GetUser() { 26 | api := New("client_id" /* or */, "access_token") 27 | 28 | userResponse, err := api.GetUser("user-id", nil) 29 | if err != nil { 30 | panic(err) 31 | } 32 | 33 | user := userResponse.User 34 | processUser(user) 35 | } 36 | 37 | // ExampleApi_GetUserSearch_Params shows how to use parameters 38 | func ExampleApi_GetUserSearch_Params() { 39 | api := New("" /* need */, "access_token") 40 | 41 | params := url.Values{} 42 | params.Set("count", "5") // Get 5 users 43 | params.Set("q", "jack") // Search for user "jack" 44 | 45 | usersResponse, err := api.GetUserSearch(params) 46 | if err != nil { 47 | panic(err) 48 | } 49 | 50 | for _, user := range usersResponse.Users { 51 | processUser(&user) 52 | } 53 | } 54 | 55 | // ExampleApi_GetMediaPopular shows how you can paginate through popular media 56 | func ExampleApi_GetMediaPopular() { 57 | api := New("client_id" /* or */, "access_token") 58 | 59 | mediasResponse, err := api.GetMediaPopular(nil) 60 | if err != nil { 61 | panic(err) 62 | } 63 | 64 | for _, media := range mediasResponse.Medias { 65 | processMedia(&media) 66 | } 67 | } 68 | 69 | // ExampleApi_IterateMedia shows how to use iteration on a channel to avoid the complex pagination calls 70 | func ExampleApi_IterateMedia() { 71 | api := New("client_id" /* or */, "access_token") 72 | 73 | mediasResponse, err := api.GetUserRecentMedia("user-id", nil) 74 | if err != nil { 75 | panic(err) 76 | } 77 | 78 | // Stop 30 days ago 79 | doneChan := make(chan bool) 80 | 81 | mediaIter, errChan := api.IterateMedia(mediasResponse, doneChan /* optional */) 82 | for media := range mediaIter { 83 | processMedia(media) 84 | 85 | if isDone(media) { 86 | close(doneChan) // Signal to iterator to quit 87 | break 88 | } 89 | } 90 | 91 | // When mediaIter is closed, errChan will either have a single error on it or it will have been closed so this is safe. 92 | if err := <-errChan; err != nil { 93 | panic(err) 94 | } 95 | } 96 | 97 | func processMedia(m *Media) {} 98 | func isDone(m *Media) bool { 99 | return false 100 | } 101 | func processUser(u *User) {} 102 | -------------------------------------------------------------------------------- /soundcloud/groups.go: -------------------------------------------------------------------------------- 1 | package soundcloud 2 | 3 | import ( 4 | "net/url" 5 | ) 6 | 7 | type GroupApi struct { 8 | groupEndpoint 9 | } 10 | 11 | func (api *Api) Groups(params url.Values) ([]*Group, error) { 12 | ret := make([]*Group, 0) 13 | err := api.get("/groups", params, &ret) 14 | return ret, err 15 | } 16 | 17 | func (api *Api) Group(id uint64) *GroupApi { 18 | return &GroupApi{*api.newGroupEndpoint("groups", id)} 19 | } 20 | 21 | func (g *GroupApi) Moderators(params url.Values) ([]*User, error) { 22 | ret := make([]*User, 0) 23 | err := g.api.get(g.base+"/moderators", params, &ret) 24 | return ret, err 25 | } 26 | 27 | func (g *GroupApi) Members(params url.Values) ([]*User, error) { 28 | ret := make([]*User, 0) 29 | err := g.api.get(g.base+"/members", params, &ret) 30 | return ret, err 31 | } 32 | 33 | func (g *GroupApi) Contributors(params url.Values) ([]*User, error) { 34 | ret := make([]*User, 0) 35 | err := g.api.get(g.base+"/contributors", params, &ret) 36 | return ret, err 37 | } 38 | 39 | func (g *GroupApi) Users(params url.Values) ([]*User, error) { 40 | ret := make([]*User, 0) 41 | err := g.api.get(g.base+"/users", params, &ret) 42 | return ret, err 43 | } 44 | 45 | func (g *GroupApi) Tracks(params url.Values) ([]*Track, error) { 46 | ret := make([]*Track, 0) 47 | err := g.api.get(g.base+"/tracks", params, &ret) 48 | return ret, err 49 | } 50 | 51 | func (g *GroupApi) PendingTracks(params url.Values) ([]*Track, error) { 52 | ret := make([]*Track, 0) 53 | err := g.api.get(g.base+"/pending_tracks", params, &ret) 54 | return ret, err 55 | } 56 | 57 | func (g *GroupApi) PendingTrack(id uint64) *trackEndpoint { 58 | return g.api.newTrackEndpoint(g.base, "pending_tracks", id) 59 | } 60 | 61 | func (g *GroupApi) Contributions(params url.Values) ([]*Track, error) { 62 | ret := make([]*Track, 0) 63 | err := g.api.get(g.base+"/contributions", params, &ret) 64 | return ret, err 65 | } 66 | 67 | func (g *GroupApi) Contribution(id uint64) *trackEndpoint { 68 | return g.api.newTrackEndpoint(g.base, "contributions", id) 69 | } 70 | -------------------------------------------------------------------------------- /soundcloud/helpers.go: -------------------------------------------------------------------------------- 1 | package soundcloud 2 | 3 | import ( 4 | "net/url" 5 | ) 6 | 7 | func Values(keyValues ...string) url.Values { 8 | v := url.Values{} 9 | for i := 0; i < len(keyValues)-1; i += 2 { 10 | v.Set(keyValues[i], keyValues[i+1]) 11 | } 12 | return v 13 | } 14 | 15 | func (api *Api) Values(keyValues ...string) url.Values { 16 | return Values(keyValues...) 17 | } 18 | -------------------------------------------------------------------------------- /soundcloud/helpers_test.go: -------------------------------------------------------------------------------- 1 | package soundcloud 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | var doAuthorizedRequests bool 9 | var api *Api 10 | var ladygaga_id uint64 = 55213175 11 | var thriftshop_id uint64 = 57886117 12 | var macklemore_id uint64 = 557633 13 | var joneisen_id uint64 = 557633 14 | 15 | // -- helpers -- 16 | 17 | func init() { 18 | doAuthorizedRequests = (TestConfig["access_token"] != "") 19 | if !doAuthorizedRequests { 20 | fmt.Println("*** Authorized requests will not performed because no access_token was specified in config_test.go") 21 | } 22 | api = createApi() 23 | } 24 | 25 | func authorizedRequest(t *testing.T) { 26 | if !doAuthorizedRequests { 27 | t.Skip("Access Token not provided.") 28 | } 29 | } 30 | 31 | func createApi() *Api { 32 | api := &Api{ 33 | ClientId: TestConfig["client_id"].(string), 34 | ClientSecret: TestConfig["client_secret"].(string), 35 | AccessToken: TestConfig["access_token"].(string), 36 | RefreshToken: TestConfig["refresh_token"].(string), 37 | } 38 | 39 | _ = api.Refresh() 40 | return api 41 | } 42 | -------------------------------------------------------------------------------- /soundcloud/me.go: -------------------------------------------------------------------------------- 1 | package soundcloud 2 | 3 | import ( 4 | "net/url" 5 | ) 6 | 7 | type MeApi struct { 8 | UserApi 9 | } 10 | 11 | func (api *Api) Me() *MeApi { 12 | return &MeApi{UserApi{*api.newUserEndpoint("me", true)}} 13 | } 14 | 15 | func (me *MeApi) Connections(params url.Values) ([]*User, error) { 16 | ret := make([]*User, 0) 17 | err := me.api.get(me.base+"/connections", params, &ret, true) 18 | return ret, err 19 | } 20 | 21 | func (me *MeApi) Connection(id uint64) *userEndpoint { 22 | return me.api.newUserEndpoint(me.base, "connections", id) 23 | } 24 | 25 | func (me *MeApi) Activities(params url.Values) (*PaginatedActivities, error) { 26 | ret := new(PaginatedActivities) 27 | err := me.api.get(me.base+"/activities", params, ret, true) 28 | return ret, err 29 | } 30 | -------------------------------------------------------------------------------- /soundcloud/me_test.go: -------------------------------------------------------------------------------- 1 | package soundcloud 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestMe(t *testing.T) { 8 | authorizedRequest(t) 9 | res, err := api.Me().Get(nil) 10 | if err != nil { 11 | t.Error(err) 12 | } else if res.Id != TestConfig["my_id"].(uint64) { 13 | t.Error("Idcheck failed", res) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /soundcloud/oauth.go: -------------------------------------------------------------------------------- 1 | package soundcloud 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | func (api *Api) Refresh() error { 8 | if api.ClientId == "" || api.ClientSecret == "" || api.RefreshToken == "" { 9 | return fmt.Errorf("ClientId, ClientSecret, and RefreshToken must all be specified") 10 | } 11 | 12 | values := Values( 13 | "client_id", api.ClientId, 14 | "client_secret", api.ClientSecret, 15 | "refresh_token", api.RefreshToken, 16 | "grant_type", "refresh_token", 17 | ) 18 | 19 | ret := new(AuthResponse) 20 | err := api.post("/oauth2/token", values, ret) 21 | 22 | if err == nil { 23 | api.AccessToken = ret.AccessToken 24 | api.RefreshToken = ret.RefreshToken 25 | } 26 | 27 | return err 28 | } 29 | -------------------------------------------------------------------------------- /soundcloud/oembed/oembed.go: -------------------------------------------------------------------------------- 1 | // Package oembed provides structure definitions for the [OEmbed](http://oembed.com) open standard 2 | package oembed 3 | 4 | // Response (Section 2.3.4) 5 | type Response struct { 6 | Type string `json:"type,omitempty"` 7 | Version float `json:"version,omitempty"` 8 | Title string `json:"title,omitempty"` 9 | AuthorName string `json:"author_name,omitempty"` 10 | AuthorUrl string `json:"author_url,omitempty"` 11 | ProviderName string `json:"provider_name,omitempty"` 12 | ProviderUrl string `json:"provider_url,omitempty"` 13 | CacheAge uint64 `json:"cache_age,omitempty"` 14 | ThumbnailUrl string `json:"thumbnail_url,omitempty"` 15 | 16 | // Despite what the standard says, may clients treat this as an html height/width parameter and use strings here sometimes like "100%" 17 | ThumbnailWidth interface{} `json:"thumbnail_width,omitempty"` 18 | ThumbnailHeight interface{} `json:"thumbnail_height,omitempty"` 19 | 20 | Html string `json:"url,omitempty"` 21 | Width interface{} `json:"width,omitempty"` 22 | Height interface{} `json:"height,omitempty"` 23 | } 24 | -------------------------------------------------------------------------------- /soundcloud/playlists.go: -------------------------------------------------------------------------------- 1 | package soundcloud 2 | 3 | import ( 4 | "net/url" 5 | ) 6 | 7 | type PlaylistApi struct { 8 | playlistEndpoint 9 | } 10 | 11 | func (api *Api) Playlists(params url.Values) ([]*Playlist, error) { 12 | ret := make([]*Playlist, 0) 13 | err := api.get("/playlists", params, &ret) 14 | return ret, err 15 | } 16 | 17 | func (api *Api) Playlist(id uint64) *PlaylistApi { 18 | return &PlaylistApi{*api.newPlaylistEndpoint("playlists", id)} 19 | } 20 | 21 | // No idea how these endpoints works 22 | // func (t *PlaylistApi) SharedToUsers() (*usersEndpoint) { 23 | // func (t *PlaylistApi) SharedToEmails() (*emailsEndpoint) { 24 | // func (t *PlaylistApi) SecretToken() (*tokenEndpoint) 25 | -------------------------------------------------------------------------------- /soundcloud/resolve.go: -------------------------------------------------------------------------------- 1 | package soundcloud 2 | 3 | import ( 4 | "errors" 5 | "net/url" 6 | ) 7 | 8 | func (api *Api) Resolve(scurl string) (uri *url.URL, err error) { 9 | params, err := api.extendParams(Values("url", scurl)) 10 | if err != nil { 11 | return nil, err 12 | } 13 | 14 | req, err := buildGetRequest(urlify("/resolve"), params) 15 | if err != nil { 16 | return nil, err 17 | } 18 | 19 | resp, err := client.Do(req) 20 | if urlerr, ok := err.(*url.Error); ok && urlerr.Err.Error() == "No redirects!" { 21 | err = nil 22 | } else if err != nil { 23 | return nil, err 24 | } 25 | 26 | loc, err := resp.Location() 27 | if loc == nil { 28 | return nil, errors.New("empty location") 29 | } 30 | 31 | return loc, nil 32 | } 33 | -------------------------------------------------------------------------------- /soundcloud/resolve_test.go: -------------------------------------------------------------------------------- 1 | package soundcloud 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestResolve(t *testing.T) { 8 | ret, err := api.Resolve("http://soundcloud.com/matas/hobnotropic") 9 | if err != nil { 10 | t.Error(err) 11 | } else if ret.Path != "/tracks/49931.json" { 12 | t.Error("resolve didn't work " + ret.String()) 13 | } 14 | } 15 | 16 | func TestResolveEmptyUrl(t *testing.T) { 17 | _, err := api.Resolve("http://soundcloud.com/xxx/yyy") 18 | if err == nil { 19 | t.Error("err is nil") 20 | } 21 | 22 | if err.Error() != "empty location" { 23 | t.Error("wrong error") 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /soundcloud/tracks.go: -------------------------------------------------------------------------------- 1 | package soundcloud 2 | 3 | import ( 4 | "net/url" 5 | ) 6 | 7 | type TrackApi struct { 8 | trackEndpoint 9 | } 10 | 11 | func (api *Api) Tracks(params url.Values) ([]*Track, error) { 12 | ret := make([]*Track, 0) 13 | err := api.get("/tracks", params, &ret) 14 | return ret, err 15 | } 16 | 17 | func (api *Api) Track(id uint64) *TrackApi { 18 | return &TrackApi{*api.newTrackEndpoint("tracks", id)} 19 | } 20 | 21 | func (t *TrackApi) Comments(params url.Values) ([]*Comment, error) { 22 | ret := make([]*Comment, 0) 23 | err := t.api.get(t.base+"/comments", params, &ret) 24 | return ret, err 25 | } 26 | 27 | func (t *TrackApi) Comment(id uint64) *commentEndpoint { 28 | return t.api.newCommentEndpoint(t.base, "comments", id) 29 | } 30 | 31 | func (t *TrackApi) Favorites(params url.Values) ([]*User, error) { 32 | ret := make([]*User, 0) 33 | err := t.api.get(t.base+"/favorites", params, &ret) 34 | return ret, err 35 | } 36 | 37 | func (t *TrackApi) Favorite(id uint64) *userEndpoint { 38 | return t.api.newUserEndpoint(t.base, "favorites", id) 39 | } 40 | 41 | // No idea how these endpoints works 42 | // func (t *TrackApi) SharedToUsers() (*usersEndpoint) { 43 | // func (t *TrackApi) SharedToEmails() (*emailsEndpoint) { 44 | // func (t *TrackApi) SecretToken() (*tokenEndpoint) 45 | -------------------------------------------------------------------------------- /soundcloud/tracks_test.go: -------------------------------------------------------------------------------- 1 | package soundcloud 2 | 3 | import ( 4 | "net/url" 5 | "testing" 6 | ) 7 | 8 | func TestTracks(t *testing.T) { 9 | ret, err := api.Tracks(url.Values{"q": []string{"welcome to night vale"}}) 10 | if err != nil { 11 | t.Error(err) 12 | } else if len(ret) == 0 { 13 | t.Error("query returned no values") 14 | } else if ret[0].User.Username != "planetoffinks" { 15 | t.Error("planetoffinks wasn't the creator first response?!") 16 | } 17 | } 18 | 19 | func TestTrackGet(t *testing.T) { 20 | ret, err := api.Track(thriftshop_id).Get(nil) 21 | if err != nil { 22 | t.Error(err) 23 | } else if ret.Id != thriftshop_id { 24 | t.Error("id didn't come back as requested") 25 | } else if ret.Permalink != "macklemore-x-ryan-lewis-thrift" { 26 | t.Error("thrift shop's permalink changed?") 27 | } else if ret.Title != "Macklemore X Ryan Lewis - Thrift Shop feat. Wanz" { 28 | t.Error("thrift shop title changed?") 29 | } else if ret.UserId != macklemore_id || ret.UserId != ret.User.Id { 30 | t.Error("user object is messed up", ret.UserId, ret.User) 31 | } else if ret.User.Permalink != "macklemore" || ret.User.Username != "Macklemore & Ryan Lewis" { 32 | t.Error("user object for macklmore changed?") 33 | } else if ret.PlaybackCount < 1000 || ret.FavoritingsCount < 1000 || ret.CommentCount < 1000 { 34 | t.Error("Some counts are wrong?", ret) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /soundcloud/types.go: -------------------------------------------------------------------------------- 1 | package soundcloud 2 | 3 | // User Object (http://developers.soundcloud.com/docs/api/reference#token) 4 | type User struct { 5 | //---- Included in mini-representation 6 | Id uint64 7 | 8 | *SubUri 9 | *SubPermalink 10 | 11 | Username string 12 | AvatarUrl string `json:"avatar_url"` 13 | 14 | //---- Not included in mini-representation 15 | Country string 16 | FullName string `json:"full_name"` 17 | City string 18 | Description string 19 | DiscogsName string `json:"discogs-name"` 20 | MyspaceName string `json:"myspace-name"` 21 | Website string 22 | WebsiteTitle string `json:"website_title"` 23 | Online bool 24 | TrackCount uint64 `json:"track_count"` 25 | PlaylistCount uint64 `json:"playlist_count"` 26 | FollowersCount uint64 `json:"followers_count"` 27 | FollowingsCount uint64 `json:"followings_count"` 28 | PublicFavoritesCount uint64 `json:"public_favorites_count"` 29 | AvatarData []byte `json:"avatar_data"` 30 | } 31 | 32 | // Sound Track (http://developers.soundcloud.com/docs/api/reference#tracks) 33 | type Track struct { 34 | Id uint64 35 | 36 | *SubUri 37 | *SubLabel 38 | *SubUser 39 | *SubPermalink 40 | 41 | CreatedAt string `json:"created_at"` 42 | Title string 43 | Sharing string 44 | EmbeddableBy string `json:"embeddable_by"` 45 | PurchaseUrl string `json:"purchase_url"` 46 | ArtworkUrl string `json:"artwork_url"` 47 | Description string 48 | Duration uint64 49 | Genre string 50 | SharedToCount uint64 `json:"shared_to_count"` 51 | TagList string `json:"tag_list"` 52 | // Release uint64 // Release is sometimes "" and sometimes null and sometimes a number? 53 | ReleaseDay uint `json:"release_day"` 54 | ReleaseMonth uint `json:"release_month"` 55 | ReleaseYear uint `json:"release_year"` 56 | Streamable bool 57 | Downloadable bool 58 | State string 59 | License string 60 | TrackType string `json:"track_type"` 61 | WaveformUrl string `json:"waveform_url"` 62 | DownloadUrl string `json:"download_url"` 63 | StreamUrl string `json:"stream_url"` 64 | VideoUrl string `json:"video_url"` 65 | Bpm float64 66 | Commentable bool 67 | ISRC string `json:"isrc"` 68 | KeySignature string `json:"key_signature"` 69 | CommentCount uint64 `json:"comment_count"` 70 | DownloadCount uint64 `json:"download_count"` 71 | PlaybackCount uint64 `json:"playback_count"` 72 | FavoritingsCount uint64 `json:"favoritings_count"` 73 | OriginalFormat string `json:"original_format"` 74 | OriginalContentSize uint64 `json:"original_content_size"` 75 | CreatedWith *App 76 | AssetData []byte `json:"asset_data"` 77 | ArtworkData []byte `json:"artwork_data"` 78 | UserFavorite bool `json:"user_favorite"` 79 | } 80 | 81 | // A Soundcloud Set (http://developers.soundcloud.com/docs/api/reference#playlists) 82 | type Playlist struct { 83 | Id uint64 84 | 85 | *SubUri 86 | *SubLabel 87 | *SubUser 88 | *SubPermalink 89 | 90 | CreatedAt string `json:"created_at"` 91 | Title string 92 | Sharing string 93 | EmbeddableBy string `json:"embeddable_by"` 94 | PurchaseUrl string `json:"purchase_url"` 95 | ArtworkUrl string `json:"artwork_url"` 96 | Description string 97 | Duration uint64 98 | Genre string 99 | SharedToCount uint64 `json:"shared_to_count"` 100 | TagList string `json:"tag_list"` 101 | // Release uint64 //See release above 102 | ReleaseDay uint `json:"release_day"` 103 | ReleaseMonth uint `json:"release_month"` 104 | ReleaseYear uint `json:"release_year"` 105 | Streamable bool 106 | Downloadable bool 107 | EAN string `json:"ean"` 108 | PlaylistType string `json:"playlist_type"` 109 | Tracks []*Track 110 | } 111 | 112 | // Groups of members with tracks (http://developers.soundcloud.com/docs/api/reference#groups) 113 | type Group struct { 114 | Id uint64 115 | 116 | *SubPermalink 117 | 118 | *SubUri 119 | CreatedAt string `json:"created_at"` 120 | ArtworkUrl string `json:"artwork_url"` 121 | Name string 122 | Description string 123 | ShortDescription string `json:"short_description"` 124 | 125 | // A mini representation of user 126 | Creator *User 127 | } 128 | 129 | // Comment on a track (http://developers.soundcloud.com/docs/api/reference#comments) 130 | type Comment struct { 131 | Id uint64 132 | 133 | *SubUri 134 | *SubUser 135 | 136 | CreatedAt string `json:"created_at"` 137 | Body string 138 | // Time in milliseconds 139 | Timestamp uint64 140 | TrackId uint64 141 | } 142 | 143 | // A connection to an external service such as twitter, tumblr, facebook, etc (http://developers.soundcloud.com/docs/api/reference#connections) 144 | type Connection struct { 145 | Id uint64 146 | 147 | *SubUri 148 | 149 | CreatedAt string `json:"created_at"` 150 | DisplayName string `json:"display_name"` 151 | PostFavorite bool `json:"post_favorite"` 152 | PostPublish bool `json:"post_publish"` 153 | Service string 154 | Type string 155 | } 156 | 157 | // Activity by a user (http://developers.soundcloud.com/docs/api/reference#activities) 158 | type Activity struct { 159 | Type string 160 | CreatedAt string `json:"created_at"` 161 | Tags string 162 | // This is a specific type that should be re-interpreted as a given type (see activities page) 163 | Origin interface{} 164 | } 165 | 166 | type App struct { 167 | Id uint64 168 | Name string 169 | 170 | *SubUri 171 | *SubPermalink 172 | 173 | ExternalUrl string `json:"external_url"` 174 | Creator *User 175 | } 176 | 177 | // -- Special Response objects 178 | 179 | type PaginatedActivities struct { 180 | *Pagination 181 | Collection []Activity 182 | } 183 | 184 | // -- subobjects 185 | 186 | type SubUri struct { 187 | Uri string 188 | } 189 | 190 | type SubPermalink struct { 191 | // The name of the permalink. Sometimes not included (on App specifically) 192 | Permalink string 193 | // Always included 194 | PermalinkUrl string `json:"permalink_url"` 195 | } 196 | 197 | type SubLabel struct { 198 | Label *User 199 | LabelId uint64 `json:"label_id"` 200 | LabelName string `json:"label_name"` 201 | } 202 | 203 | type SubUser struct { 204 | UserId uint64 `json:"user_id"` 205 | // A minimal representation of user 206 | User *User 207 | } 208 | 209 | type Pagination struct { 210 | NextHref string `json:"next_href"` 211 | } 212 | 213 | type Resolution struct { 214 | Status string 215 | Location string 216 | } 217 | 218 | type AuthResponse struct { 219 | AccessToken string `json:"access_token"` 220 | RefreshToken string `json:"refresh_token"` 221 | ExpiresIn uint64 `json:"expires_in"` 222 | Scope string 223 | } 224 | -------------------------------------------------------------------------------- /soundcloud/users.go: -------------------------------------------------------------------------------- 1 | package soundcloud 2 | 3 | import ( 4 | "net/url" 5 | ) 6 | 7 | type UserApi struct { 8 | userEndpoint 9 | } 10 | 11 | func (api *Api) Users(params url.Values) ([]*User, error) { 12 | ret := make([]*User, 0) 13 | err := api.get("/users", params, &ret) 14 | return ret, err 15 | } 16 | 17 | func (api *Api) User(id uint64) *UserApi { 18 | return &UserApi{*api.newUserEndpoint("users", id)} 19 | } 20 | 21 | func (u *UserApi) Get(params url.Values) (*User, error) { 22 | ret := new(User) 23 | err := u.api.get(u.base, params, ret, u.authReq) 24 | return ret, err 25 | } 26 | 27 | func (u *UserApi) Tracks(params url.Values) ([]*Track, error) { 28 | ret := make([]*Track, 0) 29 | err := u.api.get(u.base+"/tracks", params, &ret) 30 | return ret, err 31 | } 32 | 33 | func (u *UserApi) Playlists(params url.Values) ([]*Playlist, error) { 34 | ret := make([]*Playlist, 0) 35 | err := u.api.get(u.base+"/playlists", params, &ret) 36 | return ret, err 37 | } 38 | 39 | func (u *UserApi) Followings(params url.Values) ([]*User, error) { 40 | ret := make([]*User, 0) 41 | err := u.api.get(u.base+"/followings", params, &ret) 42 | return ret, err 43 | } 44 | 45 | func (u *UserApi) Following(id uint64) *userEndpoint { 46 | return u.api.newUserEndpoint(u.base, "followings", id) 47 | } 48 | 49 | func (u *UserApi) Followers(params url.Values) ([]*User, error) { 50 | ret := make([]*User, 0) 51 | err := u.api.get(u.base+"/followers", params, &ret) 52 | return ret, err 53 | } 54 | 55 | func (u *UserApi) Follower(id string) *userEndpoint { 56 | return u.api.newUserEndpoint(u.base, "followers", id) 57 | } 58 | 59 | func (u *UserApi) Comments(params url.Values) ([]*Comment, error) { 60 | ret := make([]*Comment, 0) 61 | err := u.api.get(u.base+"/comments", params, &ret) 62 | return ret, err 63 | } 64 | 65 | func (u *UserApi) Favorites(params url.Values) ([]*Track, error) { 66 | ret := make([]*Track, 0) 67 | err := u.api.get(u.base+"/favorites", params, &ret) 68 | return ret, err 69 | } 70 | 71 | func (u *UserApi) Favorite(id string) *trackEndpoint { 72 | return u.api.newTrackEndpoint(u.base, "favorites", id) 73 | } 74 | 75 | func (u *UserApi) Groups(params url.Values) ([]*Group, error) { 76 | ret := make([]*Group, 0) 77 | err := u.api.get(u.base+"/groups", params, &ret) 78 | return ret, err 79 | } 80 | 81 | func (u *UserApi) WebProfiles(params url.Values) ([]*Connection, error) { 82 | ret := make([]*Connection, 0) 83 | err := u.api.get(u.base+"/web-profiles", params, &ret) 84 | return ret, err 85 | } 86 | -------------------------------------------------------------------------------- /soundcloud/users_test.go: -------------------------------------------------------------------------------- 1 | package soundcloud 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | "testing" 7 | ) 8 | 9 | func TestUsers(t *testing.T) { 10 | ret, err := api.Users(url.Values{"q": []string{"ladygaga"}}) 11 | if err != nil { 12 | t.Error(err) 13 | } else if len(ret) == 0 { 14 | t.Error("query returned no values") 15 | } else if ret[0].Username != "ladygaga" { 16 | t.Error("lady gaga wasn't the first response?!") 17 | } 18 | } 19 | 20 | func TestUser(t *testing.T) { 21 | ret, err := api.User(ladygaga_id).Get(nil) 22 | if err != nil { 23 | t.Error(err) 24 | } else if ret.Username != "ladygaga" { 25 | t.Error("lady gaga's id changed?") 26 | } else if ret.Permalink != "ladygaga" || ret.PermalinkUrl != "http://soundcloud.com/ladygaga" { 27 | t.Error("permalink error", ret) 28 | } else if ret.Uri != fmt.Sprintf("https://api.soundcloud.com/users/%d", ladygaga_id) { 29 | t.Error("uri", ret) 30 | } else if ret.Country != "United States" || ret.FullName != "Lady Gaga" || ret.City != "New York City" || 31 | ret.Website != "http://littlemonsters.com" || ret.WebsiteTitle != "LittleMonsters.com" || ret.FollowersCount < 1000 { 32 | 33 | t.Error("Something wrong with ladygaga's profile", ret) 34 | } 35 | } 36 | 37 | func TestUserTracks(t *testing.T) { 38 | ret, err := api.User(ladygaga_id).Tracks(nil) 39 | if err != nil { 40 | t.Error(err) 41 | } else if len(ret) == 0 { 42 | t.Error("lady gaga has no tracks?") 43 | } else { 44 | for _, tr := range ret { 45 | if tr.UserId != ladygaga_id || tr.User.Id != ladygaga_id { 46 | t.Error("lady gaga's track isn't owned by her?") 47 | } else if tr.CreatedAt == "" { 48 | t.Error("a track doesn't have a createdAt?") 49 | } 50 | } 51 | } 52 | } 53 | 54 | func TestUserTracksCreatedAtFrom(t *testing.T) { 55 | ret, err := api.User(macklemore_id).Tracks(api.Values("created_at[from]", "2013/03/17 18:37:51")) 56 | if err != nil { 57 | t.Error(err) 58 | } else if ret[len(ret)-1].CreatedAt != "2013/03/17 18:37:51 +0000" { 59 | t.Error("Created At didn't work right") 60 | } 61 | } 62 | 63 | func TestUserPlaylists(t *testing.T) { 64 | ret, err := api.User(ladygaga_id).Playlists(nil) 65 | if err != nil { 66 | t.Error(err) 67 | } else if len(ret) == 0 { 68 | t.Error("no playlists?") 69 | } else { 70 | for _, pl := range ret { 71 | if pl.UserId != ladygaga_id || pl.User.Id != ladygaga_id { 72 | t.Error("playlist has wrong user") 73 | } 74 | } 75 | } 76 | } 77 | 78 | func TestUserFollowings(t *testing.T) { 79 | _, err := api.User(joneisen_id).Followings(nil) 80 | if err != nil { 81 | t.Error(err) 82 | // } else if len(ret) != 1 { 83 | // t.Error("should be following only one user", ret) 84 | // } else if ret[0].Id != macklemore_id { 85 | // t.Error("Should be following macklemore") 86 | } 87 | } 88 | 89 | func TestUserFollowers(t *testing.T) { 90 | ret, err := api.User(ladygaga_id).Followers(nil) 91 | if err != nil { 92 | t.Error(err) 93 | } else if len(ret) == 0 { 94 | t.Error("should be followed by millions!") 95 | } 96 | } 97 | 98 | func TestUserComments(t *testing.T) { 99 | _, err := api.User(ladygaga_id).Comments(nil) 100 | if err != nil { 101 | t.Error(err) 102 | } 103 | } 104 | 105 | func TestUserFavorites(t *testing.T) { 106 | _, err := api.User(ladygaga_id).Favorites(nil) 107 | if err != nil { 108 | t.Error(err) 109 | } 110 | } 111 | 112 | func TestUserGroups(t *testing.T) { 113 | _, err := api.User(ladygaga_id).Groups(nil) 114 | if err != nil { 115 | t.Error(err) 116 | } 117 | } 118 | 119 | func TestUserWebProfiles(t *testing.T) { 120 | _, err := api.User(ladygaga_id).WebProfiles(nil) 121 | if err != nil { 122 | t.Error(err) 123 | } 124 | } 125 | --------------------------------------------------------------------------------