jberkel-spotify-api 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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