spotify-api 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,3 @@
1
+ pkg
2
+ coverage
3
+ .DS_Store
@@ -0,0 +1,12 @@
1
+ == 0.0.4 / 2009-08-08
2
+ * Fixes
3
+
4
+ == 0.0.3 / 2009-08-07
5
+ * POST /playlists uses json
6
+
7
+ == 0.0.2 / 2009-08-05
8
+ * Fix option parser bug
9
+
10
+ == 0.0.1 / 2009-08-04
11
+ * First release to github
12
+
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,74 @@
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
+ * update playlists [PUT /playlists/id]
18
+
19
+ ## Installation
20
+
21
+ Prerequisites: *Java 6+*, JRuby 1.3.x.
22
+
23
+ $ jruby -S gem sources -a http://gems.github.com # (you only have to do this once)
24
+ $ jruby -S gem install jberkel-spotify-api
25
+ $ jruby -S spotify-api-server --account login:password
26
+ == Sinatra/0.9.4 has taken the stage on 3000 for development with backup from WEBrick
27
+ [2009-08-04 01:21:03] INFO WEBrick 1.3.1
28
+ [2009-08-04 01:21:03] INFO ruby 1.8.6 (2009-07-24) [java]
29
+ [2009-08-04 01:21:03] INFO WEBrick::HTTPServer#start: pid=12162 port=3000
30
+
31
+ $ curl http://localhost:3000/playlists | jsonpretty
32
+ {
33
+ "result": {
34
+ "playlists": [
35
+ {
36
+ "name": "my shiny playlist",
37
+ "author": "jberkel",
38
+ "url": "http:\/\/open.spotify.com\/user\/jberkel\/playlist\/5EXLGE7HPVPjvlxPmIfrDe",
39
+ "revision": 2,
40
+ "id": "b9fe3dcf88945d146ef18117faa61ab4",
41
+ "collaborative": false
42
+ }
43
+ ]
44
+ },
45
+ "status": "OK"
46
+ }
47
+
48
+ See examples directory for usage.
49
+
50
+ ## Credits
51
+
52
+ Contains code from the jotify project:
53
+
54
+ Copyright (c) 2009, Felix Bruns <felixbruns@web.de>
55
+ All rights reserved.
56
+
57
+ Redistribution and use in source and binary forms, with or without
58
+ modification, are permitted provided that the following conditions are met:
59
+ * Redistributions of source code must retain the above copyright
60
+ notice, this list of conditions and the following disclaimer.
61
+ * Redistributions in binary form must reproduce the above copyright
62
+ notice, this list of conditions and the following disclaimer in the
63
+ documentation and/or other materials provided with the distribution.
64
+
65
+ THIS SOFTWARE IS PROVIDED BY THE AUTHORS ''AS IS'' AND ANY
66
+ EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
67
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
68
+ DISCLAIMED. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY
69
+ DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
70
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
71
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
72
+ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
73
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
74
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,41 @@
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
+ desc "runs specs with rcov"
30
+ Spec::Rake::SpecTask.new('spec:coverage') do |t|
31
+ t.rcov = true
32
+ t.spec_files = FileList["spec/**/*_spec.rb"].delete_if { |f| f =~ /integration/ }
33
+ t.libs << "./lib"
34
+ t.rcov_opts = ['--exclude', 'spec/.*rb,\(__.+__\)']
35
+ end
36
+
37
+ Spec::Rake::SpecTask.new(:integration) do |t|
38
+ t.rcov = false
39
+ t.spec_files = FileList["spec/**/*_spec.rb"].select { |f| f =~ /integration/ }
40
+ t.libs << "./lib"
41
+ end
@@ -0,0 +1,4 @@
1
+ ---
2
+ :patch: 5
3
+ :major: 0
4
+ :minor: 0
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env jruby #--jdb -J-sourcepath -J/Users/jan/projects/jotify/src
2
+ $LOAD_PATH.unshift(File.expand_path(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
+ while arg = ARGV.shift do
16
+ case arg
17
+ when '-p', '--port': port = ARGV.shift
18
+ when '--account':
19
+ login, password = ARGV.shift.to_s.split(':')
20
+ raise ArgumentError, "you need to specify both login and password!" unless login and password
21
+ Jotify.credentials = { :username=>login, :password=>password }
22
+ when '-h', '--help': usage
23
+ end
24
+ end
25
+
26
+ # Taken mostly from
27
+ # http://groups.google.com/group/sinatrarb/t/a5cfc2b77a013a86
28
+ class Sinatra::Reloader < Rack::Reloader
29
+ def safe_load(file, mtime, stderr = $stderr)
30
+ # ::Sinatra::Application.reset!
31
+ # stderr.puts "#{self.class}: reseting routes"
32
+ super
33
+ end
34
+ end
35
+
36
+ #Sinatra::Application.set :environment, :production
37
+ Sinatra::Application.configure(:development) do |app|
38
+ #app.use Sinatra::Reloader
39
+ end
40
+ Sinatra::Application.run! :port=> (port || DEFAULT_PORT).to_i
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require 'httparty'
5
+
6
+ #A demo Last.fm API client, implemented
7
+ #using httparty (http://github.com/jnunemaker/httparty/)
8
+ class Lastfm
9
+ include HTTParty
10
+
11
+ base_uri 'ws.audioscrobbler.com'
12
+ default_params :api_key => "PUT_API_KEY_HERE"
13
+
14
+ class <<self
15
+ def loved_tracks(user_id)
16
+ query('user.getLovedTracks', :user=>user_id, :limit=>10)['lovedtracks']['track'].map do |r|
17
+ { 'artist' => r['artist']['name'], 'title'=>r['name'], 'mbid' => r['mbid'] }
18
+ end
19
+ end
20
+
21
+ def recent_tracks(user_id)
22
+ query('user.getRecentTracks', :user=>user_id, :limit=>100)['recenttracks']['track'].map do |r|
23
+ { 'artist' => r['artist'], 'title'=>r['name'], 'mbid' => r['mbid'] }
24
+ end
25
+ end
26
+
27
+ def top_tracks(user_id, period='overall')
28
+ unless ['overall', '7day', '3month', '6month', '12month'].include?(period)
29
+ raise ArgumentError, "invalid period"
30
+ end
31
+
32
+ query('user.getTopTracks', :period=>period, :user=>user_id)['toptracks']['track'].map do |r|
33
+ { 'artist' => r['artist']['name'], 'title'=>r['name'], 'mbid' => r['mbid'] }
34
+ end
35
+ end
36
+
37
+ def query(method, args={})
38
+ result = get("/2.0/", :query => { :method => method }.merge(args))
39
+ raise result['lfm']['error'] if result['lfm'] && result['lfm']['status'] == 'failed'
40
+ result['lfm']
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require File.join(File.dirname(__FILE__), *%w[lastfm])
4
+ require File.join(File.dirname(__FILE__), *%w[spotify])
5
+
6
+ # a demo app which grabs tracks from last.fm and creates a spotify
7
+ # playlist
8
+ if __FILE__ == $0
9
+ username = ARGV.shift or raise "#{$0} <username> [period=overall|7day|3month|6month|12month]"
10
+ period = ARGV.shift || '7day'
11
+
12
+ puts "fetching last.fm tracks (period=#{period})"
13
+ tracks = Lastfm.top_tracks(username, period).map do |track|
14
+ Spotify.tracks(track["title"], track["artist"]).first
15
+ end.flatten.compact
16
+
17
+ #puts "found tracks: #{tracks.inspect}"
18
+ puts "creating playlist with #{tracks.size} tracks"
19
+ puts Spotify.create_playlist(username, tracks.map { |t| t['id'] })
20
+ end
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require 'httparty'
5
+ require 'json'
6
+ require 'pp'
7
+
8
+ #A demo spotify API client, implemented
9
+ #using httparty (http://github.com/jnunemaker/httparty/)
10
+ class Spotify
11
+ include HTTParty
12
+
13
+ base_uri 'localhost:3000'
14
+
15
+ def self.get_or_bail(path, params={})
16
+ resp = get(path, :query=>params)
17
+ raise resp.inspect if resp["status"] != "OK"
18
+ resp["result"]
19
+ end
20
+
21
+ def self.artists(name)
22
+ get_or_bail("/artists", {:name => name})
23
+ end
24
+
25
+ def self.tracks(name, artist=nil)
26
+ q = { :name => name }
27
+ q.merge!(:artist=>artist) if artist
28
+ get_or_bail("/tracks", q)
29
+ end
30
+
31
+ def self.albums(name, artist=nil)
32
+ q = { :name => name }
33
+ q.merge!(:artist=>artist) if artist
34
+ get_or_bail("/albums", q)
35
+ end
36
+
37
+ def self.playlist(id)
38
+ get_or_bail("/playlists", :id=>id)
39
+ end
40
+
41
+ def self.playlists
42
+ get_or_bail("/playlists")
43
+ end
44
+
45
+ def self.update_playlist(id, name=nil, track_ids=[])
46
+ data = {}
47
+ data["tracks"] = track_ids.map { |id| { 'id' => id } } unless tracks_ids.empty?
48
+ data["name"] = name if name
49
+
50
+ resp = put("/playlists/#{id}", :body => data.to_json)
51
+ raise resp.inspect if resp['status'] != 'OK'
52
+ end
53
+
54
+ def self.create_playlist(name, track_ids=[])
55
+ resp = post("/playlists", :body => {
56
+ :name => name,
57
+ :tracks => track_ids.map { |id| { 'id' => id } }
58
+ }.to_json)
59
+
60
+ if resp.code == 201
61
+ location = resp.headers['location']
62
+ "201 created (#{location})"
63
+ else
64
+ raise resp.inspect
65
+ end
66
+ end
67
+ end
68
+
69
+ if __FILE__ == $0
70
+ if ARGV.empty?
71
+ STDERR.puts "#{$0} [list|create|search]"
72
+ exit(1)
73
+ end
74
+
75
+ pp case cmd = ARGV.shift
76
+ when "list"
77
+ Spotify.playlists
78
+ when "create"
79
+ raise ArgumentError, "#{$0} create <name>" unless name = ARGV.shift
80
+ Spotify.create_playlist(name)
81
+ when "search"
82
+ raise ArgumentError, "#{$0} search <what>" unless what = ARGV.shift
83
+ Spotify.albums(what)
84
+ else raise ArgumentError, "invalid command: #{cmd}"
85
+ end
86
+ end
Binary file
@@ -0,0 +1,128 @@
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, resolve_tracks=false)
53
+ playlist = @jotify.playlist(Jotify.resolve_id(id))
54
+ if resolve_tracks && !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
+ raise ArgumentError, "need name" unless name
65
+
66
+ playlist = @jotify.playlistCreate(name, collaborative)
67
+ return nil unless playlist
68
+ add_playlist(playlist)
69
+ playlist
70
+ end
71
+
72
+ def add_playlist(id)
73
+ @jotify.playlistsAddPlaylist(@jotify.playlists, id.is_a?(Media::Playlist) ? id : playlist(id))
74
+ end
75
+
76
+ def rename_playlist(playlist, name)
77
+ @jotify.playlistRename(playlist, name)
78
+ end
79
+
80
+ def set_collaborative_flag(playlist, flag)
81
+ @jotify.playlistSetCollaborative(playlist, flag)
82
+ end
83
+
84
+ def set_tracks_on_playlist(playlist, track_ids)
85
+ #puts "playlist: checksum #{playlist.getChecksum()}"
86
+ tracks = Java::JavaUtil::ArrayList.new
87
+ track_ids.each { |id| tracks.add(Media::Track.new(Jotify.resolve_id(id))) }
88
+
89
+ # delete old tracks
90
+ if playlist.tracks.size > 0
91
+ raise "could not remove tracks" unless @jotify.playlistRemoveTracks(playlist, 0, playlist.tracks.size)
92
+ end
93
+
94
+ return true if track_ids.empty?
95
+
96
+ @jotify.playlistAddTracks(playlist, tracks, playlist.tracks.size)
97
+ end
98
+
99
+ def self.resolve_id(id)
100
+ case id
101
+ when /\Ahttp:\/\/open\.spotify\.com/: SpotifyURI.to_hex(id[id.rindex('/')+1..-1])
102
+ when /spotify:/: SpotifyURI.to_hex(id[id.rindex(':')+1..-1])
103
+ when /\A[0-9a-f]{32}\Z/: id
104
+ when /\A[a-zA-Z0-9]{22}\Z/: SpotifyURI.to_hex(id)
105
+ else
106
+ raise "invalid id: #{id}"
107
+ end
108
+ end
109
+
110
+ def self.credentials
111
+ prefs = JotifyPreferences.getInstance()
112
+ prefs.load()
113
+ {
114
+ :username => prefs.getString("login.username"),
115
+ :password => prefs.getString("login.password")
116
+ }
117
+ end
118
+
119
+ def self.credentials=(creds)
120
+ prefs = JotifyPreferences.getInstance()
121
+ prefs.load()
122
+ prefs.setString("login.username", creds[:username])
123
+ prefs.setString("login.password", creds[:password])
124
+ prefs.save() or raise "could not save login details"
125
+ end
126
+ end
127
+
128
+ require File.expand_path(File.dirname(__FILE__) + '/jotify/media')