jberkel-spotify-api 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ pkg
data/LICENSE ADDED
@@ -0,0 +1,24 @@
1
+ Copyright (c) 2009, Jan Berkel
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without
5
+ modification, are permitted provided that the following conditions are met:
6
+ * Redistributions of source code must retain the above copyright
7
+ notice, this list of conditions and the following disclaimer.
8
+ * Redistributions in binary form must reproduce the above copyright
9
+ notice, this list of conditions and the following disclaimer in the
10
+ documentation and/or other materials provided with the distribution.
11
+ * Neither the name of spotify-api nor the
12
+ names of its contributors may be used to endorse or promote products
13
+ derived from this software without specific prior written permission.
14
+
15
+ THIS SOFTWARE IS PROVIDED BY Jan Berkel 'AS IS'' AND ANY
16
+ EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
17
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
18
+ DISCLAIMED. IN NO EVENT SHALL David R. MacIver BE LIABLE FOR ANY
19
+ DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
20
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
21
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
22
+ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
23
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
24
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,91 @@
1
+
2
+ # Spotify-API
3
+
4
+ I got tired of waiting for Spotify to release their API (if they release it), so decided to roll my own based on the awesome
5
+ work done by [#hack.se](http://despotify.se) (despotify) and Felix Bruns who created a Java port of despotify called
6
+ [jotify](http://jotify.felixbruns.de/). Add JRuby, Sinatra and some glue to the mix and you (almost) have an API.
7
+
8
+ *Important*: in order to use this API you'll need a premium spotify account! This API is as unofficial as it gets and *NOT* supported
9
+ by Spotify in any way.
10
+
11
+ At the moment the following features are implemented:
12
+
13
+ * searching [GET /(albums|tracks|artists)?name=Foo]
14
+ * list user's playlists [GET /playlists]
15
+ * get shared playlist [GET /playlist/id]
16
+ * create new playlist [POST /playlists]
17
+ * add tracks to playlist [PUT /playlists/id]
18
+
19
+ ## Installation
20
+
21
+ Prerequisites: *Java 6+*, JRuby 1.3.x.
22
+
23
+ $ jruby -S gem install jberkel-spotify-api --source http://gems.github.com
24
+ $ jruby -S spotify-api-server --account login:password
25
+ == Sinatra/0.9.4 has taken the stage on 3000 for development with backup from WEBrick
26
+ [2009-08-04 01:21:03] INFO WEBrick 1.3.1
27
+ [2009-08-04 01:21:03] INFO ruby 1.8.6 (2009-07-24) [java]
28
+ [2009-08-04 01:21:03] INFO WEBrick::HTTPServer#start: pid=12162 port=3000
29
+
30
+ $ curl http://localhost:3000/playlists | jsonpretty
31
+ {
32
+ "result": {
33
+ "playlists": [
34
+ {
35
+ "name": "my shiny playlist",
36
+ "author": "jberkel",
37
+ "url": "http:\/\/open.spotify.com\/user\/jberkel\/playlist\/5EXLGE7HPVPjvlxPmIfrDe",
38
+ "revision": 2,
39
+ "id": "b9fe3dcf88945d146ef18117faa61ab4",
40
+ "tracks": [
41
+ {
42
+ "artist": "Elmore Judd",
43
+ "title": "Disco In 4 Pieces",
44
+ "url": "http:\/\/open.spotify.com\/track\/1VaucR6Bsks5Q9bYBsXvuF",
45
+ "id": "3f2752a98dd947c5855278a88159d7b1",
46
+ "album": "Insect Funk",
47
+ "popularity": 0.325379997491837
48
+ },
49
+ {
50
+ "artist": "40winks",
51
+ "title": "Goodmorning (intro)",
52
+ "url": "http:\/\/open.spotify.com\/track\/6qHiOf1BFCQIzAjJsRbMfY",
53
+ "id": "d34a3a6daeed4f93983068e4e8c26cd6",
54
+ "album": "Sound Puzzle",
55
+ "popularity": 0.170609995722771
56
+ }
57
+ ],
58
+ "collaborative": false
59
+ }
60
+ ]
61
+ },
62
+ "status": "OK"
63
+ }
64
+
65
+ See examples directory for usage.
66
+
67
+ ## Credits
68
+
69
+ Contains code from the jotify project:
70
+
71
+ Copyright (c) 2009, Felix Bruns <felixbruns@web.de>
72
+ All rights reserved.
73
+
74
+ Redistribution and use in source and binary forms, with or without
75
+ modification, are permitted provided that the following conditions are met:
76
+ * Redistributions of source code must retain the above copyright
77
+ notice, this list of conditions and the following disclaimer.
78
+ * Redistributions in binary form must reproduce the above copyright
79
+ notice, this list of conditions and the following disclaimer in the
80
+ documentation and/or other materials provided with the distribution.
81
+
82
+ THIS SOFTWARE IS PROVIDED BY THE AUTHORS ''AS IS'' AND ANY
83
+ EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
84
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
85
+ DISCLAIMED. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY
86
+ DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
87
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
88
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
89
+ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
90
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
91
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,33 @@
1
+ require 'rubygems'
2
+ require 'rake/testtask'
3
+ require 'spec/rake/spectask'
4
+
5
+ begin
6
+ require 'jeweler'
7
+ Jeweler::Tasks.new do |gem|
8
+ gem.name = "spotify-api"
9
+ gem.summary = "an api for spotify, based on jotify"
10
+ gem.email = "jan.berkel@gmail.com"
11
+ gem.homepage = "http://github.com/jberkel/spotify-api"
12
+ gem.description = "an api for spotify, based on jotify"
13
+ gem.authors = ["Jan Berkel"]
14
+ gem.add_dependency "rack"
15
+ gem.add_dependency "rack-test"
16
+ gem.add_dependency "sinatra"
17
+ gem.add_dependency "json-jruby"
18
+ end
19
+ rescue LoadError
20
+ puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
21
+ end
22
+
23
+ Spec::Rake::SpecTask.new do |t|
24
+ t.rcov = false
25
+ t.spec_files = FileList["spec/**/*_spec.rb"].delete_if { |f| f =~ /integration/ }
26
+ t.libs << "./lib"
27
+ end
28
+
29
+ Spec::Rake::SpecTask.new(:integration) do |t|
30
+ t.rcov = false
31
+ t.spec_files = FileList["spec/**/*_spec.rb"].select { |f| f =~ /integration/ }
32
+ t.libs << "./lib"
33
+ end
@@ -0,0 +1,4 @@
1
+ ---
2
+ :patch: 0
3
+ :major: 0
4
+ :minor: 1
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env jruby
2
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
3
+
4
+ require 'rubygems'
5
+ require 'jotify'
6
+ require 'jotify/api'
7
+
8
+ DEFAULT_PORT = port = 3000
9
+
10
+ def usage
11
+ STDERR.puts "#{File.basename($0)} [-p <port>] [--account <login>:<password>]"
12
+ exit(1)
13
+ end
14
+
15
+ case ARGV.shift
16
+ when '-p', '--port': port = ARGV.shift
17
+ when '--account':
18
+ login, password = ARGV.shift.to_s.split(':')
19
+ raise ArgumentError, "you need to specify both login and password!" unless login and password
20
+ Jotify.credentials = { :username=>login, :password=>password }
21
+ when '-h', '--help': usage
22
+ end
23
+
24
+ #Sinatra::Application.set :environment, :production
25
+ Sinatra::Application.run! :port=> (port || DEFAULT_PORT).to_i
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require 'rest_client'
5
+ require 'json'
6
+ require 'pp'
7
+
8
+ puts "creating a new playlist"
9
+ resp = RestClient.post 'http://localhost:3000/playlists', :name=>'my shiny playlist'
10
+ if resp.code == 201
11
+ location = resp.headers[:location]
12
+ puts "201 created (#{location})"
13
+ spotify_id = location[location.rindex('/')+1..-1]
14
+
15
+ puts "adding some tracks to it"
16
+ resp = RestClient.put "http://localhost:3000/playlists/#{spotify_id}", {
17
+ "tracks" => [ {'id'=>'6qHiOf1BFCQIzAjJsRbMfY'}, {'id'=>'1VaucR6Bsks5Q9bYBsXvuF'} ]
18
+ }.to_json
19
+ puts resp
20
+ else
21
+ raise resp.to_s
22
+ end
23
+
24
+
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require 'rest_client'
5
+ require 'json'
6
+ require 'pp'
7
+
8
+ puts "getting all playlists"
9
+ resp = RestClient.get 'http://localhost:3000/playlists'
10
+ playlists = JSON.parse(resp)
11
+ pp playlists
12
+
13
+ playlists['result']['playlists'].each do |p|
14
+ puts "retrieving details for playlist #{p['name']}:"
15
+ pp JSON.parse(RestClient.get "http://localhost:3000/playlists/#{p['id']}")
16
+ end
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require 'rest_client'
5
+ require 'json'
6
+ require 'pp'
7
+
8
+ puts "searching for album: Moon Safari"
9
+ pp JSON.parse(RestClient.get('http://localhost:3000/albums?name=Moon+Safari'))
10
+
11
+ puts "searching for track: The End"
12
+ pp JSON.parse(RestClient.get('http://localhost:3000/tracks?name=The+End'))
13
+
14
+ puts "searching for artist: The Doors"
15
+ pp JSON.parse(RestClient.get('http://localhost:3000/artists?name=The+Doors'))
16
+
Binary file
@@ -0,0 +1,109 @@
1
+ require 'java'
2
+ require File.expand_path(File.dirname(__FILE__) + '/jars/jotify.jar')
3
+
4
+ class Jotify
5
+
6
+ module Media
7
+ import 'de.felixbruns.jotify.media.Playlist'
8
+ import 'de.felixbruns.jotify.media.PlaylistContainer'
9
+ import 'de.felixbruns.jotify.media.Result'
10
+ import 'de.felixbruns.jotify.media.Track'
11
+ import 'de.felixbruns.jotify.media.Artist'
12
+ import 'de.felixbruns.jotify.media.Album'
13
+ end
14
+
15
+ import 'de.felixbruns.jotify.gui.util.JotifyPreferences'
16
+ import 'de.felixbruns.jotify.util.SpotifyURI'
17
+
18
+ ByPopularity = Proc.new { |a,b| b.popularity <=> a.popularity }
19
+
20
+ [:close, :search].each do |m|
21
+ define_method(m) do |*args|
22
+ @jotify.send(m, *args)
23
+ end
24
+ end
25
+
26
+ def initialize(jotify_impl=Java::DeFelixbrunsJotify::JotifyPool.new(4))
27
+ @jotify = jotify_impl
28
+
29
+ credentials = Jotify.credentials
30
+ @jotify.login(credentials[:username], credentials[:password])
31
+
32
+ at_exit do
33
+ begin
34
+ @jotify.close
35
+ rescue Exception => e
36
+ end
37
+ end
38
+
39
+ if block_given?
40
+ begin
41
+ yield self
42
+ ensure
43
+ close
44
+ end
45
+ end
46
+ end
47
+
48
+ def playlists
49
+ @jotify.playlists.map { |p| playlist(p.getId()) }
50
+ end
51
+
52
+ def playlist(id)
53
+ playlist = @jotify.playlist(Jotify.resolve_id(id))
54
+ unless playlist.tracks.empty?
55
+ res = @jotify.browse(playlist.tracks)
56
+ res.tracks.each_with_index do |t,i|
57
+ playlist.tracks.set(i, t)
58
+ end
59
+ end
60
+ playlist
61
+ end
62
+
63
+ def create_playlist(name, collaborative=false)
64
+ playlist = @jotify.playlistCreate(name, collaborative)
65
+ return nil unless playlist
66
+ add_playlist(playlist)
67
+ playlist
68
+ end
69
+
70
+ def add_playlist(id)
71
+ @jotify.playlistsAddPlaylist(@jotify.playlists, id.is_a?(Media::Playlist) ? id : playlist(id))
72
+ end
73
+
74
+ def add_tracks_to_playlist(playlist, track_ids)
75
+ tracks = Java::JavaUtil::ArrayList.new
76
+ track_ids.each { |id| tracks.add(Media::Track.new(Jotify.resolve_id(id))) }
77
+ @jotify.playlistAddTracks(playlist, tracks, 0)
78
+ end
79
+
80
+ def self.resolve_id(id)
81
+ case id
82
+ when /\Ahttp:\/\/open\.spotify\.com/: SpotifyURI.to_hex(id[id.rindex('/')+1..-1])
83
+ when /spotify:/: SpotifyURI.to_hex(id[id.rindex(':')+1..-1])
84
+ when /\A[0-9a-f]{32}\Z/: id
85
+ when /\A[a-zA-Z0-9]{22}\Z/: SpotifyURI.to_hex(id)
86
+ else
87
+ raise "invalid id: #{id}"
88
+ end
89
+ end
90
+
91
+ def self.credentials
92
+ prefs = JotifyPreferences.getInstance()
93
+ prefs.load()
94
+ {
95
+ :username => prefs.getString("login.username"),
96
+ :password => prefs.getString("login.password")
97
+ }
98
+ end
99
+
100
+ def self.credentials=(creds)
101
+ prefs = JotifyPreferences.getInstance()
102
+ prefs.load()
103
+ prefs.setString("login.username", creds[:username])
104
+ prefs.setString("login.password", creds[:password])
105
+ prefs.save() or raise "could not save login details"
106
+ end
107
+ end
108
+
109
+ require File.expand_path(File.dirname(__FILE__) + '/jotify/media')
@@ -0,0 +1,114 @@
1
+ #!/usr/bin/env jruby
2
+ require 'rubygems'
3
+ require 'json'
4
+ require 'sinatra/base'
5
+ require File.expand_path(File.dirname(__FILE__) + '/../jotify') unless defined?(Jotify)
6
+
7
+ class Sinatra::Application
8
+ Lock = Mutex.new
9
+
10
+ def jotify
11
+ Lock.synchronize do
12
+ @@jotify ||= Jotify.new
13
+ end
14
+ end
15
+
16
+ def query(what, p=what)
17
+ params[p] ? "#{what}:" + params[p] : ''
18
+ end
19
+ end
20
+
21
+ Sinatra::Application.error ArgumentError do
22
+ content_type :json
23
+ {
24
+ 'status' => 'ERROR',
25
+ 'message' => request.env['sinatra.error'].message
26
+ }.to_json
27
+ end
28
+
29
+ Sinatra::Application.get('/tracks') do
30
+ content_type :json
31
+ raise ArgumentError, "need name" unless params[:name]
32
+
33
+ res = jotify.search([query(:track, :name), query(:artist), query(:album)].join(' '))
34
+ {
35
+ 'status'=>'OK',
36
+ 'result'=> res.tracks.map { |t| t.to_h }
37
+ }.to_json
38
+ end
39
+
40
+ Sinatra::Application.get('/albums') do
41
+ content_type :json
42
+ raise ArgumentError, "need name" unless params[:name]
43
+
44
+ res = jotify.search([query(:album, :name), query(:artist)].join(' ') )
45
+ {
46
+ 'status'=>'OK',
47
+ 'result'=> res.albums.map { |a| a.to_h }
48
+ }.to_json
49
+ end
50
+
51
+ Sinatra::Application.get('/artists') do
52
+ content_type :json
53
+ raise ArgumentError, "need name" unless params[:name]
54
+
55
+ res = jotify.search(query(:artist, :name))
56
+ {
57
+ 'status'=>'OK',
58
+ 'result'=> res.artists.map { |a| a.to_h }
59
+ }.to_json
60
+ end
61
+
62
+ Sinatra::Application.get('/playlists') do
63
+ content_type :json
64
+ #playlists = jotify.playlists
65
+ {
66
+ 'status'=>'OK',
67
+ 'result'=> { 'playlists' => jotify.playlists.map { |p| p.to_h } }
68
+ }.to_json
69
+ end
70
+
71
+ Sinatra::Application.get('/playlists/:id') do
72
+ content_type :json
73
+ if playlist = jotify.playlist(params[:id])
74
+ {
75
+ 'status'=>'OK',
76
+ 'result'=>playlist.to_h
77
+ }.to_json
78
+ else
79
+ return 404, { 'status' => 'ERROR', 'message' => 'playlist not found' }.to_json
80
+ end
81
+ end
82
+
83
+ Sinatra::Application.post('/playlists') do
84
+ content_type :json
85
+ name, collaborative = params[:name], params[:collaborative] == 'true'
86
+ playlist = jotify.create_playlist(name, collaborative)
87
+ if playlist
88
+ redirect playlist.link, 201 # created
89
+ else
90
+ return 500, { 'status' => 'ERROR', 'message' => 'playlist could not be created' }.to_json
91
+ end
92
+ end
93
+
94
+ Sinatra::Application.put('/playlists/:id') do
95
+ content_type :json
96
+ playlist = jotify.playlist(params[:id])
97
+ return 404, { 'status' => 'ERROR', 'message' => 'playlist not found' }.to_json unless playlist
98
+ body = request.body.read
99
+ data = JSON.parse(body)
100
+ raise ArgumentError, "invalid format" unless data.is_a?(Hash) && data['tracks'].is_a?(Array)
101
+ ids = data['tracks'].map { |t| t['id'] }
102
+ return 200, { 'status' => 'OK', 'message' => 'not modified' }.to_json if ids.empty?
103
+
104
+ if jotify.add_tracks_to_playlist(playlist, ids)
105
+ return 200, { 'status' => 'OK', 'message' => "successfully added #{ids.size} tracks" }.to_json
106
+ else
107
+ return 500, { 'status' => 'ERROR', 'message' => 'could not add to playlist' }.to_json
108
+ end
109
+ end
110
+
111
+
112
+ if __FILE__ == $0
113
+ Sinatra::Application.run!
114
+ end
@@ -0,0 +1,99 @@
1
+ require 'java'
2
+ require File.expand_path(File.dirname(__FILE__) + '/../jars/jotify.jar')
3
+
4
+ #Make Jotify's native Java classes a bit more rubyish by implementing some mehods / modules
5
+ module Java
6
+ module DeFelixbrunsJotifyMedia
7
+ class Media
8
+ def inspect
9
+ self.to_s
10
+ end
11
+
12
+ def to_h
13
+ h = { :id=>self.getId(), :popularity=> popularity.nan? ? 0.0 : popularity.to_f }
14
+ h[:url] = self.link if self.respond_to?(:link)
15
+ h
16
+ end
17
+ end
18
+
19
+ class Playlist
20
+ include Enumerable
21
+
22
+ def each(&block)
23
+ tracks.each(&block)
24
+ end
25
+
26
+ def inspect
27
+ "[Playlist: #{self.getId()} #{getTracks.to_a}]"
28
+ end
29
+
30
+ def to_h
31
+ {
32
+ :id => getId(),
33
+ :name=> name,
34
+ :url => link,
35
+ :tracks => tracks.map { |t| t.to_h },
36
+ :author => author,
37
+ :revision => revision,
38
+ :collaborative => collaborative
39
+ }
40
+ end
41
+ end
42
+
43
+ class PlaylistContainer
44
+ include Enumerable
45
+
46
+ def each(&block)
47
+ playlists.each(&block)
48
+ end
49
+ end
50
+
51
+ class Result
52
+ include Enumerable
53
+
54
+ def each(&block)
55
+ artists.each(&block)
56
+ tracks.each(&block)
57
+ albums.each(&block)
58
+ end
59
+
60
+ def inspect
61
+ { :artists=>self.artists.to_a, :albums=>self.albums.to_a, :tracks=>self.tracks.to_a }.inspect
62
+ end
63
+
64
+ def to_h
65
+ {
66
+ :artists => self.artists.map { |a| a.to_h },
67
+ :albums => self.albums.map { |a| a.to_h },
68
+ :tracks => self.tracks.map { |t| t.to_h }
69
+ }
70
+ end
71
+ end
72
+
73
+ class Track
74
+ def to_h
75
+ super.merge(:title=>title, :artist=>artist ? artist.name : nil, :album=>album ? album.name : nil)
76
+ end
77
+ end
78
+
79
+ class Artist
80
+ def to_h
81
+ super.merge(:name=>name)
82
+ end
83
+ end
84
+
85
+ class Album
86
+ include Enumerable
87
+
88
+ def each(&block)
89
+ tracks.each(&block)
90
+ end
91
+
92
+ def to_h
93
+ super.merge(:name=>name, :artist=>artist ? artist.name : nil, :year=>year, :type=>type)
94
+ end
95
+ end
96
+ end
97
+ end
98
+
99
+
@@ -0,0 +1,179 @@
1
+ #!/usr/bin/env jruby -S spec
2
+ require File.join(File.dirname(__FILE__), 'spec_helper')
3
+
4
+ require 'jotify/api'
5
+ require 'rack/test'
6
+ require 'sinatra'
7
+
8
+ set :environment, :test
9
+
10
+ describe 'Api' do
11
+
12
+ def app() Sinatra::Application end
13
+
14
+ def json_response
15
+ last_response.content_type.should == 'application/json'
16
+ JSON.parse(last_response.body)
17
+ end
18
+
19
+
20
+ before do
21
+ @jotify = jotify = mock('Jotify')
22
+ Sinatra::Application.send(:define_method, :jotify) { jotify }
23
+ end
24
+
25
+ describe "/artists" do
26
+ it "searches by artist name" do
27
+ res = Jotify::Media::Result.new
28
+ res.artists.add(Jotify::Media::Artist.new('4d921ebcdd8c80f32ce1ed5acafbb9c8', 'The Kinks'))
29
+ @jotify.stub!(:search).and_return(res)
30
+ get '/artists', :name=>'The Kinks'
31
+ last_response.should be_ok
32
+ json_response.should == {"status"=>"OK", "result"=>
33
+ [{"id"=>"4d921ebcdd8c80f32ce1ed5acafbb9c8", "popularity"=>0.0, "url"=>"http://open.spotify.com/artist/2mnbxTkghYtlHMdX3jdP9C", "name"=>"The Kinks"}]
34
+ }
35
+ end
36
+
37
+ it "should return an error message if no name is specified" do
38
+ lambda { get '/artists' }.should raise_error(ArgumentError)
39
+ end
40
+ end
41
+
42
+ describe "/tracks" do
43
+ it "searches by track name" do
44
+ res = Jotify::Media::Result.new
45
+ res.tracks.add(Jotify::Media::Track.new('4d921ebcdd8c80f32ce1ed5acafbb9c8'))
46
+ @jotify.stub!(:search).and_return(res)
47
+ get '/tracks', :name=>'Waterloo Sunset'
48
+ last_response.should be_ok
49
+ json_response.should == {"status"=>"OK", "result"=>
50
+ [{"id"=>"4d921ebcdd8c80f32ce1ed5acafbb9c8", "popularity"=>0.0, "url"=>"http://open.spotify.com/track/2mnbxTkghYtlHMdX3jdP9C", "title"=>nil, "artist"=>nil, "album"=>nil}]
51
+ }
52
+ end
53
+
54
+ it "should return an error message if no name is specified" do
55
+ lambda { get '/tracks' }.should raise_error(ArgumentError)
56
+ end
57
+ end
58
+
59
+ describe "/albums" do
60
+ it "searches by album name" do
61
+ res = Jotify::Media::Result.new
62
+ res.albums.add(Jotify::Media::Album.new('4d921ebcdd8c80f32ce1ed5acafbb9c8',
63
+ 'Something Else',
64
+ Jotify::Media::Artist.new('4d921ebcdd8c80f32ce1ed5acafbb9c8', 'The Kinks')))
65
+
66
+ @jotify.stub!(:search).and_return(res)
67
+ get '/albums', :name=>'Something Else'
68
+ last_response.should be_ok
69
+ json_response.should == {"status"=>"OK", "result"=>
70
+ [{"id"=>"4d921ebcdd8c80f32ce1ed5acafbb9c8",
71
+ "popularity"=>0.0,
72
+ "url"=>"http://open.spotify.com/album/2mnbxTkghYtlHMdX3jdP9C",
73
+ "name"=>"Something Else",
74
+ "artist"=>"The Kinks", "year"=>-1, "type"=>nil}]
75
+ }
76
+ end
77
+
78
+ it "should return an error message if no name is specified" do
79
+ lambda { get '/albums' }.should raise_error(ArgumentError)
80
+ end
81
+ end
82
+
83
+ describe "/playlists" do
84
+ before do
85
+ #String id, String name, String author, boolean collaborative
86
+ @playlist = Jotify::Media::Playlist.new("4d921ebcdd8c80f32ce1ed5acafbb9c8", "my shiny playlist", "test", false)
87
+ end
88
+
89
+ describe "get" do
90
+
91
+ it "should get all playlists created by the user" do
92
+ @jotify.should_receive(:playlists).and_return( [@playlist] )
93
+ get '/playlists'
94
+ last_response.should be_ok
95
+ json_response.should == {"status"=>"OK",
96
+ "result"=> {
97
+ "playlists" => [
98
+ "id"=>"4d921ebcdd8c80f32ce1ed5acafbb9c8",
99
+ "url"=>"http://open.spotify.com/user/test/playlist/2mnbxTkghYtlHMdX3jdP9C",
100
+ "name"=>"my shiny playlist",
101
+ "tracks"=>[], "author"=>"test", "revision"=>-1, "collaborative"=>false
102
+ ]
103
+ }
104
+ }
105
+ end
106
+
107
+
108
+ it "should retrieve a playlist by id" do
109
+ @jotify.should_receive(:playlist).with("4d921ebcdd8c80f32ce1ed5acafbb9c8").and_return(@playlist)
110
+ get '/playlists/4d921ebcdd8c80f32ce1ed5acafbb9c8'
111
+ last_response.should be_ok
112
+ json_response.should == {"status"=>"OK",
113
+ "result"=>{
114
+ "id"=>"4d921ebcdd8c80f32ce1ed5acafbb9c8",
115
+ "url"=>"http://open.spotify.com/user/test/playlist/2mnbxTkghYtlHMdX3jdP9C",
116
+ "name"=>"my shiny playlist",
117
+ "tracks"=>[], "author"=>"test", "revision"=>-1, "collaborative"=>false}
118
+ }
119
+ end
120
+
121
+ it "should except a base62 string as id" do
122
+ @jotify.should_receive(:playlist).with("2mnbxTkghYtlHMdX3jdP9C").and_return(@playlist)
123
+ get '/playlists/2mnbxTkghYtlHMdX3jdP9C'
124
+ last_response.should be_ok
125
+ end
126
+
127
+ it "should return 404 for non-existing playlist" do
128
+ @jotify.should_receive(:playlist).with("unknown").and_return(nil)
129
+ get '/playlists/unknown'
130
+ last_response.status.should == 404
131
+ json_response.should == {"status"=>"ERROR", "message"=>"playlist not found"}
132
+ end
133
+ end
134
+
135
+
136
+ describe "create/update" do
137
+ it "should create a playlist when posting to /playlists" do
138
+ @jotify.should_receive(:create_playlist).with('my shiny playlist', true).and_return(@playlist)
139
+ post '/playlists', :name => 'my shiny playlist', :collaborative => 'true'
140
+ last_response.status.should == 201
141
+ last_response.headers['Location'].should == 'http://open.spotify.com/user/test/playlist/2mnbxTkghYtlHMdX3jdP9C'
142
+ end
143
+
144
+ it "should update playlist when putting to /playlists/id" do
145
+ @jotify.should_receive(:playlist).with("foo").and_return(@playlist)
146
+ @jotify.should_receive(:add_tracks_to_playlist).with(@playlist, ['1','2']).and_return(true)
147
+ put '/playlists/foo', { 'tracks' => [ {'id'=>'1' }, { 'id'=>'2' } ] }.to_json
148
+ last_response.should be_ok
149
+ json_response.should == {'status'=>'OK', 'message'=>'successfully added 2 tracks'}
150
+ end
151
+
152
+ it "should return 404 if playlist to update cannot be found" do
153
+ @jotify.should_receive(:playlist).with("foo").and_return(nil)
154
+ put '/playlists/foo', { 'tracks' => [ {'id'=>'1' }, { 'id'=>'2' } ] }.to_json
155
+ last_response.status.should == 404
156
+ json_response.should == {"status"=>"ERROR", "message"=>"playlist not found"}
157
+ end
158
+
159
+ it "should return 500 if playlist could not be updated" do
160
+ @jotify.should_receive(:playlist).with("foo").and_return(@playlist)
161
+ @jotify.should_receive(:add_tracks_to_playlist).with(@playlist, ['1','2']).and_return(false)
162
+ put '/playlists/foo', { 'tracks' => [ {'id'=>'1' }, { 'id'=>'2' } ] }.to_json
163
+ last_response.status.should == 500
164
+ json_response.should == {"status"=>"ERROR", "message"=>"could not add to playlist"}
165
+ end
166
+
167
+ it "should return 403 if invalid data is supplied" do
168
+ @jotify.should_receive(:playlist).with("foo").and_return(@playlist)
169
+ lambda { put '/playlists/foo', { 'foo' => 'bar' }.to_json }.should raise_error(ArgumentError)
170
+ end
171
+
172
+ it "shouldn't do anything if no track ids supplied" do
173
+ @jotify.should_receive(:playlist).with("foo").and_return(@playlist)
174
+ put '/playlists/foo', { 'tracks' => [] }.to_json
175
+ last_response.should be_ok
176
+ end
177
+ end
178
+ end
179
+ end
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env jruby -S spec
2
+ require File.join(File.dirname(__FILE__), 'spec_helper')
3
+ require 'jotify'
4
+
5
+ # WARNING: these specs need a valid spotify (premium) account, otherwise they will fail
6
+ describe "Integration" do
7
+
8
+ before(:all) do
9
+ @jotify = Jotify.new
10
+ end
11
+
12
+ after(:all) do
13
+ @jotify.close rescue nil
14
+ end
15
+
16
+ describe "searching" do
17
+ it "should return a result for a search" do
18
+ result = @jotify.search("artist:Air")
19
+ result.should be_a(Jotify::Media::Result)
20
+
21
+ result.artists.should_not be_empty
22
+ most_popular = result.artists.to_a.sort(&Jotify::ByPopularity).first
23
+ most_popular.name.should == 'Air'
24
+
25
+ result = @jotify.search("album:Moon Safari")
26
+ result.albums.should_not be_empty
27
+ most_popular = result.albums.to_a.sort(&Jotify::ByPopularity).first
28
+ most_popular.name.should == 'Moon Safari'
29
+ most_popular.artist.name.should == 'Air'
30
+ end
31
+ end
32
+
33
+ end
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env jruby -S spec
2
+ require File.join(File.dirname(__FILE__), 'spec_helper')
3
+ require 'jotify'
4
+
5
+ describe Jotify do
6
+
7
+ before(:each) do
8
+ @jotify_impl = mock('JotifyImpl')
9
+ @jotify_impl.stub!(:login)
10
+ @jotify = Jotify.new(@jotify_impl)
11
+ end
12
+
13
+ it "should resolve ids" do
14
+ { "spotify:user:flumix:playlist:2mnbxTkghYtlHMdX3jdP9C" => "4d921ebcdd8c80f32ce1ed5acafbb9c8",
15
+ "http://open.spotify.com/user/flumix/playlist/2mnbxTkghYtlHMdX3jdP9C" => "4d921ebcdd8c80f32ce1ed5acafbb9c8",
16
+ "2mnbxTkghYtlHMdX3jdP9C" => "4d921ebcdd8c80f32ce1ed5acafbb9c8",
17
+ "4d921ebcdd8c80f32ce1ed5acafbb9c8" => "4d921ebcdd8c80f32ce1ed5acafbb9c8"
18
+ }.each { |id, expected| Jotify.resolve_id(id).should == expected }
19
+ end
20
+
21
+ it "should add tracks to playlist" do
22
+ @playlist = Jotify::Media::Playlist.new
23
+ @jotify_impl.should_receive(:playlistAddTracks) do |playlist, tracks, pos|
24
+ playlist.should be_a(Jotify::Media::Playlist)
25
+ #playlist.should == @playlist
26
+ pos.should == 0
27
+ tracks.should be_an(Java::JavaUtil::List)
28
+ tracks.size.should == 1
29
+ end
30
+ @jotify.add_tracks_to_playlist(@playlist, ['4d921ebcdd8c80f32ce1ed5acafbb9c8'])
31
+ end
32
+ end
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env jruby -S spec
2
+ require File.join(File.dirname(__FILE__), 'spec_helper')
3
+ require 'jotify'
4
+
5
+ describe Jotify::Media do
6
+
7
+ describe "an artist" do
8
+ before do
9
+ @artist = Jotify::Media::Artist.new
10
+ end
11
+
12
+ it "should implement to_h" do
13
+ @artist.respond_to?(:to_h).should be_true
14
+ end
15
+ end
16
+
17
+ describe "a track" do
18
+ before do
19
+ @track = Jotify::Media::Track.new
20
+ end
21
+
22
+ it "should implement to_h" do
23
+ @track.respond_to?(:to_h).should be_true
24
+ end
25
+ end
26
+
27
+ describe "an album" do
28
+ before do
29
+ @album = Jotify::Media::Album.new
30
+ end
31
+
32
+ it "should implement to_h" do
33
+ @album.respond_to?(:to_h).should be_true
34
+ end
35
+
36
+ it "should implement enumerable" do
37
+ @album.class.included_modules.should include(Enumerable)
38
+ end
39
+ end
40
+
41
+ describe "a result" do
42
+ before do
43
+ @result = Jotify::Media::Result.new
44
+ end
45
+
46
+ it "should implement to_h" do
47
+ @result.respond_to?(:to_h).should be_true
48
+ end
49
+
50
+ it "should implement enumerable" do
51
+ @result.class.included_modules.should include(Enumerable)
52
+ @result.respond_to?(:each).should be_true
53
+ end
54
+ end
55
+
56
+ describe "playlists" do
57
+ it "should implement enumerable" do
58
+ p = Jotify::Media::Playlist.new
59
+ p.class.included_modules.should include(Enumerable)
60
+ p.respond_to?(:each).should be_true
61
+ 10.times { p.tracks.add(Jotify::Media::Track.new) }
62
+ p.to_a.size.should == 10
63
+ end
64
+ end
65
+
66
+ describe "playlist container" do
67
+ before do
68
+ @container = Jotify::Media::PlaylistContainer.new
69
+ end
70
+
71
+ it "should implement enumerable" do
72
+ @container.respond_to?(:each).should be_true
73
+ @container.class.included_modules.should include(Enumerable)
74
+ end
75
+ end
76
+
77
+ end
@@ -0,0 +1,11 @@
1
+ require 'rubygems'
2
+ require 'spec'
3
+ require 'rack/test'
4
+
5
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
6
+
7
+ Spec::Runner.configure do |config|
8
+ config.include Rack::Test::Methods
9
+ end
10
+
11
+
@@ -0,0 +1,76 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = %q{spotify-api}
5
+ s.version = "0.0.1"
6
+
7
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
8
+ s.authors = ["Jan Berkel"]
9
+ s.date = %q{2009-08-04}
10
+ s.default_executable = %q{spotify-api-server}
11
+ s.description = %q{an api for spotify, based on jotify}
12
+ s.email = %q{jan.berkel@gmail.com}
13
+ s.executables = ["spotify-api-server"]
14
+ s.extra_rdoc_files = [
15
+ "LICENSE",
16
+ "README.md"
17
+ ]
18
+ s.files = [
19
+ ".gitignore",
20
+ "LICENSE",
21
+ "README.md",
22
+ "Rakefile",
23
+ "VERSION.yml",
24
+ "bin/spotify-api-server",
25
+ "examples/create_playlist.rb",
26
+ "examples/list_playlists.rb",
27
+ "examples/search.rb",
28
+ "lib/jars/jotify.jar",
29
+ "lib/jotify.rb",
30
+ "lib/jotify/api.rb",
31
+ "lib/jotify/media.rb",
32
+ "spec/api_spec.rb",
33
+ "spec/integration_spec.rb",
34
+ "spec/jotify_spec.rb",
35
+ "spec/media_spec.rb",
36
+ "spec/spec_helper.rb",
37
+ "spotify-api.gemspec"
38
+ ]
39
+ s.homepage = %q{http://github.com/jberkel/spotify-api}
40
+ s.rdoc_options = ["--charset=UTF-8"]
41
+ s.require_paths = ["lib"]
42
+ s.rubygems_version = %q{1.3.5}
43
+ s.summary = %q{an api for spotify, based on jotify}
44
+ s.test_files = [
45
+ "spec/api_spec.rb",
46
+ "spec/integration_spec.rb",
47
+ "spec/jotify_spec.rb",
48
+ "spec/media_spec.rb",
49
+ "spec/spec_helper.rb",
50
+ "examples/create_playlist.rb",
51
+ "examples/list_playlists.rb",
52
+ "examples/search.rb"
53
+ ]
54
+
55
+ if s.respond_to? :specification_version then
56
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
57
+ s.specification_version = 3
58
+
59
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
60
+ s.add_runtime_dependency(%q<rack>, [">= 0"])
61
+ s.add_runtime_dependency(%q<rack-test>, [">= 0"])
62
+ s.add_runtime_dependency(%q<sinatra>, [">= 0"])
63
+ s.add_runtime_dependency(%q<json-jruby>, [">= 0"])
64
+ else
65
+ s.add_dependency(%q<rack>, [">= 0"])
66
+ s.add_dependency(%q<rack-test>, [">= 0"])
67
+ s.add_dependency(%q<sinatra>, [">= 0"])
68
+ s.add_dependency(%q<json-jruby>, [">= 0"])
69
+ end
70
+ else
71
+ s.add_dependency(%q<rack>, [">= 0"])
72
+ s.add_dependency(%q<rack-test>, [">= 0"])
73
+ s.add_dependency(%q<sinatra>, [">= 0"])
74
+ s.add_dependency(%q<json-jruby>, [">= 0"])
75
+ end
76
+ end
metadata ADDED
@@ -0,0 +1,119 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: jberkel-spotify-api
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Jan Berkel
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-08-04 00:00:00 -07:00
13
+ default_executable: spotify-api-server
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: rack
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: "0"
24
+ version:
25
+ - !ruby/object:Gem::Dependency
26
+ name: rack-test
27
+ type: :runtime
28
+ version_requirement:
29
+ version_requirements: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: "0"
34
+ version:
35
+ - !ruby/object:Gem::Dependency
36
+ name: sinatra
37
+ type: :runtime
38
+ version_requirement:
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: "0"
44
+ version:
45
+ - !ruby/object:Gem::Dependency
46
+ name: json-jruby
47
+ type: :runtime
48
+ version_requirement:
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: "0"
54
+ version:
55
+ description: an api for spotify, based on jotify
56
+ email: jan.berkel@gmail.com
57
+ executables:
58
+ - spotify-api-server
59
+ extensions: []
60
+
61
+ extra_rdoc_files:
62
+ - LICENSE
63
+ - README.md
64
+ files:
65
+ - .gitignore
66
+ - LICENSE
67
+ - README.md
68
+ - Rakefile
69
+ - VERSION.yml
70
+ - bin/spotify-api-server
71
+ - examples/create_playlist.rb
72
+ - examples/list_playlists.rb
73
+ - examples/search.rb
74
+ - lib/jars/jotify.jar
75
+ - lib/jotify.rb
76
+ - lib/jotify/api.rb
77
+ - lib/jotify/media.rb
78
+ - spec/api_spec.rb
79
+ - spec/integration_spec.rb
80
+ - spec/jotify_spec.rb
81
+ - spec/media_spec.rb
82
+ - spec/spec_helper.rb
83
+ - spotify-api.gemspec
84
+ has_rdoc: false
85
+ homepage: http://github.com/jberkel/spotify-api
86
+ licenses:
87
+ post_install_message:
88
+ rdoc_options:
89
+ - --charset=UTF-8
90
+ require_paths:
91
+ - lib
92
+ required_ruby_version: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: "0"
97
+ version:
98
+ required_rubygems_version: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: "0"
103
+ version:
104
+ requirements: []
105
+
106
+ rubyforge_project:
107
+ rubygems_version: 1.3.5
108
+ signing_key:
109
+ specification_version: 3
110
+ summary: an api for spotify, based on jotify
111
+ test_files:
112
+ - spec/api_spec.rb
113
+ - spec/integration_spec.rb
114
+ - spec/jotify_spec.rb
115
+ - spec/media_spec.rb
116
+ - spec/spec_helper.rb
117
+ - examples/create_playlist.rb
118
+ - examples/list_playlists.rb
119
+ - examples/search.rb