spotify-api 0.0.5

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,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')