├── .gitignore ├── LICENSE ├── README.md ├── examples ├── gospshell │ └── gospshell.go └── portaudio │ └── portaudio.go └── spotify ├── error.go ├── libspotify.c ├── libspotify.go ├── libspotify.h ├── log.go └── log_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | *.key 2 | /examples/gospshell/gospshell 3 | /examples/portaudio/portaudio 4 | /tmp 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Golang libspotify bindings 2 | 3 | Package spotify adds language bindings for libspotify in Go. The libspotify C 4 | API package allows third-party developers to write applications that utilize 5 | the Spotify music streaming service. 6 | 7 | This is a work in progress. Expect this API to change. Patches are welcome. 8 | 9 | ## Installing 10 | 11 | ### Using *go get* 12 | 13 | $ go get github.com/op/go-libspotify/spotify 14 | 15 | After this command *spotify* is ready to use. Its source will be in: 16 | 17 | $GOPATH/src/pkg/github.com/op/go-libspotify/spotify 18 | 19 | You can use `go get -u -a` to update all installed packages. 20 | 21 | ## Documentation 22 | 23 | For docs, see http://godoc.org/github.com/op/go-libspotify/spotify or run: 24 | 25 | $ go doc github.com/op/go-libspotify/spotify 26 | -------------------------------------------------------------------------------- /examples/gospshell/gospshell.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Örjan Persson 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "bufio" 19 | "errors" 20 | "flag" 21 | "fmt" 22 | "io/ioutil" 23 | "log" 24 | "os" 25 | "os/signal" 26 | "path" 27 | "regexp" 28 | "strconv" 29 | "strings" 30 | "time" 31 | 32 | sp "github.com/op/go-libspotify/spotify" 33 | ) 34 | 35 | var ( 36 | appKeyPath = flag.String("key", "spotify_appkey.key", "path to app.key") 37 | username = flag.String("username", "o.p", "spotify username") 38 | password = flag.String("password", "", "spotify password") 39 | remember = flag.Bool("remember", false, "remember username and password") 40 | debug = flag.Bool("debug", false, "debug output") 41 | ) 42 | 43 | // command is the declaration for running a command. 44 | type command func(*sp.Session, []string, <-chan bool) error 45 | 46 | // commands contains all available commands. 47 | var commands = map[string]command{ 48 | "search": cmdSearch, 49 | "toplist": cmdToplist, 50 | "playlist": cmdPlaylist, 51 | "star": cmdStar, 52 | } 53 | 54 | var reCommand = regexp.MustCompile(`\s+`) 55 | 56 | func main() { 57 | flag.Parse() 58 | prog := path.Base(os.Args[0]) 59 | 60 | appKey, err := ioutil.ReadFile(*appKeyPath) 61 | if err != nil { 62 | log.Fatal(err) 63 | } 64 | 65 | println("libspotify", sp.BuildId()) 66 | session, err := sp.NewSession(&sp.Config{ 67 | ApplicationKey: appKey, 68 | ApplicationName: prog, 69 | CacheLocation: "tmp", 70 | SettingsLocation: "tmp", 71 | }) 72 | if err != nil { 73 | log.Fatal(err) 74 | } 75 | _ = session 76 | 77 | exit := make(chan bool) 78 | 79 | signals := make(chan os.Signal, 1) 80 | signal.Notify(signals, os.Interrupt, os.Kill) 81 | go func() { 82 | for _ = range signals { 83 | select { 84 | case exit <- true: 85 | default: 86 | } 87 | } 88 | }() 89 | go func() { 90 | exitAttempts := 0 91 | running := true 92 | for running { 93 | if *debug { 94 | println("waiting for connection state change", session.ConnectionState()) 95 | } 96 | 97 | select { 98 | case err := <-session.LoggedInUpdates(): 99 | if *debug { 100 | println("!! login updated", err) 101 | } 102 | case <-session.LoggedOutUpdates(): 103 | if *debug { 104 | println("!! logout updated") 105 | } 106 | running = false 107 | break 108 | case err := <-session.ConnectionErrorUpdates(): 109 | if *debug { 110 | println("!! connection error", err.Error()) 111 | } 112 | case msg := <-session.MessagesToUser(): 113 | println("!! message to user", msg) 114 | case message := <-session.LogMessages(): 115 | if *debug { 116 | println("!! log message", message.String()) 117 | } 118 | case _ = <-session.CredentialsBlobUpdates(): 119 | if *debug { 120 | println("!! blob updated") 121 | } 122 | case <-session.ConnectionStateUpdates(): 123 | if *debug { 124 | println("!! connstate", session.ConnectionState()) 125 | } 126 | case <-exit: 127 | if *debug { 128 | println("!! exiting") 129 | } 130 | if exitAttempts >= 3 { 131 | os.Exit(42) 132 | } 133 | exitAttempts++ 134 | session.Logout() 135 | case <-time.After(5 * time.Second): 136 | if *debug { 137 | println("state change timeout") 138 | } 139 | } 140 | } 141 | 142 | session.Close() 143 | os.Exit(32) 144 | }() 145 | 146 | if len(*password) > 0 { 147 | credentials := sp.Credentials{ 148 | Username: *username, 149 | Password: *password, 150 | } 151 | if err = session.Login(credentials, *remember); err != nil { 152 | log.Fatal(err) 153 | } 154 | } else { 155 | if err = session.Relogin(); err != nil { 156 | log.Fatal(err) 157 | } 158 | } 159 | 160 | scanner := bufio.NewScanner(os.Stdin) 161 | for { 162 | fmt.Printf("%s:> ", prog) 163 | if !scanner.Scan() { 164 | break 165 | } 166 | 167 | line := strings.Trim(scanner.Text(), " ") 168 | args := reCommand.Split(line, -1) 169 | fmt.Println("%#v", args) 170 | if len(args) == 0 || args[0] == "" { 171 | continue 172 | } 173 | cmd := commands[args[0]] 174 | if cmd == nil { 175 | fmt.Fprintf(os.Stderr, "%s: unknown command: %s\n", prog, args[0]) 176 | cmd = cmdHelp 177 | } 178 | 179 | if err := cmd(session, args, nil); err != nil { 180 | fmt.Fprintf(os.Stderr, "%s: %s\n", args[0], err) 181 | continue 182 | } 183 | } 184 | if err := scanner.Err(); err != nil { 185 | log.Fatal(err) 186 | } 187 | } 188 | 189 | func trackStr(track *sp.Track) string { 190 | track.Wait() 191 | 192 | var artists []string 193 | for i := 0; i < track.Artists(); i++ { 194 | artists = append(artists, track.Artist(i).Name()) 195 | } 196 | return fmt.Sprintf("%s ♫ %s ❂ %s ♪ %s", 197 | track.Link(), 198 | strings.Join(artists, ", "), 199 | track.Album().Name(), 200 | track.Name(), 201 | ) 202 | } 203 | 204 | func albumStr(album *sp.Album) string { 205 | album.Wait() 206 | return fmt.Sprintf("%s ♫ %s ❂ %s", 207 | album.Link(), 208 | album.Artist().Name(), 209 | album.Name(), 210 | ) 211 | } 212 | 213 | func artistStr(artist *sp.Artist) string { 214 | artist.Wait() 215 | return fmt.Sprintf("%s ♫ %s", 216 | artist.Link(), 217 | artist.Name(), 218 | ) 219 | } 220 | 221 | func playlistStr(playlist *sp.Playlist) string { 222 | playlist.Wait() 223 | return fmt.Sprintf("%s ♫ %s", 224 | playlist.Link(), 225 | playlist.Name(), 226 | ) 227 | } 228 | 229 | // cmdHelp displays available commands. 230 | func cmdHelp(session *sp.Session, args []string, abort <-chan bool) error { 231 | println("use -h for more information") 232 | for command := range commands { 233 | println(" - ", command) 234 | } 235 | return nil 236 | } 237 | 238 | // cmdSearch searches for music. 239 | func cmdSearch(session *sp.Session, args []string, abort <-chan bool) error { 240 | var f = flag.NewFlagSet(args[0], flag.ContinueOnError) 241 | opts := struct { 242 | track, album, artist, playlist *bool 243 | offset, limit *int 244 | }{ 245 | track: f.Bool("track", false, "include tracks"), 246 | album: f.Bool("album", false, "include albums"), 247 | artist: f.Bool("artist", false, "include artists"), 248 | playlist: f.Bool("playlist", false, "include playlists"), 249 | 250 | offset: f.Int("offset", 0, "result offset"), 251 | limit: f.Int("limit", 10, "result count limitation"), 252 | } 253 | 254 | if err := f.Parse(args[1:]); err != nil { 255 | return err 256 | } else if f.NArg() == 0 { 257 | return errors.New("expected query string") 258 | } 259 | 260 | // Set all values to true if none are request. 261 | if !*opts.track && !*opts.album && !*opts.artist && !*opts.playlist { 262 | *opts.track = true 263 | *opts.album = true 264 | *opts.artist = true 265 | *opts.playlist = true 266 | } 267 | 268 | query := strings.Join(f.Args(), " ") 269 | 270 | var sOpts sp.SearchOptions 271 | spec := sp.SearchSpec{*opts.offset, *opts.limit} 272 | 273 | if *opts.track { 274 | sOpts.Tracks = spec 275 | } 276 | if *opts.album { 277 | sOpts.Albums = spec 278 | } 279 | if *opts.artist { 280 | sOpts.Artists = spec 281 | } 282 | if *opts.playlist { 283 | sOpts.Playlists = spec 284 | } 285 | 286 | // TODO cancel wait when abort<-true 287 | search, err := session.Search(query, &sOpts) 288 | if err != nil { 289 | return err 290 | } 291 | search.Wait() 292 | 293 | println("###done searching", search.Tracks(), search.TotalTracks(), search.Query(), search.Link().String()) 294 | 295 | for i := 0; i < search.Tracks(); i++ { 296 | println(trackStr(search.Track(i))) 297 | } 298 | for i := 0; i < search.Albums(); i++ { 299 | println(albumStr(search.Album(i))) 300 | } 301 | for i := 0; i < search.Artists(); i++ { 302 | println(artistStr(search.Artist(i))) 303 | } 304 | // TODO playlist 305 | 306 | return nil 307 | } 308 | 309 | // cmdToplist displays toplists based on region and entity. 310 | func cmdToplist(session *sp.Session, args []string, abort <-chan bool) error { 311 | f := flag.NewFlagSet(args[0], flag.ContinueOnError) 312 | opts := struct { 313 | track, album, artist *bool 314 | user *bool 315 | offset, limit *int 316 | }{ 317 | track: f.Bool("track", false, "include tracks"), 318 | album: f.Bool("album", false, "include albums"), 319 | artist: f.Bool("artist", false, "include artists"), 320 | 321 | user: f.Bool("user", false, "query for username instead of region"), 322 | 323 | offset: f.Int("offset", 0, "result offset"), 324 | limit: f.Int("limit", 10, "result count limitation"), 325 | } 326 | 327 | if err := f.Parse(args[1:]); err != nil { 328 | return err 329 | } else if f.NArg() > 1 { 330 | return errors.New("too many arguments") 331 | } 332 | 333 | // Set all values to true if none are request. 334 | if !*opts.track && !*opts.album && !*opts.artist { 335 | *opts.track = true 336 | *opts.album = true 337 | *opts.artist = true 338 | } 339 | 340 | var user *sp.User 341 | var region = sp.ToplistRegionEverywhere 342 | 343 | if *opts.user { 344 | var err error 345 | if f.NArg() == 1 { 346 | user, err = session.GetUser(f.Arg(0)) 347 | } else { 348 | user, err = session.CurrentUser() 349 | } 350 | if err != nil { 351 | return err 352 | } 353 | user.Wait() 354 | } else { 355 | if f.NArg() > 0 { 356 | var err error 357 | if region, err = sp.NewToplistRegion(f.Arg(0)); err != nil { 358 | return errors.New("Either specify country (eg. SE) or * for worldwide") 359 | } 360 | } 361 | } 362 | 363 | if *opts.track { 364 | var toplist *sp.TracksToplist 365 | if *opts.user { 366 | toplist = user.TracksToplist() 367 | } else { 368 | toplist = session.TracksToplist(region) 369 | } 370 | toplist.Wait() 371 | println("tracks toplist loaded", region.String(), toplist.Duration().String()) 372 | for i := *opts.offset; i < toplist.Tracks() && i < *opts.limit; i++ { 373 | println(trackStr(toplist.Track(i))) 374 | } 375 | } 376 | if *opts.album { 377 | var toplist *sp.AlbumsToplist 378 | if *opts.user { 379 | toplist = user.AlbumsToplist() 380 | } else { 381 | toplist = session.AlbumsToplist(region) 382 | } 383 | toplist.Wait() 384 | println("albums toplist loaded", region.String(), toplist.Duration().String()) 385 | for i := *opts.offset; i < toplist.Albums() && i < *opts.limit; i++ { 386 | println(albumStr(toplist.Album(i))) 387 | } 388 | } 389 | if *opts.artist { 390 | var toplist *sp.ArtistsToplist 391 | if *opts.user { 392 | toplist = user.ArtistsToplist() 393 | } else { 394 | toplist = session.ArtistsToplist(region) 395 | } 396 | toplist.Wait() 397 | println("artists toplist loaded", region.String(), toplist.Duration().String()) 398 | for i := *opts.offset; i < toplist.Artists() && i < *opts.limit; i++ { 399 | println(artistStr(toplist.Artist(i))) 400 | } 401 | } 402 | 403 | return nil 404 | } 405 | 406 | // cmdPlaylist lists all playlists or contents of one playlist. 407 | func cmdPlaylist(session *sp.Session, args []string, abort <-chan bool) error { 408 | var f = flag.NewFlagSet(args[0], flag.ContinueOnError) 409 | opts := struct { 410 | offset, limit *int 411 | }{ 412 | offset: f.Int("offset", 0, "result offset"), 413 | limit: f.Int("limit", 10, "result count limitation"), 414 | } 415 | 416 | if err := f.Parse(args[1:]); err != nil { 417 | return err 418 | } else if f.NArg() >= 2 { 419 | return errors.New("too many arguments") 420 | } 421 | 422 | // TODO cancel wait when abort<-true 423 | if f.NArg() == 0 { 424 | playlists, err := session.Playlists() 425 | if err != nil { 426 | return err 427 | } 428 | playlists.Wait() 429 | 430 | println("###done playlisting") 431 | 432 | indent := 0 433 | for i := *opts.offset; i < playlists.Playlists() && i < *opts.limit; i++ { 434 | switch playlists.PlaylistType(i) { 435 | case sp.PlaylistTypeStartFolder: 436 | if folder, err := playlists.Folder(i); err == nil { 437 | print(strings.Repeat(" ", indent)) 438 | println(folder.Name()) 439 | } 440 | indent += 2 441 | case sp.PlaylistTypeEndFolder: 442 | indent -= 2 443 | case sp.PlaylistTypePlaylist: 444 | print(strings.Repeat(" ", indent)) 445 | println(playlistStr(playlists.Playlist(i))) 446 | } 447 | } 448 | } else { 449 | var playlist *sp.Playlist 450 | 451 | // Argument is either uri or a playlist index 452 | if n, err := strconv.Atoi(f.Arg(0)); err == nil { 453 | playlists, err := session.Playlists() 454 | if err != nil { 455 | return err 456 | } 457 | playlists.Wait() 458 | // TODO do not panic on invalid index 459 | playlist = playlists.Playlist(n) 460 | } else { 461 | link, err := session.ParseLink(f.Arg(0)) 462 | if err != nil { 463 | return err 464 | } 465 | if link.Type() != sp.LinkTypePlaylist { 466 | return errors.New("invalid link type") 467 | } 468 | playlist, err = link.Playlist() 469 | if err != nil { 470 | return err 471 | } 472 | } 473 | 474 | playlist.Wait() 475 | println(playlistStr(playlist)) 476 | for i := *opts.offset; i < playlist.Tracks() && i < *opts.limit; i++ { 477 | pt := playlist.Track(i) 478 | println(trackStr(pt.Track())) 479 | } 480 | } 481 | 482 | return nil 483 | } 484 | 485 | // cmdStar lists all starred tracks. 486 | func cmdStar(session *sp.Session, args []string, abort <-chan bool) error { 487 | var f = flag.NewFlagSet(args[0], flag.ContinueOnError) 488 | opts := struct { 489 | offset, limit *int 490 | user *string 491 | }{ 492 | offset: f.Int("offset", 0, "result offset"), 493 | limit: f.Int("limit", 10, "result count limitation"), 494 | user: f.String("user", "", "query for username instead of logged in user"), 495 | } 496 | 497 | if err := f.Parse(args[1:]); err != nil { 498 | return err 499 | } else if f.NArg() >= 2 { 500 | return errors.New("too many arguments") 501 | } 502 | 503 | // TODO cancel wait when abort<-true 504 | if f.NArg() == 0 { 505 | var starred *sp.Playlist 506 | if *opts.user == "" { 507 | starred = session.Starred() 508 | } else { 509 | user, err := session.GetUser(*opts.user) 510 | if err != nil { 511 | return err 512 | } 513 | user.Wait() 514 | starred = user.Starred() 515 | } 516 | starred.Wait() 517 | println("###done starred") 518 | 519 | for i := *opts.offset; i < starred.Tracks() && i < *opts.limit; i++ { 520 | pt := starred.Track(i) 521 | println(trackStr(pt.Track())) 522 | } 523 | } else { 524 | link, err := session.ParseLink(f.Arg(0)) 525 | if err != nil { 526 | return err 527 | } 528 | track, err := link.Track() 529 | if err != nil { 530 | return err 531 | } 532 | 533 | _ = track 534 | 535 | // TODO fix method 536 | return errors.New("not implemented") 537 | // session.SetStarred( 538 | // []*Track{track}, 539 | // track.IsStarred() == false 540 | // ) 541 | } 542 | 543 | return nil 544 | } 545 | -------------------------------------------------------------------------------- /examples/portaudio/portaudio.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Örjan Persson 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "flag" 19 | "fmt" 20 | "io/ioutil" 21 | "log" 22 | "os" 23 | "os/signal" 24 | "path" 25 | "strings" 26 | "sync" 27 | "syscall" 28 | "time" 29 | 30 | "code.google.com/p/portaudio-go/portaudio" 31 | "github.com/op/go-libspotify/spotify" 32 | "github.com/visionmedia/go-spin" 33 | ) 34 | 35 | var ( 36 | appKeyPath = flag.String("key", "spotify_appkey.key", "path to app.key") 37 | username = flag.String("username", "o.p", "spotify username") 38 | password = flag.String("password", "", "spotify password") 39 | debug = flag.Bool("debug", false, "debug output") 40 | ) 41 | 42 | var ( 43 | // audioInputBufferSize is the number of delivered data from libspotify before 44 | // we start rejecting it to deliver any more. 45 | audioInputBufferSize = 8 46 | 47 | // audioOutputBufferSize is the maximum number of bytes to buffer before 48 | // passing it to PortAudio. 49 | audioOutputBufferSize = 8192 50 | ) 51 | 52 | // audio wraps the delivered Spotify data into a single struct. 53 | type audio struct { 54 | format spotify.AudioFormat 55 | frames []byte 56 | } 57 | 58 | // audioWriter takes audio from libspotify and outputs it through PortAudio. 59 | type audioWriter struct { 60 | input chan audio 61 | quit chan bool 62 | wg sync.WaitGroup 63 | } 64 | 65 | // newAudioWriter creates a new audioWriter handler. 66 | func newAudioWriter() (*audioWriter, error) { 67 | w := &audioWriter{ 68 | input: make(chan audio, audioInputBufferSize), 69 | quit: make(chan bool, 1), 70 | } 71 | 72 | stream, err := newPortAudioStream() 73 | if err != nil { 74 | return w, err 75 | } 76 | 77 | w.wg.Add(1) 78 | go w.streamWriter(stream) 79 | return w, nil 80 | } 81 | 82 | // Close stops and closes the audio stream and terminates PortAudio. 83 | func (w *audioWriter) Close() error { 84 | select { 85 | case w.quit <- true: 86 | default: 87 | } 88 | w.wg.Wait() 89 | return nil 90 | } 91 | 92 | // WriteAudio implements the spotify.AudioWriter interface. 93 | func (w *audioWriter) WriteAudio(format spotify.AudioFormat, frames []byte) int { 94 | select { 95 | case w.input <- audio{format, frames}: 96 | return len(frames) 97 | default: 98 | return 0 99 | } 100 | } 101 | 102 | // streamWriter reads data from the input buffer and writes it to the output 103 | // portaudio buffer. 104 | func (w *audioWriter) streamWriter(stream *portAudioStream) { 105 | defer w.wg.Done() 106 | defer stream.Close() 107 | 108 | buffer := make([]int16, audioOutputBufferSize) 109 | output := buffer[:] 110 | 111 | for { 112 | // Wait for input data or signal to quit. 113 | var input audio 114 | select { 115 | case input = <-w.input: 116 | case <-w.quit: 117 | return 118 | } 119 | 120 | // Initialize the audio stream based on the specification of the input format. 121 | err := stream.Stream(&output, input.format.Channels, input.format.SampleRate) 122 | if err != nil { 123 | panic(err) 124 | } 125 | 126 | // Decode the incoming data which is expected to be 2 channels and 127 | // delivered as int16 in []byte, hence we need to convert it. 128 | i := 0 129 | for i < len(input.frames) { 130 | j := 0 131 | for j < len(buffer) && i < len(input.frames) { 132 | buffer[j] = int16(input.frames[i]) | int16(input.frames[i+1])<<8 133 | j += 1 134 | i += 2 135 | } 136 | 137 | output = buffer[:j] 138 | stream.Write() 139 | } 140 | } 141 | } 142 | 143 | // portAudioStream manages the output stream through PortAudio when requirement 144 | // for number of channels or sample rate changes. 145 | type portAudioStream struct { 146 | device *portaudio.DeviceInfo 147 | stream *portaudio.Stream 148 | 149 | channels int 150 | sampleRate int 151 | } 152 | 153 | // newPortAudioStream creates a new portAudioStream using the default output 154 | // device found on the system. It will also take care of automatically 155 | // initialise the PortAudio API. 156 | func newPortAudioStream() (*portAudioStream, error) { 157 | if err := portaudio.Initialize(); err != nil { 158 | return nil, err 159 | } 160 | out, err := portaudio.DefaultHostApi() 161 | if err != nil { 162 | portaudio.Terminate() 163 | return nil, err 164 | } 165 | return &portAudioStream{device: out.DefaultOutputDevice}, nil 166 | } 167 | 168 | // Close closes any open audio stream and terminates the PortAudio API. 169 | func (s *portAudioStream) Close() error { 170 | if err := s.reset(); err != nil { 171 | portaudio.Terminate() 172 | return err 173 | } 174 | return portaudio.Terminate() 175 | } 176 | 177 | func (s *portAudioStream) reset() error { 178 | if s.stream != nil { 179 | if err := s.stream.Stop(); err != nil { 180 | return err 181 | } 182 | if err := s.stream.Close(); err != nil { 183 | return err 184 | } 185 | } 186 | return nil 187 | } 188 | 189 | // Stream prepares the stream to go through the specified buffer, channels and 190 | // sample rate, re-using any previously defined stream or setting up a new one. 191 | func (s *portAudioStream) Stream(buffer *[]int16, channels int, sampleRate int) error { 192 | if s.stream == nil || s.channels != channels || s.sampleRate != sampleRate { 193 | if err := s.reset(); err != nil { 194 | return err 195 | } 196 | 197 | params := portaudio.HighLatencyParameters(nil, s.device) 198 | params.Output.Channels = channels 199 | params.SampleRate = float64(sampleRate) 200 | params.FramesPerBuffer = len(*buffer) 201 | 202 | stream, err := portaudio.OpenStream(params, buffer) 203 | if err != nil { 204 | return err 205 | } 206 | if err := stream.Start(); err != nil { 207 | stream.Close() 208 | return err 209 | } 210 | 211 | s.stream = stream 212 | s.channels = channels 213 | s.sampleRate = sampleRate 214 | } 215 | return nil 216 | } 217 | 218 | // Write pushes the data in the buffer through to PortAudio. 219 | func (s *portAudioStream) Write() error { 220 | return s.stream.Write() 221 | } 222 | 223 | type FdDiscard struct { 224 | oldFd int 225 | newFd int 226 | } 227 | 228 | func DiscardFd(fd int) FdDiscard { 229 | newFd, err := syscall.Dup(fd) 230 | if err == nil { 231 | if err = syscall.Close(fd); err != nil { 232 | newFd = 0 233 | } 234 | } 235 | return FdDiscard{fd, newFd} 236 | } 237 | 238 | func (fd FdDiscard) Restore() error { 239 | var err error 240 | if fd.newFd > 0 { 241 | err = syscall.Dup2(fd.newFd, fd.oldFd) 242 | } 243 | return err 244 | } 245 | 246 | func main() { 247 | flag.Parse() 248 | prog := path.Base(os.Args[0]) 249 | 250 | uri := "spotify:track:5C4iS9W81NM5Rp0TW0TZ4o" 251 | if flag.NArg() == 1 { 252 | uri = flag.Arg(0) 253 | } 254 | 255 | signals := make(chan os.Signal, 1) 256 | signal.Notify(signals, os.Interrupt, os.Kill) 257 | 258 | appKey, err := ioutil.ReadFile(*appKeyPath) 259 | if err != nil { 260 | log.Fatal(err) 261 | } 262 | 263 | var silenceStderr = DiscardFd(syscall.Stderr) 264 | if *debug == true { 265 | silenceStderr.Restore() 266 | } 267 | 268 | audio, err := newAudioWriter() 269 | if err != nil { 270 | log.Fatal(err) 271 | } 272 | defer audio.Close() 273 | silenceStderr.Restore() 274 | 275 | session, err := spotify.NewSession(&spotify.Config{ 276 | ApplicationKey: appKey, 277 | ApplicationName: prog, 278 | CacheLocation: "tmp", 279 | SettingsLocation: "tmp", 280 | AudioConsumer: audio, 281 | 282 | // Disable playlists to make playback faster 283 | DisablePlaylistMetadataCache: true, 284 | InitiallyUnloadPlaylists: true, 285 | }) 286 | if err != nil { 287 | log.Fatal(err) 288 | } 289 | defer session.Close() 290 | 291 | credentials := spotify.Credentials{ 292 | Username: *username, 293 | Password: *password, 294 | } 295 | if err = session.Login(credentials, false); err != nil { 296 | log.Fatal(err) 297 | } 298 | 299 | // Log messages 300 | if *debug { 301 | go func() { 302 | for msg := range session.LogMessages() { 303 | log.Print(msg) 304 | } 305 | }() 306 | } 307 | 308 | // Wait for login and expect it to go fine 309 | select { 310 | case err = <-session.LoggedInUpdates(): 311 | if err != nil { 312 | log.Fatal(err) 313 | } 314 | case <-signals: 315 | return 316 | } 317 | 318 | // Parse the track 319 | link, err := session.ParseLink(uri) 320 | if err != nil { 321 | log.Fatal(err) 322 | } 323 | track, err := link.Track() 324 | if err != nil { 325 | log.Fatal(err) 326 | } 327 | 328 | // Load the track and play it 329 | track.Wait() 330 | player := session.Player() 331 | if err := player.Load(track); err != nil { 332 | fmt.Println("%#v", err) 333 | log.Fatal(err) 334 | } 335 | defer player.Unload() 336 | 337 | player.Play() 338 | 339 | // Output some progress information 340 | spinner := spin.New() 341 | pattern := spin.Box2 342 | spinner.Set(pattern) 343 | 344 | c1 := time.Tick(time.Millisecond) 345 | c2 := time.Tick(time.Second / time.Duration(len([]rune(pattern)))) 346 | c3 := time.Tick(300 * time.Millisecond) 347 | 348 | formatDuration := func(d time.Duration) string { 349 | cen := d / time.Millisecond / 10 % 100 350 | sec := d / time.Second % 60 351 | min := d / time.Minute % 60 352 | return fmt.Sprintf("%02d:%02d.%02d", min, sec, cen) 353 | } 354 | 355 | track.Wait() 356 | var artists []string 357 | for i := 0; i < track.Artists(); i++ { 358 | artists = append(artists, track.Artist(i).Name()) 359 | } 360 | info := fmt.Sprintf(" %s - %s - %s -", 361 | strings.Join(artists, ", "), 362 | track.Album().Name(), 363 | track.Name(), 364 | ) 365 | defer func() { fmt.Printf("\r") }() 366 | 367 | now := time.Now() 368 | start := now 369 | indicator := spinner.Next() 370 | for { 371 | select { 372 | case now = <-c1: 373 | case <-c2: 374 | indicator = spinner.Next() 375 | continue 376 | case <-c3: 377 | info = info[len(info)-1:] + info[:len(info)-1] 378 | continue 379 | case <-signals: 380 | return 381 | } 382 | elapsed := now.Sub(start) 383 | fmt.Printf("\r %s %s / %s ♫ %s", 384 | indicator, 385 | formatDuration(elapsed), 386 | formatDuration(track.Duration()), 387 | info, 388 | ) 389 | if elapsed >= track.Duration() { 390 | break 391 | } 392 | } 393 | <-session.EndOfTrackUpdates() 394 | } 395 | -------------------------------------------------------------------------------- /spotify/error.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Örjan Persson 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package spotify 16 | 17 | /* 18 | #cgo pkg-config: libspotify 19 | #include 20 | */ 21 | import "C" 22 | 23 | type Error C.sp_error 24 | 25 | func (e Error) Error() string { 26 | return C.GoString(C.sp_error_message(C.sp_error(e))) 27 | } 28 | 29 | const ( 30 | // The library version targeted does not match the one you claim you 31 | // support 32 | ErrBadAPIVersion Error = Error(C.SP_ERROR_BAD_API_VERSION) 33 | 34 | // Initialization of library failed - are cache locations etc. valid? 35 | ErrAPIInitializationFailed = Error(C.SP_ERROR_API_INITIALIZATION_FAILED) 36 | 37 | // The track specified for playing cannot be played 38 | ErrTrackNotPlayable = Error(C.SP_ERROR_TRACK_NOT_PLAYABLE) 39 | 40 | // The application key is invalid 41 | ErrBadApplicationKey = Error(C.SP_ERROR_BAD_APPLICATION_KEY) 42 | 43 | // Login failed because of bad username and/or password 44 | ErrBadUsernameOrPassword = Error(C.SP_ERROR_BAD_USERNAME_OR_PASSWORD) 45 | 46 | // The specified username is banned 47 | ErrUserBanned = Error(C.SP_ERROR_USER_BANNED) 48 | 49 | // Cannot connect to the Spotify backend system 50 | ErrUnableToContactServer = Error(C.SP_ERROR_UNABLE_TO_CONTACT_SERVER) 51 | 52 | // Client is too old, library will need to be updated 53 | ErrClientTooOld = Error(C.SP_ERROR_CLIENT_TOO_OLD) 54 | 55 | // Some other error occurred, and it is permanent (e.g. trying to relogin 56 | // will not help) 57 | ErrOtherPermanent = Error(C.SP_ERROR_OTHER_PERMANENT) 58 | 59 | // The user agent string is invalid or too long 60 | ErrBadUserAgent = Error(C.SP_ERROR_BAD_USER_AGENT) 61 | 62 | // No valid callback registered to handle events 63 | ErrMissingCallback = Error(C.SP_ERROR_MISSING_CALLBACK) 64 | 65 | // Input data was either missing or invalid 66 | ErrInvalidIndata = Error(C.SP_ERROR_INVALID_INDATA) 67 | 68 | // Index out of range 69 | ErrIndexOutOfRange = Error(C.SP_ERROR_INDEX_OUT_OF_RANGE) 70 | 71 | // The specified user needs a premium account 72 | ErrUserNeedsPremium = Error(C.SP_ERROR_USER_NEEDS_PREMIUM) 73 | 74 | // A transient error occurred. 75 | ErrOtherTransient = Error(C.SP_ERROR_OTHER_TRANSIENT) 76 | 77 | // The resource is currently loading 78 | ErrIsLoading = Error(C.SP_ERROR_IS_LOADING) 79 | 80 | // Could not find any suitable stream to play 81 | ErrNoStreamAvailable = Error(C.SP_ERROR_NO_STREAM_AVAILABLE) 82 | 83 | // Requested operation is not allowed 84 | ErrPermissionDenied = Error(C.SP_ERROR_PERMISSION_DENIED) 85 | 86 | // Target inbox is full 87 | ErrInboxIsFull = Error(C.SP_ERROR_INBOX_IS_FULL) 88 | 89 | // Cache is not enabled 90 | ErrNoCache = Error(C.SP_ERROR_NO_CACHE) 91 | 92 | // Requested user does not exist 93 | ErrNoSuchUser = Error(C.SP_ERROR_NO_SUCH_USER) 94 | 95 | // No credentials are stored 96 | ErrNoCredentials = Error(C.SP_ERROR_NO_CREDENTIALS) 97 | 98 | // Network disabled 99 | ErrNetworkDisabled = Error(C.SP_ERROR_NETWORK_DISABLED) 100 | 101 | // Invalid device ID 102 | ErrInvalidDeviceId = Error(C.SP_ERROR_INVALID_DEVICE_ID) 103 | 104 | // Unable to open trace file 105 | ErrCantOpenTraceFile = Error(C.SP_ERROR_CANT_OPEN_TRACE_FILE) 106 | 107 | // This application is no longer allowed to use the Spotify service 108 | ErrApplicationBanned = Error(C.SP_ERROR_APPLICATION_BANNED) 109 | 110 | // Reached the device limit for number of tracks to download 111 | ErrOfflineTooManyTracks = Error(C.SP_ERROR_OFFLINE_TOO_MANY_TRACKS) 112 | 113 | // Disk cache is full so no more tracks can be downloaded to offline mode 114 | ErrOfflineDiskCache = Error(C.SP_ERROR_OFFLINE_DISK_CACHE) 115 | 116 | // Offline key has expired, the user needs to go online again 117 | ErrOfflineExpired = Error(C.SP_ERROR_OFFLINE_EXPIRED) 118 | 119 | // This user is not allowed to use offline mode 120 | ErrOfflineNotAllowed = Error(C.SP_ERROR_OFFLINE_NOT_ALLOWED) 121 | 122 | // The license for this device has been lost. Most likely because the user 123 | // used offline on three other device 124 | ErrOfflineLicenseLost = Error(C.SP_ERROR_OFFLINE_LICENSE_LOST) 125 | 126 | // The Spotify license server does not respond correctly 127 | ErrOfflineLicenseError = Error(C.SP_ERROR_OFFLINE_LICENSE_ERROR) 128 | 129 | // A LastFM scrobble authentication error has occurred 130 | ErrLastFMAuthError = Error(C.SP_ERROR_LASTFM_AUTH_ERROR) 131 | 132 | // An invalid argument was specified 133 | ErrInvalidArgument = Error(C.SP_ERROR_INVALID_ARGUMENT) 134 | 135 | // An operating system error 136 | ErrSystemFailure = Error(C.SP_ERROR_SYSTEM_FAILURE) 137 | ) 138 | 139 | // spError converts an error from libspotify into a Go error. 140 | func spError(err C.sp_error) error { 141 | if err != C.SP_ERROR_OK { 142 | return Error(err) 143 | } 144 | return nil 145 | } 146 | -------------------------------------------------------------------------------- /spotify/libspotify.c: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Örjan Persson 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #include "_cgo_export.h" 16 | #include "libspotify.h" 17 | 18 | void set_callbacks(sp_session_callbacks *callbacks) 19 | { 20 | callbacks->logged_in = cb_logged_in; 21 | callbacks->logged_out = cb_logged_out; 22 | callbacks->metadata_updated = cb_metadata_updated; 23 | callbacks->connection_error = cb_connection_error; 24 | callbacks->message_to_user = cb_message_to_user; 25 | callbacks->notify_main_thread = cb_notify_main_thread; 26 | callbacks->music_delivery = cb_music_delivery; 27 | callbacks->play_token_lost = cb_play_token_lost; 28 | callbacks->log_message = cb_log_message; 29 | callbacks->end_of_track = cb_end_of_track; 30 | callbacks->streaming_error = cb_streaming_error; 31 | callbacks->userinfo_updated = cb_userinfo_updated; 32 | /* callbacks->start_playback = cb_start_playback; */ 33 | /* callbacks->stop_playback = cb_stop_playback; */ 34 | /* callbacks->get_audio_buffer_stats = cb_get_audio_buffer_stats; */ 35 | callbacks->offline_status_updated = cb_offline_status_updated; 36 | callbacks->offline_error = cb_offline_error; 37 | callbacks->credentials_blob_updated = cb_credentials_blob_updated; 38 | callbacks->connectionstate_updated = cb_connectionstate_updated; 39 | callbacks->scrobble_error = cb_scrobble_error; 40 | callbacks->private_session_mode_changed = cb_private_session_mode_changed; 41 | } 42 | 43 | void SP_CALLCONV cb_logged_in(sp_session *session, sp_error error) 44 | { 45 | go_logged_in(session, error); 46 | } 47 | 48 | void SP_CALLCONV cb_logged_out(sp_session *session) 49 | { 50 | go_logged_out(session); 51 | } 52 | 53 | void SP_CALLCONV cb_metadata_updated(sp_session *session) 54 | { 55 | go_metadata_updated(session); 56 | } 57 | 58 | void SP_CALLCONV cb_connection_error(sp_session *session, sp_error error) 59 | { 60 | go_connection_error(session, error); 61 | } 62 | 63 | void SP_CALLCONV cb_message_to_user(sp_session *session, const char *message) 64 | { 65 | go_message_to_user(session, (char *) message); 66 | } 67 | 68 | void SP_CALLCONV cb_notify_main_thread(sp_session *session) 69 | { 70 | go_notify_main_thread(session); 71 | } 72 | 73 | int SP_CALLCONV cb_music_delivery(sp_session *session, const sp_audioformat *format, const void *frames, int num_frames) 74 | { 75 | return go_music_delivery(session, (sp_audioformat *) format, (void *) frames, num_frames); 76 | } 77 | 78 | void SP_CALLCONV cb_play_token_lost(sp_session *session) 79 | { 80 | go_play_token_lost(session); 81 | } 82 | 83 | void SP_CALLCONV cb_log_message(sp_session *session, const char *data) 84 | { 85 | go_log_message(session, (char *) data); 86 | } 87 | 88 | void SP_CALLCONV cb_end_of_track(sp_session *session) 89 | { 90 | go_end_of_track(session); 91 | } 92 | 93 | void SP_CALLCONV cb_streaming_error(sp_session *session, sp_error error) 94 | { 95 | go_streaming_error(session, error); 96 | } 97 | 98 | void SP_CALLCONV cb_userinfo_updated(sp_session *session) 99 | { 100 | go_userinfo_updated(session); 101 | } 102 | 103 | void SP_CALLCONV cb_start_playback(sp_session *session) 104 | { 105 | go_start_playback(session); 106 | } 107 | 108 | void SP_CALLCONV cb_stop_playback(sp_session *session) 109 | { 110 | go_stop_playback(session); 111 | } 112 | 113 | void SP_CALLCONV cb_get_audio_buffer_stats(sp_session *session, sp_audio_buffer_stats *stats) 114 | { 115 | go_get_audio_buffer_stats(session, stats); 116 | } 117 | 118 | void SP_CALLCONV cb_offline_status_updated(sp_session *session) 119 | { 120 | go_offline_status_updated(session); 121 | } 122 | 123 | void SP_CALLCONV cb_offline_error(sp_session *session, sp_error error) 124 | { 125 | go_offline_error(session, error); 126 | } 127 | 128 | void SP_CALLCONV cb_credentials_blob_updated(sp_session *session, const char *blob) 129 | { 130 | go_credentials_blob_updated(session, (char *) blob); 131 | } 132 | 133 | void SP_CALLCONV cb_connectionstate_updated(sp_session *session) 134 | { 135 | go_connectionstate_updated(session); 136 | } 137 | 138 | void SP_CALLCONV cb_scrobble_error(sp_session *session, sp_error error) 139 | { 140 | go_scrobble_error(session, error); 141 | } 142 | 143 | void SP_CALLCONV cb_private_session_mode_changed(sp_session *session, bool is_private) 144 | { 145 | go_private_session_mode_changed(session, is_private); 146 | } 147 | 148 | sp_search* search_create(sp_session *session, const char *query, int track_offset, int track_count, int album_offset, int album_count, int artist_offset, int artist_count, int playlist_offset, int playlist_count, sp_search_type search_type, void *userdata) 149 | { 150 | return sp_search_create( 151 | session, query, 152 | track_offset, track_count, 153 | album_offset, album_count, 154 | artist_offset, artist_count, 155 | playlist_offset, playlist_count, 156 | search_type, cb_search_complete, 157 | userdata 158 | ); 159 | } 160 | 161 | void SP_CALLCONV cb_search_complete(sp_search *search, void *userdata) 162 | { 163 | go_search_complete(search, userdata); 164 | } 165 | 166 | sp_toplistbrowse* toplistbrowse_create(sp_session *session, sp_toplisttype type, sp_toplistregion region, const char *username, void *userdata) 167 | { 168 | return sp_toplistbrowse_create( 169 | session, type, region, username, 170 | cb_toplistbrowse_complete, userdata 171 | ); 172 | } 173 | 174 | void SP_CALLCONV cb_toplistbrowse_complete(sp_toplistbrowse *toplist, void *userdata) 175 | { 176 | go_toplistbrowse_complete(toplist, userdata); 177 | } 178 | 179 | void set_playlistcontainer_callbacks(sp_playlistcontainer_callbacks *callbacks) 180 | { 181 | callbacks->playlist_added = cb_playlistcontainer_playlist_added; 182 | callbacks->container_loaded = cb_playlistcontainer_loaded; 183 | } 184 | 185 | void SP_CALLCONV cb_playlistcontainer_playlist_added(sp_playlistcontainer *pc, sp_playlist *playlist, int position, void *userdata) 186 | { 187 | go_playlistcontainer_playlist_added(pc, playlist, position, userdata); 188 | } 189 | 190 | void SP_CALLCONV cb_playlistcontainer_loaded(sp_playlistcontainer *pc, void *userdata) 191 | { 192 | go_playlistcontainer_loaded(pc, userdata); 193 | } 194 | 195 | void set_playlist_callbacks(sp_playlist_callbacks *callbacks) 196 | { 197 | callbacks->playlist_state_changed = cb_playlist_state_changed; 198 | } 199 | 200 | void SP_CALLCONV cb_playlist_state_changed(sp_playlist *playlist, void *userdata) 201 | { 202 | go_playlist_state_changed(playlist, userdata); 203 | } 204 | 205 | void set_image_callback(sp_image *image, void *userdata) 206 | { 207 | sp_image_add_load_callback(image, &cb_image_complete, userdata); 208 | } 209 | 210 | void SP_CALLCONV cb_image_complete(sp_image *image, void *userdata) 211 | { 212 | go_image_complete(image, userdata); 213 | } -------------------------------------------------------------------------------- /spotify/libspotify.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Örjan Persson 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package spotify adds language bindings for spotify in Go. The libspotify 16 | // C API package allows third-party developers to write applications which 17 | // utilize the Spotify music streaming service. 18 | package spotify 19 | 20 | /* 21 | #cgo pkg-config: libspotify 22 | #include 23 | #include "libspotify.h" 24 | */ 25 | import "C" 26 | 27 | import ( 28 | "bytes" 29 | "errors" 30 | "image" 31 | _ "image/jpeg" 32 | "net/url" 33 | "runtime" 34 | "strings" 35 | "sync" 36 | "syscall" 37 | "time" 38 | "unsafe" 39 | ) 40 | 41 | var ( 42 | ErrMissingApplicationKey = errors.New("spotify: application key is required") 43 | ) 44 | 45 | // Config represents the configuration setup when creating a new session. 46 | type Config struct { 47 | // ApplicationKey is required and can be acquired from developer.spotify.com. 48 | ApplicationKey []byte 49 | 50 | // ApplicationName is used to determine cache locations and user agent. 51 | ApplicationName string 52 | 53 | // UserAgent is used when communicating with Spotify. If left empty, it 54 | // will automatically be created based on ApplicationName. 55 | UserAgent string 56 | 57 | // CacheLocation defines were Spotify will write any cache 58 | // files. This includes tracks, browse results and coverarts. 59 | // Leave empty to disable. 60 | CacheLocation string 61 | 62 | // SettingsLocation defines where Spotify will write settings 63 | // and per-user cache items. This includes playlists etc. It 64 | // may be the same location as the CacheLocation. 65 | // 66 | // Note: this directory will not be automatically created. 67 | SettingsLocation string 68 | 69 | // CompressPlaylists, if enabled, will compress local copies 70 | // of playlists to reduce disk space usage. 71 | CompressPlaylists bool 72 | 73 | // DisablePlaylistMetadataCache disables metadata caches for 74 | // playlists. It reduces disk space usage at the expense of 75 | // needing to request metadata from Spotify backend when 76 | // loading lists. 77 | DisablePlaylistMetadataCache bool 78 | 79 | // InitiallyUnloadPlaylists will avoid loading playlists into 80 | // RAM on startup if enabled. 81 | InitiallyUnloadPlaylists bool 82 | 83 | // TODO device_id 84 | // TODO proxy 85 | // TODO ca_certs 86 | // TODO tracefile 87 | 88 | AudioConsumer AudioConsumer 89 | } 90 | 91 | // Connection state describes the state of the connection of a session. 92 | type ConnectionState C.sp_connectionstate 93 | 94 | const ( 95 | // User not yet logged in 96 | ConnectionStateLoggedOut ConnectionState = C.SP_CONNECTION_STATE_LOGGED_OUT 97 | 98 | // Logged in against an Spotify accesspoint 99 | ConnectionStateLoggedIn = C.SP_CONNECTION_STATE_LOGGED_IN 100 | 101 | // Was logged in, but has now been disconnected 102 | ConnectionStateDisconnected = C.SP_CONNECTION_STATE_DISCONNECTED 103 | 104 | // Connection state is undefined 105 | ConnectionStateUndefined = C.SP_CONNECTION_STATE_UNDEFINED 106 | 107 | // Logged in, but in offline mode 108 | ConnectionStateOffline = C.SP_CONNECTION_STATE_OFFLINE 109 | ) 110 | 111 | var ( 112 | // once is used to initiate the global state of the package. 113 | once sync.Once 114 | 115 | // callbacks is a static set of callbacks used for all sessions. 116 | callbacks C.sp_session_callbacks 117 | ) 118 | 119 | // event is an internal type passed around to wake the main session thread up. 120 | type event int 121 | 122 | // Credentials are used when logging a user in. 123 | type Credentials struct { 124 | // Username is the spotify username. 125 | Username string 126 | 127 | // Password for the spotify username. 128 | Password string 129 | 130 | // Blob is an opaque data chunk used when logging in instead of password. If 131 | // login is successful and the remember flag set to true, this should be the 132 | // data blob retrieved from CredentialsBlobUpdates. 133 | Blob []byte 134 | } 135 | 136 | // Session is the representation of a Spotify session. 137 | type Session struct { 138 | config C.sp_session_config 139 | sp_session *C.sp_session 140 | mu sync.Mutex 141 | 142 | loggedIn chan error 143 | loggedOut chan struct{} 144 | 145 | metadataUpdatesMu sync.Mutex 146 | metadataUpdates map[updatesListener]struct{} 147 | 148 | connectionErrors chan error 149 | messagesToUser chan string 150 | notifyMainThread chan struct{} 151 | playTokenLost chan struct{} 152 | 153 | // rawLogMessages is the first place where all log messages ends up, and 154 | // later once parsed they're moved to the logMessage channel which is 155 | // exposed. 156 | rawLogMessages chan string 157 | logMessages chan *LogMessage 158 | 159 | endOfTrack chan struct{} 160 | streamingErrors chan error 161 | 162 | userInfoUpdatesMu sync.Mutex 163 | userInfoUpdates map[updatesListener]struct{} 164 | 165 | offlineStatusUpdates chan struct{} 166 | offlineErrors chan error 167 | credentialsBlobs chan []byte 168 | connectionStates chan struct{} 169 | 170 | scrobbleErrors chan error 171 | privateSessionChanges chan bool 172 | 173 | audioConsumer AudioConsumer 174 | 175 | wg sync.WaitGroup 176 | dealloc sync.Once 177 | shutdown chan struct{} 178 | } 179 | 180 | // sessionCall maps the C Spotify session structure to the Go session and 181 | // executes the given function. 182 | func sessionCall(spSession unsafe.Pointer, callback func(*Session)) { 183 | s := (*C.sp_session)(spSession) 184 | session := (*Session)(C.sp_session_userdata(s)) 185 | callback(session) 186 | } 187 | 188 | // NewSession creates a new session based on the given configuration. 189 | func NewSession(config *Config) (*Session, error) { 190 | session := &Session{ 191 | shutdown: make(chan struct{}), 192 | 193 | // Event channels, same order as api.h 194 | loggedIn: make(chan error, 1), 195 | loggedOut: make(chan struct{}, 1), 196 | 197 | metadataUpdates: make(map[updatesListener]struct{}), 198 | 199 | connectionErrors: make(chan error, 1), 200 | messagesToUser: make(chan string, 1), 201 | notifyMainThread: make(chan struct{}, 1), 202 | playTokenLost: make(chan struct{}, 1), 203 | 204 | rawLogMessages: make(chan string, 128), 205 | logMessages: make(chan *LogMessage, 128), 206 | 207 | endOfTrack: make(chan struct{}, 1), 208 | streamingErrors: make(chan error, 1), 209 | 210 | userInfoUpdates: make(map[updatesListener]struct{}), 211 | 212 | offlineStatusUpdates: make(chan struct{}, 1), 213 | offlineErrors: make(chan error, 1), 214 | credentialsBlobs: make(chan []byte, 1), 215 | connectionStates: make(chan struct{}, 1), 216 | 217 | scrobbleErrors: make(chan error, 1), 218 | privateSessionChanges: make(chan bool, 1), 219 | 220 | audioConsumer: config.AudioConsumer, 221 | } 222 | 223 | if err := session.setupConfig(config); err != nil { 224 | return nil, err 225 | } 226 | 227 | // libspotify expects certain methods to be called from the same thread as was 228 | // used when the sp_session_create was called. Hence we do lock down one 229 | // thread to only process events and some of these special calls. 230 | // 231 | // AFAIK this is the only way we can decide which thread a given goroutine 232 | // executes on. 233 | errc := make(chan error, 1) 234 | go func() { 235 | // TODO make sure we have enough threads available 236 | runtime.LockOSThread() 237 | 238 | err := spError(C.sp_session_create(&session.config, &session.sp_session)) 239 | errc <- err 240 | if err != nil { 241 | return 242 | } 243 | session.processEvents() 244 | }() 245 | 246 | if err := <-errc; err != nil { 247 | return nil, err 248 | } 249 | 250 | go session.processBackground() 251 | 252 | return session, nil 253 | } 254 | 255 | // setupConfig sets the config up to be used when connecting the session. 256 | func (s *Session) setupConfig(config *Config) error { 257 | if config.ApplicationKey == nil { 258 | return ErrMissingApplicationKey 259 | } 260 | 261 | s.config.api_version = C.SPOTIFY_API_VERSION 262 | 263 | s.config.cache_location = C.CString(config.CacheLocation) 264 | if s.config.cache_location == nil { 265 | return syscall.ENOMEM 266 | } 267 | 268 | s.config.settings_location = C.CString(config.SettingsLocation) 269 | if s.config.settings_location == nil { 270 | return syscall.ENOMEM 271 | } 272 | 273 | appKey := C.CString(string(config.ApplicationKey)) 274 | s.config.application_key = unsafe.Pointer(appKey) 275 | if s.config.application_key == nil { 276 | return syscall.ENOMEM 277 | } 278 | s.config.application_key_size = C.size_t(len(config.ApplicationKey)) 279 | 280 | userAgent := config.UserAgent 281 | if len(userAgent) == 0 { 282 | userAgent = "go-libspotify" 283 | if len(config.ApplicationName) > 0 { 284 | userAgent += "/" + config.ApplicationName 285 | } 286 | } 287 | s.config.user_agent = C.CString(userAgent) 288 | if s.config.user_agent == nil { 289 | return syscall.ENOMEM 290 | } 291 | 292 | // Setup the callbacks structure used for all sessions. The difference 293 | // between each session object is the userdata object which points into the 294 | // Go Session object. 295 | once.Do(func() { C.set_callbacks(&callbacks) }) 296 | s.config.callbacks = &callbacks 297 | s.config.userdata = unsafe.Pointer(s) 298 | 299 | if config.CompressPlaylists { 300 | s.config.compress_playlists = 1 301 | } 302 | if config.DisablePlaylistMetadataCache { 303 | s.config.dont_save_metadata_for_playlists = 1 304 | } 305 | if config.InitiallyUnloadPlaylists { 306 | s.config.initially_unload_playlists = 1 307 | } 308 | return nil 309 | } 310 | 311 | func (s *Session) free() { 312 | if s.config.cache_location != nil { 313 | C.free(unsafe.Pointer(s.config.cache_location)) 314 | s.config.cache_location = nil 315 | } 316 | if s.config.settings_location != nil { 317 | C.free(unsafe.Pointer(s.config.settings_location)) 318 | s.config.settings_location = nil 319 | } 320 | if s.config.application_key != nil { 321 | C.free(unsafe.Pointer(s.config.application_key)) 322 | s.config.application_key = nil 323 | } 324 | if s.config.user_agent != nil { 325 | C.free(unsafe.Pointer(s.config.user_agent)) 326 | s.config.user_agent = nil 327 | } 328 | } 329 | 330 | // Close closes the session, making the session unusable for any future calls. 331 | // This call releases the session internally back to libspotify and shuts the 332 | // background processing thread down. 333 | func (s *Session) Close() error { 334 | var err error 335 | s.dealloc.Do(func() { 336 | // Send shutdown events to log and event processor 337 | s.shutdown <- struct{}{} 338 | s.shutdown <- struct{}{} 339 | s.wg.Wait() 340 | 341 | err = spError(C.sp_session_release(s.sp_session)) 342 | s.free() 343 | }) 344 | return nil 345 | } 346 | 347 | // Login logs the the specified username and password combo. This 348 | // initiates the login in the background. 349 | // 350 | // An application MUST NEVER store the user's password in clear 351 | // text. If automatic relogin is required, use Relogin. 352 | func (s *Session) Login(c Credentials, remember bool) error { 353 | cusername := C.CString(c.Username) 354 | defer C.free(unsafe.Pointer(cusername)) 355 | var crememberme C.bool = 0 356 | if remember { 357 | crememberme = 1 358 | } 359 | var cpassword, cblob *C.char 360 | if len(c.Password) > 0 { 361 | cpassword = C.CString(c.Password) 362 | defer C.free(unsafe.Pointer(cpassword)) 363 | } 364 | if len(c.Blob) > 0 { 365 | cblob = C.CString(string(c.Blob)) 366 | defer C.free(unsafe.Pointer(cblob)) 367 | } 368 | 369 | s.mu.Lock() 370 | defer s.mu.Unlock() 371 | rc := C.sp_session_login( 372 | s.sp_session, 373 | cusername, 374 | cpassword, 375 | crememberme, 376 | cblob, 377 | ) 378 | return spError(rc) 379 | } 380 | 381 | // Relogin logs the remembered user in if the last user which logged in, logged 382 | // in with the remember flag set to true. 383 | // 384 | // If no credentials are stored, this will return ErrNoCredentials. 385 | func (s *Session) Relogin() error { 386 | return spError(C.sp_session_relogin(s.sp_session)) 387 | } 388 | 389 | func (s *Session) RememberedUser() string { 390 | size := C.sp_session_remembered_user(s.sp_session, nil, 0) 391 | buf := (*C.char)(C.malloc(C.size_t(size) + 1)) 392 | if buf == nil { 393 | panic("spotify: failed to allocate memory") 394 | } 395 | defer C.free(unsafe.Pointer(buf)) 396 | C.sp_session_remembered_user(s.sp_session, buf, C.size_t(size)+1) 397 | return C.GoString(buf) 398 | } 399 | 400 | // LoginUsername returns the user's login username. 401 | func (s *Session) LoginUsername() string { 402 | return C.GoString(C.sp_session_user_name(s.sp_session)) 403 | } 404 | 405 | // ForgetMe removes any stored credentials. If no credentials are currently 406 | // stored, nothing will happen. 407 | func (s *Session) ForgetMe() error { 408 | return spError(C.sp_session_forget_me(s.sp_session)) 409 | } 410 | 411 | // CurrentUser returns a user object for the currently logged in user. 412 | func (s *Session) CurrentUser() (*User, error) { 413 | sp_user := C.sp_session_user(s.sp_session) 414 | if sp_user == nil { 415 | return nil, errors.New("spotify: no user logged in") 416 | } 417 | return newUser(s, sp_user), nil 418 | } 419 | 420 | func (s *Session) GetUser(username string) (*User, error) { 421 | uri := "spotify:user:" + url.QueryEscape(username) 422 | link, err := s.ParseLink(uri) 423 | if err != nil { 424 | return nil, err 425 | } 426 | 427 | return link.User() 428 | } 429 | 430 | // Logout logs the currently logged in user out 431 | // 432 | // Always call this before terminating the application and 433 | // libspotify is currently logged in. Otherwise, the settings and 434 | // cache may be lost. 435 | func (s *Session) Logout() error { 436 | return spError(C.sp_session_logout(s.sp_session)) 437 | } 438 | 439 | // FlushCaches makes libspotify write all data that is meant to 440 | // be stored on disk to the disk immediately. libspotify does this 441 | // periodically by itself and also on logout. Under normal 442 | // conditions this shouldn't be needed. 443 | func (s *Session) FlushCaches() error { 444 | return spError(C.sp_session_flush_caches(s.sp_session)) 445 | } 446 | 447 | // SetAudioConsumer sets the audio consumer. 448 | func (s *Session) SetAudioConsumer(c AudioConsumer) { 449 | s.audioConsumer = c 450 | } 451 | 452 | // ConnectionState returns the current connection state for the 453 | // session. 454 | func (s *Session) ConnectionState() ConnectionState { 455 | state := C.sp_session_connectionstate(s.sp_session) 456 | return ConnectionState(state) 457 | } 458 | 459 | // SetCacheSize sets the maximum cache size in megabytes. 460 | // 461 | // Setting it to 0 (the default) will let libspotify automatically resize the 462 | // cache (10% of disk free space). 463 | func (s *Session) SetCacheSize(size int) { 464 | C.sp_session_set_cache_size(s.sp_session, C.size_t(size)) 465 | } 466 | 467 | func (s *Session) Player() *Player { 468 | return &Player{s} 469 | } 470 | 471 | type Bitrate C.sp_bitrate 472 | 473 | const ( 474 | Bitrate96k = Bitrate(C.SP_BITRATE_96k) 475 | Bitrate160k = Bitrate(C.SP_BITRATE_160k) 476 | Bitrate320k = Bitrate(C.SP_BITRATE_320k) 477 | ) 478 | 479 | type SampleType C.sp_sampletype 480 | 481 | const ( 482 | // 16-bit signed integer samples 483 | SampleTypeInt16NativeEndian = SampleType(C.SP_SAMPLETYPE_INT16_NATIVE_ENDIAN) 484 | ) 485 | 486 | type AudioFormat struct { 487 | // Sample type 488 | SampleType SampleType 489 | 490 | // Audio sample rate, in samples per second. 491 | SampleRate int 492 | 493 | // Number of channels. Currently 1 or 2. 494 | Channels int 495 | } 496 | 497 | func (af AudioFormat) Equal(u AudioFormat) bool { 498 | return af.SampleType == u.SampleType && 499 | af.SampleRate == u.SampleRate && 500 | af.Channels == u.Channels 501 | } 502 | 503 | func cbool(b bool) C.bool { 504 | if b { 505 | return 1 506 | } else { 507 | return 0 508 | } 509 | } 510 | 511 | func (s *Session) PreferredBitrate(bitrate Bitrate) error { 512 | return spError(C.sp_session_preferred_bitrate( 513 | s.sp_session, C.sp_bitrate(bitrate), 514 | )) 515 | } 516 | 517 | func (s *Session) PreferredOfflineBitrate(bitrate Bitrate, resync bool) error { 518 | return spError(C.sp_session_preferred_offline_bitrate( 519 | s.sp_session, C.sp_bitrate(bitrate), cbool(resync), 520 | )) 521 | } 522 | 523 | func (s *Session) VolumeNormalization() bool { 524 | return C.sp_session_get_volume_normalization(s.sp_session) == 1 525 | } 526 | 527 | func (s *Session) SetVolumeNormalization(normalize bool) { 528 | C.sp_session_set_volume_normalization(s.sp_session, cbool(normalize)) 529 | } 530 | 531 | func (s *Session) Playlists() (*PlaylistContainer, error) { 532 | return newPlaylistContainer(s) 533 | } 534 | 535 | func (s *Session) Starred() *Playlist { 536 | sp_playlist := C.sp_session_starred_create(s.sp_session) 537 | return newPlaylist(s, sp_playlist, true) 538 | } 539 | 540 | func (s *Session) PrivateSession() bool { 541 | return C.sp_session_is_private_session(s.sp_session) == 1 542 | } 543 | 544 | func (s *Session) SetPrivateSession(private bool) error { 545 | return spError(C.sp_session_set_private_session(s.sp_session, cbool(private))) 546 | } 547 | 548 | type SocialProvider C.sp_social_provider 549 | 550 | const ( 551 | SocialProviderSpotify = SocialProvider(C.SP_SOCIAL_PROVIDER_SPOTIFY) 552 | SocialProviderFacebook = SocialProvider(C.SP_SOCIAL_PROVIDER_FACEBOOK) 553 | SocialProviderLastFM = SocialProvider(C.SP_SOCIAL_PROVIDER_LASTFM) 554 | ) 555 | 556 | type ScrobblingState C.sp_scrobbling_state 557 | 558 | const ( 559 | ScrobblingStateUseGlobalSetting = ScrobblingState(C.SP_SCROBBLING_STATE_USE_GLOBAL_SETTING) 560 | ScrobblingStateLocalEnabled = ScrobblingState(C.SP_SCROBBLING_STATE_LOCAL_ENABLED) 561 | ScrobblingStateLocalDisabled = ScrobblingState(C.SP_SCROBBLING_STATE_LOCAL_DISABLED) 562 | ScrobblingStateGlobalEnabled = ScrobblingState(C.SP_SCROBBLING_STATE_GLOBAL_ENABLED) 563 | ScrobblingStateGlobalDisabled = ScrobblingState(C.SP_SCROBBLING_STATE_GLOBAL_DISABLED) 564 | ) 565 | 566 | func (s *Session) Scrobbling(provider SocialProvider) (ScrobblingState, error) { 567 | var state C.sp_scrobbling_state 568 | err := spError(C.sp_session_is_scrobbling( 569 | s.sp_session, C.sp_social_provider(provider), &state, 570 | )) 571 | return ScrobblingState(state), err 572 | } 573 | 574 | func (s *Session) SetScrobbling(provider SocialProvider, state ScrobblingState) error { 575 | return spError(C.sp_session_set_scrobbling( 576 | s.sp_session, C.sp_social_provider(provider), C.sp_scrobbling_state(state), 577 | )) 578 | } 579 | 580 | func (s *Session) IsScrobblingPossible(provider SocialProvider) bool { 581 | var possible C.bool 582 | C.sp_session_is_scrobbling_possible( 583 | s.sp_session, C.sp_social_provider(provider), &possible, 584 | ) 585 | return possible == 1 586 | } 587 | 588 | type ConnectionType C.sp_connection_type 589 | 590 | const ( 591 | // Connection type unknown (Default) 592 | ConnectionTypeUnknown = ConnectionType(C.SP_CONNECTION_TYPE_UNKNOWN) 593 | // No connection 594 | ConnectionTypeNone = ConnectionType(C.SP_CONNECTION_TYPE_NONE) 595 | // Mobile data (EDGE, 3G, etc) 596 | ConnectionTypeMobile = ConnectionType(C.SP_CONNECTION_TYPE_MOBILE) 597 | // Roamed mobile data (EDGE, 3G, etc) 598 | ConnectionTypeMobileRoaming = ConnectionType(C.SP_CONNECTION_TYPE_MOBILE_ROAMING) 599 | // Wireless connection 600 | ConnectionTypeWifi = ConnectionType(C.SP_CONNECTION_TYPE_WIFI) 601 | // Ethernet cable, etc 602 | ConnectionTypeWired = ConnectionType(C.SP_CONNECTION_TYPE_WIRED) 603 | ) 604 | 605 | func (s *Session) SetConnectionType(t ConnectionType) { 606 | C.sp_session_set_connection_type(s.sp_session, C.sp_connection_type(t)) 607 | } 608 | 609 | type ConnectionRules struct { 610 | Network bool 611 | NetworkIfRoaming bool 612 | SyncOverMobile bool 613 | SyncOverWifi bool 614 | } 615 | 616 | func (s *Session) SetConnectionRules(r ConnectionRules) { 617 | var rules C.sp_connection_rules 618 | if r.Network { 619 | rules |= C.SP_CONNECTION_RULE_NETWORK 620 | } 621 | if r.NetworkIfRoaming { 622 | rules |= C.SP_CONNECTION_RULE_NETWORK_IF_ROAMING 623 | } 624 | if r.SyncOverMobile { 625 | rules |= C.SP_CONNECTION_RULE_ALLOW_SYNC_OVER_MOBILE 626 | } 627 | if r.SyncOverWifi { 628 | rules |= C.SP_CONNECTION_RULE_ALLOW_SYNC_OVER_WIFI 629 | } 630 | C.sp_session_set_connection_rules(s.sp_session, rules) 631 | } 632 | 633 | func (s *Session) OfflineTracksToSync() int { 634 | return int(C.sp_offline_tracks_to_sync(s.sp_session)) 635 | } 636 | 637 | func (s *Session) OfflinePlaylists() int { 638 | return int(C.sp_offline_num_playlists(s.sp_session)) 639 | } 640 | 641 | type OfflineSyncStatus struct { 642 | sp_status C.sp_offline_sync_status 643 | } 644 | 645 | func (oss *OfflineSyncStatus) QueuedTracks() int { 646 | return int(oss.sp_status.queued_tracks) 647 | } 648 | 649 | func (oss *OfflineSyncStatus) QueuedBytes() int { 650 | return int(oss.sp_status.queued_bytes) 651 | } 652 | 653 | func (oss *OfflineSyncStatus) DoneTracks() int { 654 | return int(oss.sp_status.done_tracks) 655 | } 656 | 657 | func (oss *OfflineSyncStatus) DoneBytes() int { 658 | return int(oss.sp_status.done_bytes) 659 | } 660 | 661 | func (oss *OfflineSyncStatus) CopiedTracks() int { 662 | return int(oss.sp_status.copied_tracks) 663 | } 664 | 665 | func (oss *OfflineSyncStatus) CopiedBytes() int { 666 | return int(oss.sp_status.copied_bytes) 667 | } 668 | 669 | func (oss *OfflineSyncStatus) WillNotCopyTracks() int { 670 | return int(oss.sp_status.willnotcopy_tracks) 671 | } 672 | 673 | func (oss *OfflineSyncStatus) ErrorTracks() int { 674 | return int(oss.sp_status.error_tracks) 675 | } 676 | 677 | func (oss *OfflineSyncStatus) Synching() bool { 678 | return oss.sp_status.syncing == 1 679 | } 680 | 681 | func (s *Session) OfflineSyncStatus() (*OfflineSyncStatus, error) { 682 | status := &OfflineSyncStatus{} 683 | synching := C.sp_offline_sync_get_status(s.sp_session, &status.sp_status) 684 | if synching == 0 { 685 | return nil, errors.New("spotify: no sync in progress") 686 | } 687 | return status, nil 688 | } 689 | 690 | func (s *Session) OfflineTimeLeft() time.Duration { 691 | seconds := C.sp_offline_time_left(s.sp_session) 692 | return time.Duration(seconds) * time.Second 693 | } 694 | 695 | func (s *Session) Region() Region { 696 | return Region(C.sp_session_user_country(s.sp_session)) 697 | } 698 | 699 | func (s *Session) ArtistsToplist(region ToplistRegion) *ArtistsToplist { 700 | return newArtistsToplist(s, region, nil) 701 | } 702 | 703 | func (s *Session) AlbumsToplist(region ToplistRegion) *AlbumsToplist { 704 | return newAlbumsToplist(s, region, nil) 705 | } 706 | 707 | func (s *Session) TracksToplist(region ToplistRegion) *TracksToplist { 708 | return newTracksToplist(s, region, nil) 709 | } 710 | 711 | // LoggedInUpdates returns a channel used to get notified when the 712 | // login has been processed. 713 | func (s *Session) LoggedInUpdates() <-chan error { 714 | return s.loggedIn 715 | } 716 | 717 | // LoggedOutUpdates returns a channel used to get notified when the 718 | // session has been logged out. 719 | func (s *Session) LoggedOutUpdates() <-chan struct{} { 720 | return s.loggedOut 721 | } 722 | 723 | // ConnectionErrorUpdates returns a channel containing connection errors. 724 | func (s *Session) ConnectionErrorUpdates() <-chan error { 725 | return s.connectionErrors 726 | } 727 | 728 | // MessagesToUser returns a channel containing messages which the access point 729 | // wants to display to a user. 730 | // 731 | // In the desktop client, these are shown in a blueish toolbar just below the 732 | // search box. 733 | func (s *Session) MessagesToUser() <-chan string { 734 | return s.messagesToUser 735 | } 736 | 737 | // PlayTokenLostUpdates returns a channel used to get updates 738 | // when user loses the play token. 739 | func (s *Session) PlayTokenLostUpdates() <-chan struct{} { 740 | return s.playTokenLost 741 | } 742 | 743 | // LogMessages returns a channel used to get log messages. 744 | func (s *Session) LogMessages() <-chan *LogMessage { 745 | return s.logMessages 746 | } 747 | 748 | // EndOfTrackUpdates returns a channel used to get updates 749 | // when a track ends playing 750 | func (s *Session) EndOfTrackUpdates() <-chan struct{} { 751 | return s.endOfTrack 752 | } 753 | 754 | // StreamingErrors returns a channel with streaming errors. 755 | func (s *Session) StreamingErrors() <-chan error { 756 | return s.streamingErrors 757 | } 758 | 759 | // OfflineStatusUpdates returns a channel containing 760 | // offline synchronization status updates. 761 | func (s *Session) OfflineStatusUpdates() <-chan struct{} { 762 | return s.offlineStatusUpdates 763 | } 764 | 765 | // TODO document the difference between these functions 766 | 767 | // OfflineErrors returns a channel containing offline 768 | // synchronization status status updates. 769 | func (s *Session) OfflineErrors() <-chan error { 770 | return s.offlineErrors 771 | } 772 | 773 | // CredentialsBlobUpdates returns a channel used to get updates 774 | // for credential blobs. 775 | func (s *Session) CredentialsBlobUpdates() <-chan []byte { 776 | return s.credentialsBlobs 777 | } 778 | 779 | // ConnectionStateUpdates returns a channel used to get updates on 780 | // the connection state. 781 | func (s *Session) ConnectionStateUpdates() <-chan struct{} { 782 | return s.connectionStates 783 | } 784 | 785 | // ScrobbleErrors returns a channel with scrobble errors. 786 | // 787 | // Called when there is a scrobble error event. 788 | func (s *Session) ScrobbleErrors() <-chan error { 789 | return s.scrobbleErrors 790 | } 791 | 792 | // PrivateSessionModeChanges returns a channel where 793 | // private session changes are published. 794 | // 795 | // If the value is true, the user is in private mode. 796 | func (s *Session) PrivateSessionModeChanges() <-chan bool { 797 | return s.privateSessionChanges 798 | } 799 | 800 | type SearchType C.sp_search_type 801 | 802 | const ( 803 | SearchStandard SearchType = SearchType(C.SP_SEARCH_STANDARD) 804 | SearchSuggest = SearchType(C.SP_SEARCH_SUGGEST) 805 | ) 806 | 807 | type SearchSpec struct { 808 | // Search result offset 809 | Offset int 810 | 811 | // Search result limitation 812 | Count int 813 | } 814 | 815 | // SearchOptions contains offsets and limits for the search query. 816 | type SearchOptions struct { 817 | // Tracks is the number of tracks to search for 818 | Tracks SearchSpec 819 | 820 | // Albums is the number of albums to search for 821 | Albums SearchSpec 822 | 823 | // Artists is the number of artists to search for 824 | Artists SearchSpec 825 | 826 | // Playlist is the number of playlists to search for 827 | Playlists SearchSpec 828 | 829 | // Type is the search type. Defaults to normal searching. 830 | Type SearchType 831 | } 832 | 833 | // Search searches Spotify for track, album, artist and / or playlists. 834 | func (s *Session) Search(query string, opts *SearchOptions) (*Search, error) { 835 | return newSearch(s, query, opts) 836 | } 837 | 838 | // ParseLink parses a Spotify URI / URL string. 839 | func (s *Session) ParseLink(link string) (*Link, error) { 840 | clink := C.CString(link) 841 | defer C.free(unsafe.Pointer(clink)) 842 | sp_link := C.sp_link_create_from_string(clink) 843 | if sp_link == nil { 844 | return nil, errors.New("spotify: invalid spotify link") 845 | } 846 | return newLink(s, sp_link, false), nil 847 | } 848 | 849 | // SetStarred is used to star/unstar a set of tracks. 850 | // func (s *Session) SetStarred(tracks []*Track, star bool) { 851 | // sp_tracks := (**C.sp_track)(C.malloc(C.size_t(len(tracks)))) 852 | // defer C.free(unsafe.Pointer(sp_tracks)) 853 | // 854 | // for i, track := range tracks { 855 | // sp_tracks[i] = track.sp_track 856 | // } 857 | // 858 | // C.sp_track_set_starred( 859 | // s.sp_session, sp_tracks, C.int(len(tracks)), cbool(star), 860 | // ) 861 | // } 862 | 863 | func (s *Session) log(level LogLevel, message string) { 864 | m := &LogMessage{time.Now(), level, "go-libspotify", message} 865 | select { 866 | case s.logMessages <- m: 867 | default: 868 | } 869 | } 870 | 871 | func (s *Session) processEvents() { 872 | s.wg.Add(1) 873 | defer s.wg.Done() 874 | 875 | var nextTimeoutMs C.int 876 | for { 877 | s.mu.Lock() 878 | rc := C.sp_session_process_events(s.sp_session, &nextTimeoutMs) 879 | s.mu.Unlock() 880 | if err := spError(rc); err != nil { 881 | s.log(LogDebug, "process error err "+err.Error()) 882 | continue 883 | } 884 | 885 | timeout := time.Duration(nextTimeoutMs) * time.Millisecond 886 | select { 887 | case <-time.After(timeout): 888 | case <-s.notifyMainThread: 889 | case <-s.shutdown: 890 | return 891 | } 892 | } 893 | } 894 | 895 | func (s *Session) processBackground() { 896 | s.wg.Add(1) 897 | defer s.wg.Done() 898 | 899 | for { 900 | select { 901 | case message := <-s.rawLogMessages: 902 | m, err := parseLogMessage(message) 903 | if m != nil { 904 | select { 905 | case s.logMessages <- m: 906 | default: 907 | } 908 | } 909 | if err != nil { 910 | s.log(LogWarning, err.Error()+": "+message) 911 | } 912 | case <-s.shutdown: 913 | return 914 | } 915 | } 916 | } 917 | 918 | type updatesListener interface { 919 | cbUpdated() 920 | } 921 | 922 | func (s *Session) listenForMetadataUpdates(checkIfLoaded func() bool, l updatesListener) bool { 923 | return s.listenForUpdates(&s.metadataUpdatesMu, s.metadataUpdates, checkIfLoaded, l) 924 | } 925 | 926 | func (s *Session) stopListenForMetadataUpdates(l updatesListener) { 927 | s.stopListenForUpdates(&s.metadataUpdatesMu, s.metadataUpdates, l) 928 | } 929 | 930 | func (s *Session) listenForUserInfoUpdates(checkIfLoaded func() bool, l updatesListener) bool { 931 | return s.listenForUpdates(&s.userInfoUpdatesMu, s.userInfoUpdates, checkIfLoaded, l) 932 | } 933 | 934 | func (s *Session) stopListenForUserInfoUpdates(l updatesListener) { 935 | s.stopListenForUpdates(&s.userInfoUpdatesMu, s.userInfoUpdates, l) 936 | } 937 | 938 | func (s *Session) listenForUpdates(mu *sync.Mutex, m map[updatesListener]struct{}, checkIfLoaded func() bool, l updatesListener) bool { 939 | var added bool 940 | mu.Lock() 941 | defer mu.Unlock() 942 | if !checkIfLoaded() { 943 | m[l] = struct{}{} 944 | added = true 945 | } 946 | return added 947 | } 948 | 949 | func (s *Session) stopListenForUpdates(mu *sync.Mutex, m map[updatesListener]struct{}, l updatesListener) { 950 | mu.Lock() 951 | defer mu.Unlock() 952 | delete(m, l) 953 | } 954 | 955 | func (s *Session) sendUpdates(mu *sync.Mutex, m map[updatesListener]struct{}) { 956 | mu.Lock() 957 | defer mu.Unlock() 958 | for l := range m { 959 | l.cbUpdated() 960 | } 961 | } 962 | 963 | func (s *Session) cbLoggedIn(err error) { 964 | select { 965 | case s.loggedIn <- err: 966 | default: 967 | s.log(LogDebug, "failed to send logged in event") 968 | } 969 | } 970 | 971 | func (s *Session) cbLoggedOut() { 972 | select { 973 | case s.loggedOut <- struct{}{}: 974 | default: 975 | s.log(LogDebug, "failed to send logged out event") 976 | } 977 | } 978 | 979 | func (s *Session) cbMetadataUpdated() { 980 | s.sendUpdates(&s.metadataUpdatesMu, s.metadataUpdates) 981 | } 982 | 983 | func (s *Session) cbConnectionError(err error) { 984 | select { 985 | case s.connectionErrors <- err: 986 | default: 987 | } 988 | } 989 | 990 | func (s *Session) cbMessagesToUser(message string) { 991 | select { 992 | case s.messagesToUser <- message: 993 | default: 994 | } 995 | } 996 | 997 | func (s *Session) cbNotifyMainThread() { 998 | select { 999 | case s.notifyMainThread <- struct{}{}: 1000 | default: 1001 | s.log(LogDebug, "failed to notify main thread") 1002 | // TODO generate (internal) log message 1003 | } 1004 | } 1005 | 1006 | // cbMusicDelivery is called when there is decompressed audio data available. 1007 | // NOTE: This function must never block. 1008 | func (s *Session) cbMusicDelivery(format AudioFormat, frames []byte) int { 1009 | if s.audioConsumer == nil { 1010 | return 0 1011 | } 1012 | return s.audioConsumer.WriteAudio(format, frames) 1013 | } 1014 | 1015 | func (s *Session) cbPlayTokenLost() { 1016 | select { 1017 | case s.playTokenLost <- struct{}{}: 1018 | default: 1019 | } 1020 | } 1021 | 1022 | func (s *Session) cbLogMessage(message string) { 1023 | select { 1024 | case s.rawLogMessages <- message: 1025 | default: 1026 | } 1027 | } 1028 | 1029 | func (s *Session) cbEndOfTrack() { 1030 | select { 1031 | case s.endOfTrack <- struct{}{}: 1032 | default: 1033 | } 1034 | } 1035 | 1036 | func (s *Session) cbStreamingError(err error) { 1037 | select { 1038 | case s.streamingErrors <- err: 1039 | default: 1040 | } 1041 | } 1042 | 1043 | func (s *Session) cbUserInfoUpdated() { 1044 | s.sendUpdates(&s.userInfoUpdatesMu, s.userInfoUpdates) 1045 | } 1046 | 1047 | func (s *Session) cbStartPlayback() { 1048 | s.log(LogDebug, "start playback") 1049 | } 1050 | 1051 | func (s *Session) cbStopPlayback() { 1052 | s.log(LogDebug, "stop playback") 1053 | } 1054 | 1055 | func (s *Session) cbGetAudioBufferStats() { 1056 | s.log(LogDebug, "get audio buffer stats") 1057 | } 1058 | 1059 | func (s *Session) cbOfflineStatusUpdated() { 1060 | select { 1061 | case s.offlineStatusUpdates <- struct{}{}: 1062 | default: 1063 | } 1064 | } 1065 | 1066 | func (s *Session) cbOfflineError(err error) { 1067 | select { 1068 | case s.offlineErrors <- err: 1069 | default: 1070 | } 1071 | } 1072 | 1073 | func (s *Session) cbCredentialsBlobUpdated(blob []byte) { 1074 | select { 1075 | case s.credentialsBlobs <- blob: 1076 | default: 1077 | } 1078 | } 1079 | 1080 | func (s *Session) cbConnectionStateUpdated() { 1081 | select { 1082 | case s.connectionStates <- struct{}{}: 1083 | default: 1084 | } 1085 | } 1086 | 1087 | func (s *Session) cbScrobbleError(err error) { 1088 | select { 1089 | case s.scrobbleErrors <- err: 1090 | default: 1091 | } 1092 | } 1093 | 1094 | func (s *Session) cbPrivateSessionModeChanged(private bool) { 1095 | select { 1096 | case s.privateSessionChanges <- private: 1097 | default: 1098 | } 1099 | } 1100 | 1101 | //export go_logged_in 1102 | func go_logged_in(spSession unsafe.Pointer, spErr C.sp_error) { 1103 | sessionCall(spSession, func(s *Session) { 1104 | s.cbLoggedIn(spError(spErr)) 1105 | }) 1106 | } 1107 | 1108 | //export go_logged_out 1109 | func go_logged_out(spSession unsafe.Pointer) { 1110 | sessionCall(spSession, (*Session).cbLoggedOut) 1111 | } 1112 | 1113 | //export go_metadata_updated 1114 | func go_metadata_updated(spSession unsafe.Pointer) { 1115 | sessionCall(spSession, (*Session).cbMetadataUpdated) 1116 | } 1117 | 1118 | //export go_connection_error 1119 | func go_connection_error(spSession unsafe.Pointer, spErr C.sp_error) { 1120 | sessionCall(spSession, func(s *Session) { 1121 | s.cbConnectionError(spError(spErr)) 1122 | }) 1123 | } 1124 | 1125 | //export go_message_to_user 1126 | func go_message_to_user(spSession unsafe.Pointer, message *C.char) { 1127 | sessionCall(spSession, func(s *Session) { 1128 | s.cbMessagesToUser(C.GoString(message)) 1129 | }) 1130 | } 1131 | 1132 | //export go_notify_main_thread 1133 | func go_notify_main_thread(spSession unsafe.Pointer) { 1134 | sessionCall(spSession, (*Session).cbNotifyMainThread) 1135 | } 1136 | 1137 | //export go_music_delivery 1138 | func go_music_delivery(spSession unsafe.Pointer, format *C.sp_audioformat, data unsafe.Pointer, num_frames C.int) C.int { 1139 | s := (*C.sp_session)(spSession) 1140 | session := (*Session)(C.sp_session_userdata(s)) 1141 | audioFormat := AudioFormat{ 1142 | SampleType(format.sample_type), 1143 | int(format.sample_rate), 1144 | int(format.channels), 1145 | } 1146 | // TODO optimize allocation 1147 | var frames []byte 1148 | switch audioFormat.SampleType { 1149 | case SampleTypeInt16NativeEndian: 1150 | frames = C.GoBytes(data, 2*num_frames*format.channels) 1151 | default: 1152 | panic("Unsupported sample type") 1153 | } 1154 | return C.int(session.cbMusicDelivery(audioFormat, frames)) 1155 | } 1156 | 1157 | //export go_play_token_lost 1158 | func go_play_token_lost(spSession unsafe.Pointer) { 1159 | sessionCall(spSession, (*Session).cbPlayTokenLost) 1160 | } 1161 | 1162 | //export go_log_message 1163 | func go_log_message(spSession unsafe.Pointer, message *C.char) { 1164 | sessionCall(spSession, func(s *Session) { 1165 | s.cbLogMessage(C.GoString(message)) 1166 | }) 1167 | } 1168 | 1169 | //export go_end_of_track 1170 | func go_end_of_track(spSession unsafe.Pointer) { 1171 | sessionCall(spSession, (*Session).cbEndOfTrack) 1172 | } 1173 | 1174 | //export go_streaming_error 1175 | func go_streaming_error(spSession unsafe.Pointer, err C.sp_error) { 1176 | sessionCall(spSession, func(s *Session) { 1177 | s.cbStreamingError(spError(err)) 1178 | }) 1179 | } 1180 | 1181 | //export go_userinfo_updated 1182 | func go_userinfo_updated(spSession unsafe.Pointer) { 1183 | sessionCall(spSession, (*Session).cbUserInfoUpdated) 1184 | } 1185 | 1186 | //export go_start_playback 1187 | func go_start_playback(spSession unsafe.Pointer) { 1188 | sessionCall(spSession, (*Session).cbStartPlayback) 1189 | } 1190 | 1191 | //export go_stop_playback 1192 | func go_stop_playback(spSession unsafe.Pointer) { 1193 | sessionCall(spSession, (*Session).cbStopPlayback) 1194 | } 1195 | 1196 | //export go_get_audio_buffer_stats 1197 | func go_get_audio_buffer_stats(spSession unsafe.Pointer, stats *C.sp_audio_buffer_stats) { 1198 | sessionCall(spSession, func(s *Session) { 1199 | // TODO make some translation here, pass in stats 1200 | s.cbGetAudioBufferStats() 1201 | }) 1202 | } 1203 | 1204 | //export go_offline_status_updated 1205 | func go_offline_status_updated(spSession unsafe.Pointer) { 1206 | sessionCall(spSession, (*Session).cbOfflineStatusUpdated) 1207 | } 1208 | 1209 | //export go_offline_error 1210 | func go_offline_error(spSession unsafe.Pointer, err C.sp_error) { 1211 | sessionCall(spSession, func(s *Session) { 1212 | s.cbOfflineError(spError(err)) 1213 | }) 1214 | } 1215 | 1216 | //export go_credentials_blob_updated 1217 | func go_credentials_blob_updated(spSession unsafe.Pointer, data *C.char) { 1218 | sessionCall(spSession, func(s *Session) { 1219 | // We keep the blob as []byte instead of string because it just makes more 1220 | // sense than how libspotify does it. 1221 | blob := []byte(C.GoString(data)) 1222 | s.cbCredentialsBlobUpdated(blob) 1223 | }) 1224 | } 1225 | 1226 | //export go_connectionstate_updated 1227 | func go_connectionstate_updated(spSession unsafe.Pointer) { 1228 | sessionCall(spSession, (*Session).cbConnectionStateUpdated) 1229 | } 1230 | 1231 | //export go_scrobble_error 1232 | func go_scrobble_error(spSession unsafe.Pointer, err C.sp_error) { 1233 | sessionCall(spSession, func(s *Session) { 1234 | s.cbScrobbleError(spError(err)) 1235 | }) 1236 | } 1237 | 1238 | //export go_private_session_mode_changed 1239 | func go_private_session_mode_changed(spSession unsafe.Pointer, is_private C.bool) { 1240 | sessionCall(spSession, func(s *Session) { 1241 | s.cbPrivateSessionModeChanged(is_private == 1) 1242 | }) 1243 | } 1244 | 1245 | //export go_search_complete 1246 | func go_search_complete(spSearch unsafe.Pointer, userdata unsafe.Pointer) { 1247 | s := (*Search)(userdata) 1248 | s.cbComplete() 1249 | } 1250 | 1251 | //export go_toplistbrowse_complete 1252 | func go_toplistbrowse_complete(sp_toplistsearch unsafe.Pointer, userdata unsafe.Pointer) { 1253 | // TODO find a nicer way to do this 1254 | t := (*toplist)(userdata) 1255 | t.cbComplete() 1256 | } 1257 | 1258 | //export go_image_complete 1259 | func go_image_complete(spImage unsafe.Pointer, userdata unsafe.Pointer) { 1260 | i := (*Image)(userdata) 1261 | i.cbComplete() 1262 | } 1263 | 1264 | // AudioConsumer is the interface used to deliver music. The data delivered 1265 | // will be available as []byte and the format contains information about it. 1266 | type AudioConsumer interface { 1267 | WriteAudio(AudioFormat, []byte) int 1268 | } 1269 | 1270 | type Player struct { 1271 | s *Session 1272 | } 1273 | 1274 | func (p *Player) Load(t *Track) error { 1275 | return spError(C.sp_session_player_load(p.s.sp_session, t.sp_track)) 1276 | } 1277 | 1278 | func (p *Player) Seek(offset time.Duration) { 1279 | ms := C.int(offset / time.Millisecond) 1280 | C.sp_session_player_seek(p.s.sp_session, ms) 1281 | } 1282 | 1283 | func (p *Player) Play() { 1284 | C.sp_session_player_play(p.s.sp_session, 1) 1285 | } 1286 | 1287 | func (p *Player) Pause() { 1288 | C.sp_session_player_play(p.s.sp_session, 0) 1289 | } 1290 | 1291 | func (p *Player) Unload() { 1292 | C.sp_session_player_unload(p.s.sp_session) 1293 | } 1294 | 1295 | func (p *Player) Prefetch(t *Track) error { 1296 | return spError(C.sp_session_player_prefetch(p.s.sp_session, t.sp_track)) 1297 | } 1298 | 1299 | type PlaylistType C.sp_playlist_type 1300 | 1301 | const ( 1302 | // A normal playlist. 1303 | PlaylistTypePlaylist = PlaylistType(C.SP_PLAYLIST_TYPE_PLAYLIST) 1304 | 1305 | // Marks a folder's starting point 1306 | PlaylistTypeStartFolder = PlaylistType(C.SP_PLAYLIST_TYPE_START_FOLDER) 1307 | 1308 | // Marks previous folder's ending point 1309 | PlaylistTypeEndFolder = PlaylistType(C.SP_PLAYLIST_TYPE_END_FOLDER) 1310 | 1311 | // Placeholder 1312 | PlaylistTypePlaceholder = PlaylistType(C.SP_PLAYLIST_TYPE_PLACEHOLDER) 1313 | ) 1314 | 1315 | type PlaylistContainer struct { 1316 | session *Session 1317 | 1318 | sp_playlistcontainer *C.sp_playlistcontainer 1319 | callbacks C.sp_playlistcontainer_callbacks 1320 | 1321 | mu sync.Mutex 1322 | folders map[uint64]*PlaylistFolder 1323 | 1324 | wg sync.WaitGroup 1325 | loaded chan struct{} 1326 | } 1327 | 1328 | func newPlaylistContainer(s *Session) (*PlaylistContainer, error) { 1329 | pc := &PlaylistContainer{ 1330 | session: s, 1331 | loaded: make(chan struct{}, 1), 1332 | folders: make(map[uint64]*PlaylistFolder), 1333 | } 1334 | pc.mu.Lock() 1335 | defer pc.mu.Unlock() 1336 | 1337 | pc.sp_playlistcontainer = C.sp_session_playlistcontainer(s.sp_session) 1338 | if pc.sp_playlistcontainer == nil { 1339 | return nil, errors.New("spotify: failed to get playlist container") 1340 | } 1341 | 1342 | if pc.isLoaded() { 1343 | pc.loaded <- struct{}{} 1344 | } else { 1345 | pc.wg.Add(1) 1346 | } 1347 | 1348 | C.sp_playlistcontainer_add_ref(pc.sp_playlistcontainer) 1349 | runtime.SetFinalizer(pc, (*PlaylistContainer).release) 1350 | C.set_playlistcontainer_callbacks(&pc.callbacks) 1351 | C.sp_playlistcontainer_add_callbacks(pc.sp_playlistcontainer, &pc.callbacks, unsafe.Pointer(pc)) 1352 | 1353 | return pc, nil 1354 | } 1355 | 1356 | func (pc *PlaylistContainer) release() { 1357 | if pc.sp_playlistcontainer == nil { 1358 | panic("spotify: playlist container object has no sp_playlistcontainer object") 1359 | } 1360 | C.sp_playlistcontainer_remove_callbacks(pc.sp_playlistcontainer, &pc.callbacks, unsafe.Pointer(pc)) 1361 | C.sp_playlistcontainer_release(pc.sp_playlistcontainer) 1362 | pc.sp_playlistcontainer = nil 1363 | } 1364 | 1365 | func (pc *PlaylistContainer) Owner() (*User, error) { 1366 | sp_user := C.sp_playlistcontainer_owner(pc.sp_playlistcontainer) 1367 | if sp_user == nil { 1368 | return nil, errors.New("spotify: unknown user") 1369 | } 1370 | return newUser(pc.session, sp_user), nil 1371 | } 1372 | 1373 | // TODO rename to Entries? 1374 | func (pc *PlaylistContainer) Playlists() int { 1375 | return int(C.sp_playlistcontainer_num_playlists(pc.sp_playlistcontainer)) 1376 | } 1377 | 1378 | // TODO rename to EntryType? 1379 | func (pc *PlaylistContainer) PlaylistType(n int) PlaylistType { 1380 | if n < 0 || n >= pc.Playlists() { 1381 | panic("spotify: playlist out of range") 1382 | } 1383 | return PlaylistType(C.sp_playlistcontainer_playlist_type(pc.sp_playlistcontainer, C.int(n))) 1384 | } 1385 | 1386 | func (pc *PlaylistContainer) Folder(n int) (*PlaylistFolder, error) { 1387 | // if pc.PlaylistType(n) != PlaylistTypeStartFolder 1388 | folderId := uint64(C.sp_playlistcontainer_playlist_folder_id(pc.sp_playlistcontainer, C.int(n))) 1389 | if folderId == 0 { 1390 | return nil, errors.New("spotify: not a folder") 1391 | } 1392 | 1393 | pc.mu.Lock() 1394 | defer pc.mu.Unlock() 1395 | if f := pc.folders[folderId]; f != nil { 1396 | return f, nil 1397 | } 1398 | 1399 | f := newPlaylistFolder(pc, n, folderId) 1400 | pc.folders[folderId] = f 1401 | return f, nil 1402 | } 1403 | 1404 | func (pc *PlaylistContainer) Playlist(n int) *Playlist { 1405 | var sp_playlist *C.sp_playlist 1406 | switch pc.PlaylistType(n) { 1407 | case PlaylistTypePlaceholder: 1408 | fallthrough 1409 | case PlaylistTypePlaylist: 1410 | sp_playlist = C.sp_playlistcontainer_playlist(pc.sp_playlistcontainer, C.int(n)) 1411 | default: 1412 | panic("spotify: index does not hold a playlist entry") 1413 | } 1414 | return newPlaylist(pc.session, sp_playlist, false) 1415 | } 1416 | 1417 | func (pc *PlaylistContainer) isLoaded() bool { 1418 | return C.sp_playlistcontainer_is_loaded(pc.sp_playlistcontainer) == 1 1419 | } 1420 | 1421 | func (pc *PlaylistContainer) Wait() { 1422 | pc.wg.Wait() 1423 | } 1424 | 1425 | func (pc *PlaylistContainer) cbLoaded() { 1426 | pc.session.log(LogDebug, "playlist container loaded") 1427 | select { 1428 | case pc.loaded <- struct{}{}: 1429 | pc.wg.Done() 1430 | } 1431 | } 1432 | 1433 | //export go_playlistcontainer_playlist_added 1434 | func go_playlistcontainer_playlist_added(sp_playlistcontainer unsafe.Pointer, sp_playlist unsafe.Pointer, position C.int, userdata unsafe.Pointer) { 1435 | (*PlaylistContainer)(userdata).session.log(LogDebug, "playlist container playlist added") 1436 | } 1437 | 1438 | //export go_playlistcontainer_loaded 1439 | func go_playlistcontainer_loaded(sp_playlistcontainer unsafe.Pointer, userdata unsafe.Pointer) { 1440 | // playlistContainerCall(spSession, (*PlaylistContainer).cbLoaded) 1441 | (*PlaylistContainer)(userdata).session.log(LogDebug, "playlistcontainer loaded") 1442 | (*PlaylistContainer)(userdata).cbLoaded() 1443 | } 1444 | 1445 | type PlaylistFolder struct { 1446 | pc *PlaylistContainer 1447 | index int 1448 | id uint64 1449 | 1450 | mu sync.Mutex 1451 | name string 1452 | } 1453 | 1454 | func newPlaylistFolder(pc *PlaylistContainer, n int, id uint64) *PlaylistFolder { 1455 | return &PlaylistFolder{pc: pc, index: n, id: id} 1456 | } 1457 | 1458 | func (pf *PlaylistFolder) Id() uint64 { 1459 | return pf.id 1460 | } 1461 | 1462 | func (pf *PlaylistFolder) Name() string { 1463 | pf.mu.Lock() 1464 | defer pf.mu.Unlock() 1465 | if pf.name == "" { 1466 | const bufSize = 256 1467 | buf := (*C.char)(C.malloc(bufSize)) 1468 | if buf == nil { 1469 | panic("spotify: failed to allocate buffer") 1470 | } 1471 | defer C.free(unsafe.Pointer(buf)) 1472 | 1473 | rc := C.sp_playlistcontainer_playlist_folder_name( 1474 | pf.pc.sp_playlistcontainer, C.int(pf.index), 1475 | buf, 256, 1476 | ) 1477 | if rc != C.SP_ERROR_OK { 1478 | panic("spotify: folder is no longer in range") 1479 | } 1480 | pf.name = C.GoString(buf) 1481 | } 1482 | return pf.name 1483 | } 1484 | 1485 | type LinkType C.sp_linktype 1486 | 1487 | const ( 1488 | // Link type not valid - default until the library has parsed the link, or 1489 | // when parsing failed 1490 | LinkTypeInvalid = LinkType(C.SP_LINKTYPE_INVALID) 1491 | // Link type is track 1492 | LinkTypeTrack = LinkType(C.SP_LINKTYPE_TRACK) 1493 | // Link type is album 1494 | LinkTypeAlbum = LinkType(C.SP_LINKTYPE_ALBUM) 1495 | // Link type is artist 1496 | LinkTypeArtist = LinkType(C.SP_LINKTYPE_ARTIST) 1497 | // Link type is search 1498 | LinkTypeSearch = LinkType(C.SP_LINKTYPE_SEARCH) 1499 | // Link type is playlist 1500 | LinkTypePlaylist = LinkType(C.SP_LINKTYPE_PLAYLIST) 1501 | // Link type is user 1502 | LinkTypeUser = LinkType(C.SP_LINKTYPE_PROFILE) 1503 | // Link type is starred 1504 | LinkTypeStarred = LinkType(C.SP_LINKTYPE_STARRED) 1505 | // Link type is a local file 1506 | LinkTypeLocalTrack = LinkType(C.SP_LINKTYPE_LOCALTRACK) 1507 | // Link type is an image 1508 | LinkTypeImage = LinkType(C.SP_LINKTYPE_IMAGE) 1509 | ) 1510 | 1511 | type Link struct { 1512 | session *Session 1513 | sp_link *C.sp_link 1514 | } 1515 | 1516 | func newLink(s *Session, sp_link *C.sp_link, incRef bool) *Link { 1517 | if incRef { 1518 | C.sp_link_add_ref(sp_link) 1519 | } 1520 | link := &Link{s, sp_link} 1521 | runtime.SetFinalizer(link, (*Link).release) 1522 | return link 1523 | } 1524 | 1525 | func (l *Link) release() { 1526 | if l.sp_link == nil { 1527 | panic("spotify: link object has no sp_link object") 1528 | } 1529 | C.sp_link_release(l.sp_link) 1530 | l.sp_link = nil 1531 | } 1532 | 1533 | // String implements the Stringer interface and returns the Link URI. 1534 | func (l *Link) String() string { 1535 | // Determine how big string we need and get the string out. 1536 | size := C.sp_link_as_string(l.sp_link, nil, 0) 1537 | if C.size_t(size) == 0 { 1538 | return "" 1539 | } 1540 | buf := (*C.char)(C.malloc(C.size_t(size) + 1)) 1541 | if buf == nil { 1542 | return "" 1543 | } 1544 | defer C.free(unsafe.Pointer(buf)) 1545 | C.sp_link_as_string(l.sp_link, buf, size+1) 1546 | return C.GoString(buf) 1547 | } 1548 | 1549 | // LinkType returns the type of link. 1550 | func (l *Link) Type() LinkType { 1551 | return LinkType(C.sp_link_type(l.sp_link)) 1552 | } 1553 | 1554 | func (l *Link) Track() (*Track, error) { 1555 | if l.Type() != LinkTypeTrack { 1556 | return nil, errors.New("spotify: link is not a track") 1557 | } 1558 | return newTrack(l.session, C.sp_link_as_track(l.sp_link)), nil 1559 | } 1560 | 1561 | // TrackOffset returns the offset for the track link. 1562 | func (l *Link) TrackOffset() time.Duration { 1563 | var offsetMs C.int 1564 | C.sp_link_as_track_and_offset(l.sp_link, &offsetMs) 1565 | return time.Duration(offsetMs) / time.Millisecond 1566 | } 1567 | 1568 | func (l *Link) Album() (*Album, error) { 1569 | if l.Type() != LinkTypeAlbum { 1570 | return nil, errors.New("spotify: link is not an album") 1571 | } 1572 | return newAlbum(l.session, C.sp_link_as_album(l.sp_link)), nil 1573 | } 1574 | 1575 | func (l *Link) Artist() (*Artist, error) { 1576 | if l.Type() != LinkTypeArtist { 1577 | return nil, errors.New("spotify: link is not an artist") 1578 | } 1579 | return newArtist(l.session, C.sp_link_as_artist(l.sp_link)), nil 1580 | } 1581 | 1582 | func (l *Link) Playlist() (*Playlist, error) { 1583 | if l.Type() != LinkTypePlaylist { 1584 | return nil, errors.New("spotify: link is not a playlist") 1585 | } 1586 | 1587 | sp_playlist := C.sp_playlist_create(l.session.sp_session, l.sp_link) 1588 | return newPlaylist(l.session, sp_playlist, true), nil 1589 | } 1590 | 1591 | func (l *Link) User() (*User, error) { 1592 | if l.Type() != LinkTypeUser { 1593 | return nil, errors.New("spotify: link is not for a user") 1594 | } 1595 | return newUser(l.session, C.sp_link_as_user(l.sp_link)), nil 1596 | } 1597 | 1598 | func (l *Link) Image() (*Image, error) { 1599 | if l.Type() != LinkTypeImage { 1600 | return nil, errors.New("spotify: link is not for an image") 1601 | } 1602 | sp_image := C.sp_image_create_from_link(l.session.sp_session, l.sp_link) 1603 | if sp_image == nil { 1604 | return nil, errors.New("spotify: failed to create image from link") 1605 | } 1606 | return newImage(l.session, sp_image), nil 1607 | } 1608 | 1609 | type Search struct { 1610 | session *Session 1611 | sp_search *C.sp_search 1612 | wg sync.WaitGroup 1613 | } 1614 | 1615 | func newSearch(session *Session, query string, opts *SearchOptions) (*Search, error) { 1616 | s := &Search{session: session} 1617 | s.wg.Add(1) 1618 | 1619 | cquery := C.CString(query) 1620 | defer C.free(unsafe.Pointer(cquery)) 1621 | 1622 | s.sp_search = C.search_create( 1623 | s.session.sp_session, 1624 | cquery, 1625 | C.int(opts.Tracks.Offset), 1626 | C.int(opts.Tracks.Count), 1627 | C.int(opts.Albums.Offset), 1628 | C.int(opts.Albums.Count), 1629 | C.int(opts.Artists.Offset), 1630 | C.int(opts.Artists.Count), 1631 | C.int(opts.Playlists.Offset), 1632 | C.int(opts.Playlists.Count), 1633 | C.sp_search_type(opts.Type), 1634 | unsafe.Pointer(s), 1635 | ) 1636 | if s.sp_search == nil { 1637 | return nil, errors.New("spotify: failed to search") 1638 | } 1639 | runtime.SetFinalizer(s, (*Search).release) 1640 | return s, nil 1641 | } 1642 | 1643 | func (s *Search) release() { 1644 | if s.sp_search == nil { 1645 | panic("spotify: search object has no sp_search object") 1646 | } 1647 | C.sp_search_release(s.sp_search) 1648 | s.sp_search = nil 1649 | } 1650 | 1651 | func (s *Search) Wait() { 1652 | s.wg.Wait() 1653 | } 1654 | 1655 | func (s *Search) Link() *Link { 1656 | sp_link := C.sp_link_create_from_search(s.sp_search) 1657 | return newLink(s.session, sp_link, false) 1658 | } 1659 | 1660 | func (s *Search) cbComplete() { 1661 | s.wg.Done() 1662 | } 1663 | 1664 | func (s *Search) Error() error { 1665 | return spError(C.sp_search_error(s.sp_search)) 1666 | } 1667 | 1668 | func (s *Search) Query() string { 1669 | return C.GoString(C.sp_search_query(s.sp_search)) 1670 | } 1671 | 1672 | func (s *Search) DidYouMean() string { 1673 | return C.GoString(C.sp_search_did_you_mean(s.sp_search)) 1674 | } 1675 | 1676 | func (s *Search) Tracks() int { 1677 | return int(C.sp_search_num_tracks(s.sp_search)) 1678 | } 1679 | 1680 | func (s *Search) TotalTracks() int { 1681 | return int(C.sp_search_total_tracks(s.sp_search)) 1682 | } 1683 | 1684 | func (s *Search) Track(n int) *Track { 1685 | if n < 0 || n >= s.Tracks() { 1686 | panic("spotify: search track out of range") 1687 | } 1688 | sp_track := C.sp_search_track(s.sp_search, C.int(n)) 1689 | return newTrack(s.session, sp_track) 1690 | } 1691 | 1692 | func (s *Search) Albums() int { 1693 | return int(C.sp_search_num_albums(s.sp_search)) 1694 | } 1695 | 1696 | func (s *Search) TotalAlbums() int { 1697 | return int(C.sp_search_total_albums(s.sp_search)) 1698 | } 1699 | 1700 | func (s *Search) Album(n int) *Album { 1701 | if n < 0 || n >= s.Albums() { 1702 | panic("spotify: search album out of range") 1703 | } 1704 | sp_album := C.sp_search_album(s.sp_search, C.int(n)) 1705 | return newAlbum(s.session, sp_album) 1706 | } 1707 | 1708 | func (s *Search) Artists() int { 1709 | return int(C.sp_search_num_artists(s.sp_search)) 1710 | } 1711 | 1712 | func (s *Search) TotalArtists() int { 1713 | return int(C.sp_search_total_artists(s.sp_search)) 1714 | } 1715 | 1716 | func (s *Search) Artist(n int) *Artist { 1717 | if n < 0 || n >= s.Artists() { 1718 | panic("spotify: search artist out of range") 1719 | } 1720 | sp_artist := C.sp_search_artist(s.sp_search, C.int(n)) 1721 | return newArtist(s.session, sp_artist) 1722 | } 1723 | 1724 | func (s *Search) Playlists() int { 1725 | return int(C.sp_search_num_playlists(s.sp_search)) 1726 | } 1727 | 1728 | func (s *Search) TotalPlaylists() int { 1729 | return int(C.sp_search_total_playlists(s.sp_search)) 1730 | } 1731 | 1732 | func (s *Search) Playlist(n int) *Playlist { 1733 | if n < 0 || n >= s.Playlists() { 1734 | panic("spotify: search playlist out of range") 1735 | } 1736 | sp_playlist := C.sp_search_playlist(s.sp_search, C.int(n)) 1737 | return newPlaylist(s.session, sp_playlist, true) 1738 | } 1739 | 1740 | func (s *Search) PlaylistName(n int) string { 1741 | if n < 0 || n >= s.Playlists() { 1742 | panic("spotify: search playlist out of range") 1743 | } 1744 | return C.GoString(C.sp_search_playlist_name(s.sp_search, C.int(n))) 1745 | } 1746 | 1747 | func (s *Search) PlaylistUri(n int) string { 1748 | if n < 0 || n >= s.Playlists() { 1749 | panic("spotify: search playlist out of range") 1750 | } 1751 | return C.GoString(C.sp_search_playlist_uri(s.sp_search, C.int(n))) 1752 | } 1753 | 1754 | func (s *Search) PlaylistImageUri(n int) string { 1755 | if n < 0 || n >= s.Playlists() { 1756 | panic("spotify: search playlist out of range") 1757 | } 1758 | return C.GoString(C.sp_search_playlist_image_uri(s.sp_search, C.int(n))) 1759 | } 1760 | 1761 | type Track struct { 1762 | session *Session 1763 | sp_track *C.sp_track 1764 | wg sync.WaitGroup 1765 | 1766 | // sometimes when loading a track the callback metadata_updated is called twice 1767 | // as there is a WaitGroup to control the Wait method it can't call WaitGroup.Done twice. 1768 | loadOnce sync.Once 1769 | } 1770 | 1771 | func newTrack(s *Session, t *C.sp_track) *Track { 1772 | C.sp_track_add_ref(t) 1773 | track := &Track{session: s, sp_track: t} 1774 | runtime.SetFinalizer(track, (*Track).release) 1775 | 1776 | if s.listenForMetadataUpdates(track.isLoaded, track) { 1777 | track.wg.Add(1) 1778 | } 1779 | return track 1780 | } 1781 | 1782 | func (t *Track) release() { 1783 | if t.sp_track == nil { 1784 | panic("spotify: track object has no sp_track object") 1785 | } 1786 | C.sp_track_release(t.sp_track) 1787 | t.sp_track = nil 1788 | } 1789 | 1790 | func (t *Track) cbUpdated() { 1791 | if t.isLoaded() { 1792 | t.loadOnce.Do(func() { t.wg.Done() }) 1793 | } 1794 | } 1795 | 1796 | func (t *Track) Wait() { 1797 | t.wg.Wait() 1798 | if !t.isLoaded() { 1799 | panic("spotify: track is not loaded") 1800 | } 1801 | t.session.stopListenForMetadataUpdates(t) 1802 | } 1803 | 1804 | func (t *Track) isLoaded() bool { 1805 | return C.sp_track_is_loaded(t.sp_track) == 1 1806 | } 1807 | 1808 | // Error returns an error associated with a track. 1809 | func (t *Track) Error() error { 1810 | return spError(C.sp_track_error(t.sp_track)) 1811 | } 1812 | 1813 | func (t *Track) OfflineStatus() TrackOfflineStatus { 1814 | status := C.sp_track_offline_get_status(t.sp_track) 1815 | return TrackOfflineStatus(status) 1816 | } 1817 | 1818 | // Availability returns the track availability. 1819 | func (t *Track) Availability() TrackAvailability { 1820 | avail := C.sp_track_get_availability( 1821 | t.session.sp_session, 1822 | t.sp_track, 1823 | ) 1824 | return TrackAvailability(avail) 1825 | } 1826 | 1827 | // IsLocal returns true if the track is a local file. 1828 | func (t *Track) IsLocal() bool { 1829 | local := C.sp_track_is_local( 1830 | t.session.sp_session, 1831 | t.sp_track, 1832 | ) 1833 | return local == 1 1834 | } 1835 | 1836 | // IsAutoLinked returns true if the track is auto-linked to another track. 1837 | func (t *Track) IsAutoLinked() bool { 1838 | linked := C.sp_track_is_autolinked( 1839 | t.session.sp_session, 1840 | t.sp_track, 1841 | ) 1842 | return linked == 1 1843 | } 1844 | 1845 | // PlayableTrack returns the track which is the actual track that will be 1846 | // played if the given track is played. 1847 | func (t *Track) PlayableTrack() *Track { 1848 | sp_track := C.sp_track_get_playable( 1849 | t.session.sp_session, 1850 | t.sp_track, 1851 | ) 1852 | return newTrack(t.session, sp_track) 1853 | } 1854 | 1855 | // IsPlaceholder returns true if the track is a placeholder. Placeholder tracks 1856 | // are used to store other objects than tracks in the playlist. Currently this 1857 | // is used in the inbox to store artists, albums and playlists. 1858 | // 1859 | // Use Link() to get a link object that points to the real object this "track" 1860 | // points to. 1861 | func (t *Track) IsPlaceholder() bool { 1862 | placeholder := C.sp_track_is_placeholder( 1863 | t.sp_track, 1864 | ) 1865 | return placeholder == 1 1866 | } 1867 | 1868 | // Link returns a link object representing the track. 1869 | func (t *Track) Link() *Link { 1870 | return t.LinkOffset(0) 1871 | } 1872 | 1873 | // Link returns a link object representing the track at the given offset. 1874 | func (t *Track) LinkOffset(offset time.Duration) *Link { 1875 | offsetMs := C.int(offset / time.Millisecond) 1876 | sp_link := C.sp_link_create_from_track(t.sp_track, offsetMs) 1877 | return newLink(t.session, sp_link, false) 1878 | } 1879 | 1880 | // IsStarred returns true if the track is starred by the currently logged in 1881 | // user. 1882 | func (t *Track) IsStarred() bool { 1883 | starred := C.sp_track_is_starred( 1884 | t.session.sp_session, 1885 | t.sp_track, 1886 | ) 1887 | return starred == 1 1888 | } 1889 | 1890 | // TODO sp_track_set_starred 1891 | 1892 | // Artists returns the number of artists performing on the track. 1893 | func (t *Track) Artists() int { 1894 | return int(C.sp_track_num_artists(t.sp_track)) 1895 | } 1896 | 1897 | // Artist returns the artist on the specified index. Use Artists to know how 1898 | // many artists that performed on the track. 1899 | func (t *Track) Artist(n int) *Artist { 1900 | if n < 0 || n >= t.Artists() { 1901 | panic("spotify: track artist index out of range") 1902 | } 1903 | sp_artist := C.sp_track_artist(t.sp_track, C.int(n)) 1904 | return newArtist(t.session, sp_artist) 1905 | } 1906 | 1907 | // Album returns the album of the track. 1908 | func (t *Track) Album() *Album { 1909 | sp_album := C.sp_track_album(t.sp_track) 1910 | return newAlbum(t.session, sp_album) 1911 | } 1912 | 1913 | // Name returns the track name. 1914 | func (t *Track) Name() string { 1915 | return C.GoString(C.sp_track_name(t.sp_track)) 1916 | } 1917 | 1918 | // Duration returns the length of the current track. 1919 | func (t *Track) Duration() time.Duration { 1920 | ms := C.sp_track_duration(t.sp_track) 1921 | return time.Duration(ms) * time.Millisecond 1922 | } 1923 | 1924 | // Popularity is in the range [0, 100]. 1925 | type Popularity int 1926 | 1927 | // Popularity returns the popularity for the track. 1928 | func (t *Track) Popularity() Popularity { 1929 | p := C.sp_track_popularity(t.sp_track) 1930 | return Popularity(p) 1931 | } 1932 | 1933 | // Disc returns the disc number for the track. 1934 | func (t *Track) Disc() int { 1935 | return int(C.sp_track_disc(t.sp_track)) 1936 | } 1937 | 1938 | // Position returns the position of a track on its disc. 1939 | // It starts at 1 (relative the corresponding disc). 1940 | // 1941 | // This function returns valid data only for tracks 1942 | // appearing in a browse artist or browse album result 1943 | // (otherwise returns 0). 1944 | func (t *Track) Index() int { 1945 | return int(C.sp_track_index(t.sp_track)) 1946 | } 1947 | 1948 | // TODO sp_localtrack_create 1949 | 1950 | type TrackAvailability C.sp_track_availability 1951 | 1952 | const ( 1953 | // Track is not available 1954 | TrackAvailabilityUnavailable = TrackAvailability(C.SP_TRACK_AVAILABILITY_UNAVAILABLE) 1955 | 1956 | // Track is available and can be played 1957 | TrackAvailabilityAvailable = TrackAvailability(C.SP_TRACK_AVAILABILITY_AVAILABLE) 1958 | 1959 | // Track can not be streamed using this account 1960 | TrackAvailabilityNotStreamable = TrackAvailability(C.SP_TRACK_AVAILABILITY_NOT_STREAMABLE) 1961 | 1962 | // Track not available on artist's request 1963 | TrackAvailabilityBannedByArtist = TrackAvailability(C.SP_TRACK_AVAILABILITY_BANNED_BY_ARTIST) 1964 | ) 1965 | 1966 | type TrackOfflineStatus C.sp_track_offline_status 1967 | 1968 | const ( 1969 | // Not marked for offline 1970 | TrackOfflineNo = TrackOfflineStatus(C.SP_TRACK_OFFLINE_NO) 1971 | // Waiting for download 1972 | TrackOfflineWaiting = TrackOfflineStatus(C.SP_TRACK_OFFLINE_WAITING) 1973 | // Currently downloading 1974 | TrackOfflineDownloading = TrackOfflineStatus(C.SP_TRACK_OFFLINE_DOWNLOADING) 1975 | // Downloaded OK and can be played 1976 | TrackOfflineDone = TrackOfflineStatus(C.SP_TRACK_OFFLINE_DONE) 1977 | // TrackOfflineStatus during download 1978 | TrackOfflineTrackOfflineStatus = TrackOfflineStatus(C.SP_TRACK_OFFLINE_ERROR) 1979 | // Downloaded OK but not playable due to expiery 1980 | TrackOfflineDoneExpired = TrackOfflineStatus(C.SP_TRACK_OFFLINE_DONE_EXPIRED) 1981 | // Waiting because device have reached max number of allowed tracks 1982 | TrackOfflineLimitExceeded = TrackOfflineStatus(C.SP_TRACK_OFFLINE_LIMIT_EXCEEDED) 1983 | // Downloaded OK and available but scheduled for re-download 1984 | TrackOfflineDoneResync = TrackOfflineStatus(C.SP_TRACK_OFFLINE_DONE_RESYNC) 1985 | ) 1986 | 1987 | type Album struct { 1988 | session *Session 1989 | sp_album *C.sp_album 1990 | wg sync.WaitGroup 1991 | } 1992 | 1993 | type AlbumType C.sp_albumtype 1994 | 1995 | const ( 1996 | // Normal album 1997 | AlbumTypeAlbum = AlbumType(C.SP_ALBUMTYPE_ALBUM) 1998 | // Single 1999 | AlbumTypeSingle = AlbumType(C.SP_ALBUMTYPE_SINGLE) 2000 | // Compilation 2001 | AlbumTypeCompilation = AlbumType(C.SP_ALBUMTYPE_COMPILATION) 2002 | // Unknown type 2003 | AlbumTypeUnknown = AlbumType(C.SP_ALBUMTYPE_UNKNOWN) 2004 | ) 2005 | 2006 | func newAlbum(s *Session, sp_album *C.sp_album) *Album { 2007 | C.sp_album_add_ref(sp_album) 2008 | album := &Album{session: s, sp_album: sp_album} 2009 | runtime.SetFinalizer(album, (*Album).release) 2010 | 2011 | if s.listenForMetadataUpdates(album.isLoaded, album) { 2012 | album.wg.Add(1) 2013 | } 2014 | return album 2015 | } 2016 | 2017 | func (a *Album) release() { 2018 | if a.sp_album == nil { 2019 | panic("spotify: album object has no sp_album object") 2020 | } 2021 | C.sp_album_release(a.sp_album) 2022 | a.sp_album = nil 2023 | } 2024 | 2025 | func (a *Album) cbUpdated() { 2026 | if a.isLoaded() { 2027 | a.wg.Done() 2028 | } 2029 | } 2030 | 2031 | func (a *Album) Wait() { 2032 | a.wg.Wait() 2033 | if !a.isLoaded() { 2034 | panic("spotify: album is not loaded") 2035 | } 2036 | a.session.stopListenForMetadataUpdates(a) 2037 | } 2038 | 2039 | // Link creates a link object from the album. 2040 | func (a *Album) Link() *Link { 2041 | sp_link := C.sp_link_create_from_album(a.sp_album) 2042 | return newLink(a.session, sp_link, false) 2043 | } 2044 | 2045 | // IsAvailable returns true if the album is available in the current region and 2046 | // for playback. 2047 | func (a *Album) IsAvailable() bool { 2048 | return C.sp_album_is_available(a.sp_album) == 1 2049 | } 2050 | 2051 | func (a *Album) Artist() *Artist { 2052 | // TODO we never should wait for metadata updates? 2053 | return newArtist(a.session, C.sp_album_artist(a.sp_album)) 2054 | } 2055 | 2056 | func (a *Album) CoverLink(size ImageSize) *Link { 2057 | if sp_link := C.sp_link_create_from_album_cover(a.sp_album, 2058 | C.sp_image_size(size)); sp_link != nil { 2059 | return newLink(a.session, sp_link, false) 2060 | } 2061 | 2062 | return nil 2063 | } 2064 | 2065 | func (a *Album) Cover(size ImageSize) (*Image, error) { 2066 | id := C.sp_album_cover(a.sp_album, C.sp_image_size(size)) 2067 | if id == nil { 2068 | return nil, errors.New("spotify: failed to load album cover") 2069 | } 2070 | sp_image := C.sp_image_create(a.session.sp_session, id) 2071 | if sp_image == nil { 2072 | return nil, errors.New("spotify: failed to create image for album cover") 2073 | } 2074 | return newImage(a.session, sp_image), nil 2075 | } 2076 | 2077 | // Name returns the name of the album. 2078 | func (a *Album) Name() string { 2079 | return C.GoString(C.sp_album_name(a.sp_album)) 2080 | } 2081 | 2082 | // Year returns the release year. 2083 | func (a *Album) Year() int { 2084 | return int(C.sp_album_year(a.sp_album)) 2085 | } 2086 | 2087 | // Type returns the type of album. 2088 | func (a *Album) Type() AlbumType { 2089 | return AlbumType(C.sp_album_type(a.sp_album)) 2090 | } 2091 | 2092 | func (a *Album) isLoaded() bool { 2093 | return C.sp_album_is_loaded(a.sp_album) == 1 2094 | } 2095 | 2096 | type Artist struct { 2097 | session *Session 2098 | sp_artist *C.sp_artist 2099 | wg sync.WaitGroup 2100 | } 2101 | 2102 | func newArtist(s *Session, sp_artist *C.sp_artist) *Artist { 2103 | C.sp_artist_add_ref(sp_artist) 2104 | artist := &Artist{session: s, sp_artist: sp_artist} 2105 | runtime.SetFinalizer(artist, (*Artist).release) 2106 | 2107 | if s.listenForMetadataUpdates(artist.isLoaded, artist) { 2108 | artist.wg.Add(1) 2109 | } 2110 | return artist 2111 | } 2112 | 2113 | func (a *Artist) release() { 2114 | if a.sp_artist == nil { 2115 | panic("spotify: artist object has no sp_artist object") 2116 | } 2117 | C.sp_artist_release(a.sp_artist) 2118 | a.sp_artist = nil 2119 | } 2120 | 2121 | func (a *Artist) cbUpdated() { 2122 | if a.isLoaded() { 2123 | a.wg.Done() 2124 | } 2125 | } 2126 | 2127 | func (a *Artist) isLoaded() bool { 2128 | return C.sp_artist_is_loaded(a.sp_artist) == 1 2129 | } 2130 | 2131 | func (a *Artist) Wait() { 2132 | a.wg.Wait() 2133 | if !a.isLoaded() { 2134 | panic("spotify: artist is not loaded") 2135 | } 2136 | a.session.stopListenForMetadataUpdates(a) 2137 | } 2138 | 2139 | // Link creates a link object from the artist. 2140 | func (a *Artist) Link() *Link { 2141 | sp_link := C.sp_link_create_from_artist(a.sp_artist) 2142 | return newLink(a.session, sp_link, false) 2143 | } 2144 | 2145 | // Name returns the name of the artist. 2146 | func (a *Artist) Name() string { 2147 | return C.GoString(C.sp_artist_name(a.sp_artist)) 2148 | } 2149 | 2150 | func (a *Artist) PortraitLink(size ImageSize) *Link { 2151 | sp_link := C.sp_link_create_from_artist_portrait( 2152 | a.sp_artist, C.sp_image_size(size), 2153 | ) 2154 | if sp_link == nil { 2155 | panic("spotify: portrait link is null") 2156 | } 2157 | return newLink(a.session, sp_link, false) 2158 | } 2159 | 2160 | func (a *Artist) Portrait(size ImageSize) (*Image, error) { 2161 | id := C.sp_artist_portrait(a.sp_artist, C.sp_image_size(size)) 2162 | if id == nil { 2163 | return nil, errors.New("spotify: failed to load artist portrait") 2164 | } 2165 | sp_image := C.sp_image_create(a.session.sp_session, id) 2166 | if sp_image == nil { 2167 | return nil, errors.New("spotify: failed to create image for artist portrait") 2168 | } 2169 | return newImage(a.session, sp_image), nil 2170 | } 2171 | 2172 | type RelationType C.sp_relation_type 2173 | 2174 | const ( 2175 | // Not yet known 2176 | RelationTypeUnknown = RelationType(C.SP_RELATION_TYPE_UNKNOWN) 2177 | // No relation 2178 | RelationTypeNone = RelationType(C.SP_RELATION_TYPE_NONE) 2179 | // The currently logged in user is following this uer 2180 | RelationTypeUnIdirectional = RelationType(C.SP_RELATION_TYPE_UNIDIRECTIONAL) 2181 | // Bidirectional friendship established 2182 | RelationTypeBidirectional = RelationType(C.SP_RELATION_TYPE_BIDIRECTIONAL) 2183 | ) 2184 | 2185 | type User struct { 2186 | session *Session 2187 | sp_user *C.sp_user 2188 | 2189 | wg sync.WaitGroup 2190 | } 2191 | 2192 | func newUser(s *Session, sp_user *C.sp_user) *User { 2193 | C.sp_user_add_ref(sp_user) 2194 | user := &User{session: s, sp_user: sp_user} 2195 | // TODO make an inteface with release and some convenient func 2196 | runtime.SetFinalizer(user, (*User).release) 2197 | 2198 | if s.listenForUserInfoUpdates(user.isLoaded, user) { 2199 | user.wg.Add(1) 2200 | } 2201 | 2202 | return user 2203 | } 2204 | 2205 | func (u *User) release() { 2206 | if u.sp_user == nil { 2207 | panic("spotify: user object has no sp_user object") 2208 | } 2209 | C.sp_user_release(u.sp_user) 2210 | u.sp_user = nil 2211 | } 2212 | 2213 | func (u *User) cbUpdated() { 2214 | if u.isLoaded() { 2215 | u.wg.Done() 2216 | } 2217 | } 2218 | 2219 | func (u *User) Wait() { 2220 | u.wg.Wait() 2221 | u.session.stopListenForUserInfoUpdates(u) 2222 | } 2223 | 2224 | func (u *User) isLoaded() bool { 2225 | return C.sp_user_is_loaded(u.sp_user) == 1 2226 | } 2227 | 2228 | // CanonicalName returns the user's canonical username. 2229 | func (u *User) CanonicalName() string { 2230 | return C.GoString(C.sp_user_canonical_name(u.sp_user)) 2231 | } 2232 | 2233 | // ArtistsToplist loads the artist toplist for the user. 2234 | func (u *User) ArtistsToplist() *ArtistsToplist { 2235 | return newArtistsToplist(u.session, toplistRegionUser, u) 2236 | } 2237 | 2238 | // AlbumsToplist loads the album toplist for the user. 2239 | func (u *User) AlbumsToplist() *AlbumsToplist { 2240 | return newAlbumsToplist(u.session, toplistRegionUser, u) 2241 | } 2242 | 2243 | // TracksToplist loads the track toplist for the user. 2244 | func (u *User) TracksToplist() *TracksToplist { 2245 | return newTracksToplist(u.session, toplistRegionUser, u) 2246 | } 2247 | 2248 | // DisplayName returns the user's displayable username. 2249 | func (u *User) DisplayName() string { 2250 | return C.GoString(C.sp_user_display_name(u.sp_user)) 2251 | } 2252 | 2253 | func (u *User) Starred() *Playlist { 2254 | cuser := C.CString(u.CanonicalName()) 2255 | defer C.free(unsafe.Pointer(cuser)) 2256 | sp_playlist := C.sp_session_starred_for_user_create(u.session.sp_session, cuser) 2257 | return newPlaylist(u.session, sp_playlist, true) 2258 | } 2259 | 2260 | type Playlist struct { 2261 | session *Session 2262 | sp_playlist *C.sp_playlist 2263 | callbacks C.sp_playlist_callbacks 2264 | refOwned bool 2265 | 2266 | wg sync.WaitGroup 2267 | loaded chan struct{} 2268 | } 2269 | 2270 | func newPlaylist(s *Session, sp_playlist *C.sp_playlist, refOwned bool) *Playlist { 2271 | // TODO register all callbacks 2272 | p := &Playlist{ 2273 | session: s, 2274 | sp_playlist: sp_playlist, 2275 | refOwned: refOwned, 2276 | loaded: make(chan struct{}, 1), 2277 | } 2278 | 2279 | runtime.SetFinalizer(p, (*Playlist).release) 2280 | C.set_playlist_callbacks(&p.callbacks) 2281 | C.sp_playlist_add_callbacks(sp_playlist, &p.callbacks, unsafe.Pointer(p)) 2282 | 2283 | // TODO make a nice interface and expose channel 2284 | if p.isLoaded() { 2285 | p.loaded <- struct{}{} 2286 | } else { 2287 | p.wg.Add(1) 2288 | } 2289 | 2290 | return p 2291 | } 2292 | 2293 | func (p *Playlist) release() { 2294 | if p.sp_playlist == nil { 2295 | panic("spotify: playlist object has no sp_playlist object") 2296 | } 2297 | C.sp_playlist_remove_callbacks(p.sp_playlist, &p.callbacks, unsafe.Pointer(p)) 2298 | if p.refOwned { 2299 | C.sp_playlist_release(p.sp_playlist) 2300 | } 2301 | p.sp_playlist = nil 2302 | } 2303 | 2304 | func (p *Playlist) isLoaded() bool { 2305 | return C.sp_playlist_is_loaded(p.sp_playlist) == 1 2306 | } 2307 | 2308 | func (p *Playlist) cbStateChanged() { 2309 | if p.isLoaded() { 2310 | select { 2311 | case p.loaded <- struct{}{}: 2312 | p.wg.Done() 2313 | } 2314 | } 2315 | } 2316 | 2317 | //export go_playlist_state_changed 2318 | func go_playlist_state_changed(sp_playlist unsafe.Pointer, userdata unsafe.Pointer) { 2319 | (*Playlist)(userdata).cbStateChanged() 2320 | } 2321 | 2322 | func (p *Playlist) Wait() { 2323 | p.wg.Wait() 2324 | } 2325 | 2326 | func (p *Playlist) Link() *Link { 2327 | sp_link := C.sp_link_create_from_playlist(p.sp_playlist) 2328 | return newLink(p.session, sp_link, false) 2329 | } 2330 | 2331 | func (p *Playlist) Name() string { 2332 | return C.GoString(C.sp_playlist_name(p.sp_playlist)) 2333 | } 2334 | 2335 | func (p *Playlist) SetName(n string) error { 2336 | cname := C.CString(n) 2337 | defer C.free(unsafe.Pointer(cname)) 2338 | rc := C.sp_playlist_rename(p.sp_playlist, cname) 2339 | if rc != C.SP_ERROR_OK { 2340 | return spError(rc) 2341 | } 2342 | return nil 2343 | } 2344 | 2345 | func (p *Playlist) Owner() (*User, error) { 2346 | sp_user := C.sp_playlist_owner(p.sp_playlist) 2347 | if sp_user == nil { 2348 | return nil, errors.New("spotify: unknown user") 2349 | } 2350 | return newUser(p.session, sp_user), nil 2351 | } 2352 | 2353 | func (p *Playlist) Tracks() int { 2354 | return int(C.sp_playlist_num_tracks(p.sp_playlist)) 2355 | } 2356 | 2357 | func (p *Playlist) Track(n int) *PlaylistTrack { 2358 | if n < 0 || n >= p.Tracks() { 2359 | panic("spotify: playlist track out of range") 2360 | } 2361 | // TODO hook into the playlist to know when the index changes etc? 2362 | return &PlaylistTrack{p, n} 2363 | } 2364 | 2365 | func (p *Playlist) Collaborative() bool { 2366 | return C.sp_playlist_is_collaborative(p.sp_playlist) == 1 2367 | } 2368 | 2369 | func (p *Playlist) SetCollaborative(c bool) { 2370 | C.sp_playlist_set_collaborative(p.sp_playlist, cbool(c)) 2371 | } 2372 | 2373 | // SetAutolinkTracks sets the autolinking state for a playlist. 2374 | // 2375 | // If a playlist is autolinked, unplayable tracks will be made playable by 2376 | // linking them to other Spotify tracks, where possible. 2377 | func (p *Playlist) SetAutolinkTracks(l bool) { 2378 | C.sp_playlist_set_autolink_tracks(p.sp_playlist, cbool(l)) 2379 | } 2380 | 2381 | func (p *Playlist) Description() string { 2382 | return C.GoString(C.sp_playlist_get_description(p.sp_playlist)) 2383 | } 2384 | 2385 | func (p *Playlist) Image() (*Image, error) { 2386 | id := make([]byte, 20) 2387 | if C.sp_playlist_get_image(p.sp_playlist, (*C.byte)(&id[0])) == 0 { 2388 | return nil, errors.New("spotify: playlist has no image") 2389 | } 2390 | sp_image := C.sp_image_create(p.session.sp_session, (*C.byte)(&id[0])) 2391 | if sp_image == nil { 2392 | return nil, errors.New("spotify: failed to create image for playlist") 2393 | } 2394 | return newImage(p.session, sp_image), nil 2395 | } 2396 | 2397 | func (p *Playlist) HasPendingChanges() bool { 2398 | return C.sp_playlist_has_pending_changes(p.sp_playlist) == 1 2399 | } 2400 | 2401 | // TODO sp_playlist_add_tracks 2402 | // TODO sp_playlist_remove_tracks 2403 | // TODO sp_playlist_reorder_tracks 2404 | 2405 | func (p *Playlist) NumSubscribers() int { 2406 | return int(C.sp_playlist_num_subscribers(p.sp_playlist)) 2407 | } 2408 | 2409 | // TODO sp_playlist_subscribers 2410 | 2411 | func (p *Playlist) InMemory() bool { 2412 | return C.sp_playlist_is_in_ram(p.session.sp_session, p.sp_playlist) == 1 2413 | } 2414 | 2415 | func (p *Playlist) LoadInMemory(m bool) { 2416 | C.sp_playlist_set_in_ram(p.session.sp_session, p.sp_playlist, cbool(m)) 2417 | } 2418 | 2419 | func (p *Playlist) SetOffline(o bool) { 2420 | C.sp_playlist_set_offline_mode(p.session.sp_session, p.sp_playlist, cbool(o)) 2421 | } 2422 | 2423 | func (p *Playlist) Offline() PlaylistOfflineStatus { 2424 | s := C.sp_playlist_get_offline_status(p.session.sp_session, p.sp_playlist) 2425 | return PlaylistOfflineStatus(s) 2426 | } 2427 | 2428 | type PlaylistOfflineStatus C.sp_playlist_offline_status 2429 | 2430 | const ( 2431 | // Playlist is not offline enabled 2432 | PlaylistOfflineStatusNo = PlaylistOfflineStatus(C.SP_PLAYLIST_OFFLINE_STATUS_NO) 2433 | // Playlist is synchronized to local storage 2434 | PlaylistOfflineStatusYes = PlaylistOfflineStatus(C.SP_PLAYLIST_OFFLINE_STATUS_YES) 2435 | // This playlist is currently downloading. Only one playlist can be in this state any given time 2436 | PlaylistOfflineStatusDownloading = PlaylistOfflineStatus(C.SP_PLAYLIST_OFFLINE_STATUS_DOWNLOADING) 2437 | // Playlist is queued for download 2438 | PlaylistOfflineStatusWaiting = PlaylistOfflineStatus(C.SP_PLAYLIST_OFFLINE_STATUS_WAITING) 2439 | ) 2440 | 2441 | type PlaylistTrack struct { 2442 | playlist *Playlist 2443 | index int 2444 | } 2445 | 2446 | // User returns the user that added the track to the playlist. 2447 | func (pt *PlaylistTrack) User() *User { 2448 | sp_user := C.sp_playlist_track_creator(pt.playlist.sp_playlist, C.int(pt.index)) 2449 | return newUser(pt.playlist.session, sp_user) 2450 | } 2451 | 2452 | // Time returns the time when the track was added to the playlist. 2453 | func (pt *PlaylistTrack) Time() time.Time { 2454 | t := C.sp_playlist_track_create_time(pt.playlist.sp_playlist, C.int(pt.index)) 2455 | return time.Unix(int64(t), 0) 2456 | } 2457 | 2458 | // Track returns the track metadata object for the playlist entry. 2459 | func (pt *PlaylistTrack) Track() *Track { 2460 | // TODO return PlaylistTrack and add extra functionality on top of that 2461 | sp_track := C.sp_playlist_track(pt.playlist.sp_playlist, C.int(pt.index)) 2462 | return newTrack(pt.playlist.session, sp_track) 2463 | } 2464 | 2465 | // Seen returns true if the entry has been marked as seen or not. 2466 | func (pt *PlaylistTrack) Seen() bool { 2467 | seen := C.sp_playlist_track_seen(pt.playlist.sp_playlist, C.int(pt.index)) 2468 | return seen == 1 2469 | } 2470 | 2471 | // SetSeen marks the playlist track item as seen or not. 2472 | func (pt *PlaylistTrack) SetSeen(seen bool) error { 2473 | rc := C.sp_playlist_track_set_seen(pt.playlist.sp_playlist, C.int(pt.index), cbool(seen)) 2474 | if rc != C.SP_ERROR_OK { 2475 | return spError(rc) 2476 | } 2477 | return nil 2478 | } 2479 | 2480 | // Message returns the message attached to a playlist item. Typically used on inbox. 2481 | // TODO only expose this for inbox? 2482 | func (pt *PlaylistTrack) Message() string { 2483 | cmsg := C.sp_playlist_track_message(pt.playlist.sp_playlist, C.int(pt.index)) 2484 | if cmsg == nil { 2485 | return "" 2486 | } 2487 | return C.GoString(cmsg) 2488 | } 2489 | 2490 | type toplistType C.sp_toplisttype 2491 | 2492 | const ( 2493 | toplistTypeArtists = toplistType(C.SP_TOPLIST_TYPE_ARTISTS) 2494 | toplistTypeAlbums = toplistType(C.SP_TOPLIST_TYPE_ALBUMS) 2495 | toplistTypeTracks = toplistType(C.SP_TOPLIST_TYPE_TRACKS) 2496 | ) 2497 | 2498 | type Region int 2499 | 2500 | func (r Region) String() string { 2501 | return string([]byte{byte(r >> 8), byte(r)}) 2502 | } 2503 | 2504 | type ToplistRegion Region 2505 | 2506 | const ( 2507 | // Global toplist 2508 | ToplistRegionEverywhere = ToplistRegion(C.SP_TOPLIST_REGION_EVERYWHERE) 2509 | 2510 | // Toplist for the given user 2511 | toplistRegionUser = ToplistRegion(C.SP_TOPLIST_REGION_USER) 2512 | ) 2513 | 2514 | // NewToplistRegion returns the toplist region for a ISO 2515 | // 3166-1 country code. 2516 | // 2517 | // Also see ToplistRegionEverywhere and ToplistRegionUser 2518 | // for some special constants. 2519 | func NewToplistRegion(region string) (ToplistRegion, error) { 2520 | if len(region) != 2 { 2521 | return 0, errors.New("spotify: invalid toplist region") 2522 | } 2523 | region = strings.ToUpper(region) 2524 | r := int(region[0])<<8 | int(region[1]) 2525 | return ToplistRegion(r), nil 2526 | } 2527 | 2528 | func (r ToplistRegion) String() string { 2529 | switch r { 2530 | case ToplistRegionEverywhere: 2531 | return "Worldwide" 2532 | case toplistRegionUser: 2533 | return "User" 2534 | default: 2535 | return (Region)(r).String() 2536 | } 2537 | } 2538 | 2539 | type toplist struct { 2540 | session *Session 2541 | 2542 | sp_toplistbrowse *C.sp_toplistbrowse 2543 | ttype toplistType 2544 | 2545 | wg sync.WaitGroup 2546 | } 2547 | 2548 | // newToplist creates a wrapper around the toplist object. If the user object 2549 | // is nil, the global toplist for the region will be used. Both user and region 2550 | // can be specified if the toplist for the user's region should be fetched. 2551 | func newToplist(s *Session, ttype toplistType, r ToplistRegion, user *User) *toplist { 2552 | var cusername *C.char 2553 | if user != nil { 2554 | cusername = C.CString(user.CanonicalName()) 2555 | defer C.free(unsafe.Pointer(cusername)) 2556 | } 2557 | 2558 | t := &toplist{session: s, ttype: ttype} 2559 | t.wg.Add(1) 2560 | t.sp_toplistbrowse = C.toplistbrowse_create( 2561 | t.session.sp_session, 2562 | C.sp_toplisttype(ttype), 2563 | C.sp_toplistregion(r), 2564 | cusername, 2565 | unsafe.Pointer(t), 2566 | ) 2567 | runtime.SetFinalizer(t, (*toplist).release) 2568 | return t 2569 | } 2570 | 2571 | func (t *toplist) release() { 2572 | if t.sp_toplistbrowse == nil { 2573 | panic("spotify: toplist object has no sp_toplistbrowse object") 2574 | } 2575 | C.sp_toplistbrowse_release(t.sp_toplistbrowse) 2576 | t.sp_toplistbrowse = nil 2577 | } 2578 | 2579 | func (t *toplist) cbComplete() { 2580 | t.session.log(LogDebug, "toplist done") 2581 | t.wg.Done() 2582 | } 2583 | 2584 | func (t *toplist) Wait() { 2585 | t.session.log(LogDebug, "waiting for toplist") 2586 | t.wg.Wait() 2587 | } 2588 | 2589 | func (t *toplist) Error() error { 2590 | return spError(C.sp_toplistbrowse_error(t.sp_toplistbrowse)) 2591 | } 2592 | 2593 | // Duration returns the time spent waiting for 2594 | // the Spotify backend to serve the toplist. 2595 | func (t *toplist) Duration() time.Duration { 2596 | ms := C.sp_toplistbrowse_backend_request_duration(t.sp_toplistbrowse) 2597 | if ms < 0 { 2598 | ms = 0 2599 | } 2600 | return time.Duration(ms) * time.Millisecond 2601 | } 2602 | 2603 | // TODO plural here, really? 2604 | type ArtistsToplist struct { 2605 | *toplist 2606 | } 2607 | 2608 | func newArtistsToplist(s *Session, r ToplistRegion, user *User) *ArtistsToplist { 2609 | toplist := newToplist(s, toplistTypeArtists, r, user) 2610 | return &ArtistsToplist{toplist} 2611 | } 2612 | 2613 | func (at *ArtistsToplist) Artists() int { 2614 | return int(C.sp_toplistbrowse_num_artists(at.sp_toplistbrowse)) 2615 | } 2616 | 2617 | func (at *ArtistsToplist) Artist(n int) *Artist { 2618 | if n < 0 || n >= at.Artists() { 2619 | panic("spotify: toplist artist out of range") 2620 | } 2621 | sp_artist := C.sp_toplistbrowse_artist(at.sp_toplistbrowse, C.int(n)) 2622 | return newArtist(at.session, sp_artist) 2623 | } 2624 | 2625 | // TODO 2626 | type AlbumsToplist struct { 2627 | *toplist 2628 | } 2629 | 2630 | func newAlbumsToplist(s *Session, r ToplistRegion, user *User) *AlbumsToplist { 2631 | toplist := newToplist(s, toplistTypeAlbums, r, user) 2632 | return &AlbumsToplist{toplist} 2633 | } 2634 | 2635 | func (at *AlbumsToplist) Albums() int { 2636 | return int(C.sp_toplistbrowse_num_albums(at.sp_toplistbrowse)) 2637 | } 2638 | 2639 | func (at *AlbumsToplist) Album(n int) *Album { 2640 | if n < 0 || n >= at.Albums() { 2641 | panic("spotify: toplist album out of range") 2642 | } 2643 | sp_album := C.sp_toplistbrowse_album(at.sp_toplistbrowse, C.int(n)) 2644 | return newAlbum(at.session, sp_album) 2645 | } 2646 | 2647 | type TracksToplist struct { 2648 | *toplist 2649 | } 2650 | 2651 | func newTracksToplist(s *Session, r ToplistRegion, user *User) *TracksToplist { 2652 | toplist := newToplist(s, toplistTypeTracks, r, user) 2653 | return &TracksToplist{toplist} 2654 | } 2655 | 2656 | // Tracks returns the numbers of tracks in the toplist. 2657 | func (tt *TracksToplist) Tracks() int { 2658 | return int(C.sp_toplistbrowse_num_tracks(tt.sp_toplistbrowse)) 2659 | } 2660 | 2661 | // Track returns the track given the index from the toplist. 2662 | func (tt *TracksToplist) Track(n int) *Track { 2663 | if n < 0 || n >= tt.Tracks() { 2664 | panic("spotify: toplist track out of range") 2665 | } 2666 | sp_track := C.sp_toplistbrowse_track(tt.sp_toplistbrowse, C.int(n)) 2667 | return newTrack(tt.session, sp_track) 2668 | } 2669 | 2670 | type ImageSize C.sp_image_size 2671 | 2672 | const ( 2673 | // Normal image size 2674 | ImageSizeNormal = ImageSize(C.SP_IMAGE_SIZE_NORMAL) 2675 | 2676 | // Small image size 2677 | ImageSizeSmall = ImageSize(C.SP_IMAGE_SIZE_SMALL) 2678 | 2679 | // Large image size 2680 | ImageSizeLarge = ImageSize(C.SP_IMAGE_SIZE_LARGE) 2681 | ) 2682 | 2683 | type ImageFormat C.sp_imageformat 2684 | 2685 | const ( 2686 | // Unknown image format 2687 | ImageFormatUnknown = ImageFormat(C.SP_IMAGE_FORMAT_UNKNOWN) 2688 | 2689 | // JPEG image 2690 | ImageFormatJpeg = ImageFormat(C.SP_IMAGE_FORMAT_JPEG) 2691 | ) 2692 | 2693 | type Image struct { 2694 | session *Session 2695 | sp_image *C.sp_image 2696 | 2697 | wg sync.WaitGroup 2698 | loaded chan struct{} 2699 | } 2700 | 2701 | func newImage(s *Session, sp_image *C.sp_image) *Image { 2702 | i := &Image{ 2703 | session: s, 2704 | sp_image: sp_image, 2705 | loaded: make(chan struct{}, 1), 2706 | } 2707 | if i.isLoaded() { 2708 | i.loaded <- struct{}{} 2709 | } else { 2710 | i.wg.Add(1) 2711 | } 2712 | C.set_image_callback(sp_image, unsafe.Pointer(i)) 2713 | runtime.SetFinalizer(i, (*Image).release) 2714 | return i 2715 | } 2716 | 2717 | func (i *Image) release() { 2718 | if i.sp_image == nil { 2719 | panic("spotify: image object has no sp_image object") 2720 | } 2721 | C.sp_image_release(i.sp_image) 2722 | i.sp_image = nil 2723 | } 2724 | 2725 | func (i *Image) cbComplete() { 2726 | select { 2727 | case i.loaded <- struct{}{}: 2728 | i.wg.Done() 2729 | default: 2730 | } 2731 | } 2732 | 2733 | func (i *Image) Wait() { 2734 | i.wg.Wait() 2735 | } 2736 | 2737 | func (i *Image) isLoaded() bool { 2738 | return C.sp_image_is_loaded(i.sp_image) == 1 2739 | } 2740 | 2741 | // Error returns an error associated with an image. 2742 | func (i *Image) Error() error { 2743 | return spError(C.sp_image_error(i.sp_image)) 2744 | } 2745 | 2746 | // Data returns the image data. 2747 | func (i *Image) Data() []byte { 2748 | var size C.size_t 2749 | var data unsafe.Pointer = C.sp_image_data(i.sp_image, &size) 2750 | return C.GoBytes(data, C.int(size)) 2751 | } 2752 | 2753 | // Decode returns a decoded image and the format name used during format 2754 | // registration. 2755 | func (i *Image) Decode() (image.Image, string, error) { 2756 | data := i.Data() 2757 | buffer := bytes.NewBuffer(data) 2758 | return image.Decode(buffer) 2759 | } 2760 | 2761 | // Format returns the image format. 2762 | func (i *Image) Format() ImageFormat { 2763 | return ImageFormat(C.sp_image_format(i.sp_image)) 2764 | } 2765 | 2766 | // BuildId returns the libspotify build ID. 2767 | func BuildId() string { 2768 | return C.GoString(C.sp_build_id()) 2769 | } 2770 | -------------------------------------------------------------------------------- /spotify/libspotify.h: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Örjan Persson 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #ifndef __GO_LIBSPOTIFY_H 16 | #define __GO_LIBSPOTIFY_H 17 | 18 | #include 19 | #include 20 | 21 | void set_callbacks(sp_session_callbacks*); 22 | 23 | void SP_CALLCONV cb_logged_in(sp_session *session, sp_error error); 24 | void SP_CALLCONV cb_logged_out(sp_session *session); 25 | void SP_CALLCONV cb_metadata_updated(sp_session *session); 26 | void SP_CALLCONV cb_connection_error(sp_session *session, sp_error error); 27 | void SP_CALLCONV cb_message_to_user(sp_session *session, const char *message); 28 | void SP_CALLCONV cb_notify_main_thread(sp_session *session); 29 | int SP_CALLCONV cb_music_delivery(sp_session *session, const sp_audioformat *format, const void *frames, int num_frames); 30 | void SP_CALLCONV cb_play_token_lost(sp_session *session); 31 | void SP_CALLCONV cb_log_message(sp_session *session, const char *data); 32 | void SP_CALLCONV cb_end_of_track(sp_session *session); 33 | void SP_CALLCONV cb_streaming_error(sp_session *session, sp_error error); 34 | void SP_CALLCONV cb_userinfo_updated(sp_session *session); 35 | void SP_CALLCONV cb_start_playback(sp_session *session); 36 | void SP_CALLCONV cb_stop_playback(sp_session *session); 37 | void SP_CALLCONV cb_get_audio_buffer_stats(sp_session *session, sp_audio_buffer_stats *stats); 38 | void SP_CALLCONV cb_offline_status_updated(sp_session *session); 39 | void SP_CALLCONV cb_offline_error(sp_session *session, sp_error error); 40 | void SP_CALLCONV cb_credentials_blob_updated(sp_session *session, const char *blob); 41 | void SP_CALLCONV cb_connectionstate_updated(sp_session *session); 42 | void SP_CALLCONV cb_scrobble_error(sp_session *session, sp_error error); 43 | void SP_CALLCONV cb_private_session_mode_changed(sp_session *session, bool is_private); 44 | 45 | sp_search* search_create(sp_session *session, const char *query, int track_offset, int track_count, int album_offset, int album_count, int artist_offset, int artist_count, int playlist_offset, int playlist_count, sp_search_type search_type, void *userdata); 46 | void SP_CALLCONV cb_search_complete(sp_search *search, void *userdata); 47 | sp_toplistbrowse* toplistbrowse_create(sp_session *session, sp_toplisttype type, sp_toplistregion region, const char *username, void *userdata); 48 | void SP_CALLCONV cb_toplistbrowse_complete(sp_toplistbrowse *toplist, void *userdata); 49 | 50 | void set_playlistcontainer_callbacks(sp_playlistcontainer_callbacks*); 51 | void SP_CALLCONV cb_playlistcontainer_playlist_added(sp_playlistcontainer *pc, sp_playlist *playlist, int position, void *userdata); 52 | void SP_CALLCONV cb_playlistcontainer_loaded(sp_playlistcontainer *pc, void *userdata); 53 | 54 | void set_playlist_callbacks(sp_playlist_callbacks*); 55 | void SP_CALLCONV cb_playlist_state_changed(sp_playlist *playlist, void *userdata); 56 | 57 | void set_image_callback(sp_image *image, void *userdata); 58 | void SP_CALLCONV cb_image_complete(sp_image *image, void *userdata); 59 | 60 | #endif // __GO_LIBSPOTIFY_H 61 | -------------------------------------------------------------------------------- /spotify/log.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Örjan Persson 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package spotify 16 | 17 | import ( 18 | "errors" 19 | "fmt" 20 | "regexp" 21 | "strings" 22 | "time" 23 | ) 24 | 25 | var ( 26 | errLogInvalidFormat = errors.New("spotify: invalid log message") 27 | ) 28 | 29 | // logMessageRe is used to parse the log message from libspotify after the 30 | // timestamp (15:04:05.999) in the format: 31 | // D [module:line] message 32 | var logMessageRe = regexp.MustCompile(`^([\dA-Z]) \[([^ \]]+)\s*\] (.*)[\n]*$`) 33 | 34 | type LogLevel int 35 | 36 | const ( 37 | LogFatal LogLevel = iota 38 | LogError 39 | LogWarning 40 | LogInfo 41 | LogDebug 42 | ) 43 | 44 | var logLevels = map[string]LogLevel{ 45 | "F": LogFatal, 46 | "E": LogError, 47 | "W": LogWarning, 48 | "I": LogInfo, 49 | "D": LogDebug, 50 | } 51 | 52 | type LogMessage struct { 53 | Time time.Time 54 | Level LogLevel 55 | Module string 56 | Message string 57 | } 58 | 59 | func (l *LogMessage) String() string { 60 | return fmt.Sprintf("[%s] %s", l.Module, l.Message) 61 | } 62 | 63 | // parseLogMessage will parse libspotify generated log messages into LogMessage 64 | // and return any error if it failed. This function might return a usable log 65 | // message together with an error, which might indicate that just some of the 66 | // fields failed to be parsed. 67 | // 68 | // The full format of the log message is: 69 | // 15:04:05.999 D [module:line] message 70 | func parseLogMessage(line string) (*LogMessage, error) { 71 | var tsLayout = "15:04:05.999" 72 | 73 | // The time and the rest is separated by a space. Parse them individually. 74 | pos := strings.Index(line, " ") 75 | if pos != len(tsLayout) && len(line) < pos+1 { 76 | return nil, errLogInvalidFormat 77 | } 78 | ts := line[0:pos] 79 | unparsed := line[pos+1:] 80 | 81 | // Parse the timestamp, which is reported in local time. If the difference 82 | // between the current time and the parsed time is too big, we assume that 83 | // the reported time is for the previous day. 84 | now := time.Now() 85 | t, err := time.ParseInLocation(tsLayout, ts, time.Local) 86 | if err != nil { 87 | return nil, err 88 | } 89 | t = t.AddDate(now.Year(), int(now.Month())-1, now.Day()-1) 90 | if t.Hour() > now.Hour() { 91 | t = t.Add(-24 * time.Hour) 92 | } 93 | 94 | m := logMessageRe.FindStringSubmatch(unparsed) 95 | if m == nil { 96 | return nil, errLogInvalidFormat 97 | } 98 | strLevel := m[1] 99 | module := strings.Trim(m[2], " ") 100 | message := strings.Trim(m[3], " ") 101 | 102 | level, exists := logLevels[strLevel] 103 | if !exists { 104 | level = LogError 105 | err = fmt.Errorf("spotify: unknown log level: %s", strLevel) 106 | } 107 | 108 | return &LogMessage{t, level, module, message}, err 109 | } 110 | -------------------------------------------------------------------------------- /spotify/log_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Örjan Persson 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package spotify 16 | 17 | import ( 18 | "errors" 19 | "reflect" 20 | "testing" 21 | "time" 22 | ) 23 | 24 | // newLogMessage creates a log message with the current dates timestamp at 25 | // 00:00:00, with the time diff applied to it. 26 | func newLogMessage(diff time.Duration, level LogLevel, module, message string) *LogMessage { 27 | t := time.Now().Round(time.Hour) 28 | t = t.Add(-1 * time.Duration(t.Hour()) * time.Hour) 29 | msg := &LogMessage{ 30 | Time: t.Add(diff), 31 | Level: level, 32 | Module: module, 33 | Message: message, 34 | } 35 | return msg 36 | } 37 | 38 | func TestNewLogMessage(t *testing.T) { 39 | var tests = []struct { 40 | line string 41 | expected *LogMessage 42 | err error 43 | }{ 44 | { 45 | line: `23:59:00.123 F [ap:1226 ] Send SPDY query (2) 'http://playlist/user/o.p/playlist/' (DIFF) 46 | `, 47 | expected: newLogMessage( 48 | -1*(59*time.Second+877*time.Millisecond), 49 | LogFatal, 50 | "ap:1226", 51 | "Send SPDY query (2) 'http://playlist/user/o.p/playlist/' (DIFF)", 52 | ), 53 | }, 54 | { 55 | line: `00:00:01.001 E [ap:1226] `, 56 | expected: newLogMessage( 57 | 1*time.Second+1*time.Millisecond, 58 | LogError, 59 | "ap:1226", 60 | "", 61 | ), 62 | }, 63 | { 64 | line: `00:00:01.001 W [ap:1226] `, 65 | expected: newLogMessage( 66 | 1*time.Second+1*time.Millisecond, 67 | LogWarning, 68 | "ap:1226", 69 | "", 70 | ), 71 | }, 72 | { 73 | line: `00:00:01.001 I [ap:1226] `, 74 | expected: newLogMessage( 75 | 1*time.Second+1*time.Millisecond, 76 | LogInfo, 77 | "ap:1226", 78 | "", 79 | ), 80 | }, 81 | { 82 | line: `00:00:01.001 D [ap:343] ChannelError(1, 1, link-tracks)`, 83 | expected: newLogMessage( 84 | 1*time.Second+1*time.Millisecond, 85 | LogDebug, 86 | "ap:343", 87 | "ChannelError(1, 1, link-tracks)", 88 | ), 89 | }, 90 | { 91 | line: `00:00:01.001 X [ap:1226] `, 92 | expected: newLogMessage( 93 | 1*time.Second+1*time.Millisecond, 94 | LogError, 95 | "ap:1226", 96 | "", 97 | ), 98 | err: errors.New("spotify: unknown log level: X"), 99 | }, 100 | { 101 | line: `00:00:01.001 D `, 102 | err: errLogInvalidFormat, 103 | }, 104 | } 105 | 106 | for _, test := range tests { 107 | m, err := parseLogMessage(test.line) 108 | 109 | // Convert errors to string since errors.New("") != errors.New("") 110 | if err != test.err && err.Error() != test.err.Error() { 111 | t.Fatal(err) 112 | } 113 | 114 | if !reflect.DeepEqual(m, test.expected) { 115 | t.Errorf("%#v != %#v", m, test.expected) 116 | } 117 | } 118 | } 119 | --------------------------------------------------------------------------------