shortwave 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.
- data/.document +5 -0
- data/.gitignore +10 -0
- data/.gitmodules +3 -0
- data/LICENSE +20 -0
- data/README.rdoc +74 -0
- data/Rakefile +133 -0
- data/VERSION +1 -0
- data/lib/shortwave.rb +12 -0
- data/lib/shortwave/authentication.rb +107 -0
- data/lib/shortwave/facade.rb +3 -0
- data/lib/shortwave/facade/build/facade_builder.rb +199 -0
- data/lib/shortwave/facade/build/facade_template.erb +17 -0
- data/lib/shortwave/facade/lastfm.rb +878 -0
- data/lib/shortwave/facade/remote.rb +86 -0
- data/lib/shortwave/model/album.rb +33 -0
- data/lib/shortwave/model/artist.rb +62 -0
- data/lib/shortwave/model/base_model.rb +112 -0
- data/lib/shortwave/model/chart_dates.rb +15 -0
- data/lib/shortwave/model/comparison.rb +15 -0
- data/lib/shortwave/model/event.rb +55 -0
- data/lib/shortwave/model/group.rb +26 -0
- data/lib/shortwave/model/location.rb +45 -0
- data/lib/shortwave/model/playlist.rb +36 -0
- data/lib/shortwave/model/shout.rb +21 -0
- data/lib/shortwave/model/tag.rb +47 -0
- data/lib/shortwave/model/track.rb +71 -0
- data/lib/shortwave/model/user.rb +70 -0
- data/lib/shortwave/model/venue.rb +37 -0
- data/lib/shortwave/model/weekly_charts.rb +31 -0
- data/lib/shortwave/providers.rb +161 -0
- data/shortwave.gemspec +178 -0
- data/test/authentication_test.rb +64 -0
- data/test/build/build_test.rb +25 -0
- data/test/build/data/intro.yml +2 -0
- data/test/build/data/screens/album_addTags.html +1238 -0
- data/test/build/data/screens/intro_truncated.html +426 -0
- data/test/build/data/screens/tasteometer_compare.html +1274 -0
- data/test/build/data/screens/user_getLovedTracks.html +1278 -0
- data/test/build/data/screens/venue_search.html +1261 -0
- data/test/build/facade_builder_test.rb +23 -0
- data/test/build/parameter_test.rb +43 -0
- data/test/build/remote_method_test.rb +47 -0
- data/test/build/ruby_class_test.rb +12 -0
- data/test/build/ruby_method_test.rb +137 -0
- data/test/helper.rb +35 -0
- data/test/model/album_test.rb +62 -0
- data/test/model/artist_test.rb +103 -0
- data/test/model/chart_dates_test.rb +11 -0
- data/test/model/comparison_test.rb +18 -0
- data/test/model/data/album_info.xml +38 -0
- data/test/model/data/album_search.xml +210 -0
- data/test/model/data/artist_info.xml +58 -0
- data/test/model/data/artist_search.xml +109 -0
- data/test/model/data/artist_shouts.xml +546 -0
- data/test/model/data/artist_top_fans.xml +405 -0
- data/test/model/data/event_info.xml +47 -0
- data/test/model/data/group_members.xml +242 -0
- data/test/model/data/group_weekly_album_chart.xml +1754 -0
- data/test/model/data/group_weekly_artist_chart.xml +604 -0
- data/test/model/data/group_weekly_track_chart.xml +1005 -0
- data/test/model/data/location_events.xml +383 -0
- data/test/model/data/ok.xml +2 -0
- data/test/model/data/playlist_fetch.xml +227 -0
- data/test/model/data/tag_search.xml +110 -0
- data/test/model/data/tag_similar.xml +254 -0
- data/test/model/data/tag_top_albums.xml +805 -0
- data/test/model/data/tag_top_artists.xml +605 -0
- data/test/model/data/tag_top_tags.xml +1254 -0
- data/test/model/data/tag_top_tracks.xml +843 -0
- data/test/model/data/tag_weekly_chart_list.xml +57 -0
- data/test/model/data/tasteometer_compare.xml +54 -0
- data/test/model/data/track_info.xml +53 -0
- data/test/model/data/track_search.xml +195 -0
- data/test/model/data/user_chartlist.xml +90 -0
- data/test/model/data/user_info.xml +16 -0
- data/test/model/data/user_neighbours.xml +484 -0
- data/test/model/data/user_playlists.xml +17 -0
- data/test/model/data/user_recent_tracks.xml +124 -0
- data/test/model/data/user_recommended_artists.xml +454 -0
- data/test/model/data/user_shouts.xml +9 -0
- data/test/model/data/user_weekly_artist_chart.xml +478 -0
- data/test/model/data/venue_events.xml +556 -0
- data/test/model/data/venue_past_events.xml +1778 -0
- data/test/model/data/venue_search.xml +355 -0
- data/test/model/event_test.rb +63 -0
- data/test/model/group_test.rb +45 -0
- data/test/model/location_test.rb +25 -0
- data/test/model/playlist_test.rb +51 -0
- data/test/model/shout_test.rb +23 -0
- data/test/model/tag_test.rb +39 -0
- data/test/model/track_test.rb +67 -0
- data/test/model/user_test.rb +125 -0
- data/test/model/venue_test.rb +60 -0
- data/test/provider/album_provider_test.rb +26 -0
- data/test/provider/artist_provider_test.rb +25 -0
- data/test/provider/group_provider_test.rb +9 -0
- data/test/provider/location_provider_test.rb +9 -0
- data/test/provider/playlist_provider_test.rb +12 -0
- data/test/provider/tag_provider_test.rb +24 -0
- data/test/provider/track_provider_test.rb +26 -0
- data/test/provider/user_provider_test.rb +11 -0
- data/test/provider/venue_provider_test.rb +15 -0
- data/test/provider_test_helper.rb +27 -0
- data/test/remote_test.rb +26 -0
- metadata +209 -0
data/.document
ADDED
data/.gitignore
ADDED
data/.gitmodules
ADDED
data/LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Copyright (c) 2009 Roland Swingler
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
4
|
+
a copy of this software and associated documentation files (the
|
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
9
|
+
the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be
|
|
12
|
+
included in all copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
= Shortwave
|
|
2
|
+
|
|
3
|
+
Shortwave aims to be a complete API wrapper for the Last.fm web service.
|
|
4
|
+
|
|
5
|
+
Shortwave is split into two: the Facade, which provides a low-level thin wrapper around the
|
|
6
|
+
remote api, and the Model (which uses the Facade) which provides a more OO view of the api.
|
|
7
|
+
It would be possible to build your own api on top of the Facade if you don't like the Model
|
|
8
|
+
api provided - the two are fairly separate. See the docs for the Facade model if you want to
|
|
9
|
+
use it directly.
|
|
10
|
+
|
|
11
|
+
<b>Shortwave is currently pre-alpha. This means both the API and dependencies may change quite
|
|
12
|
+
a bit over forthcoming releases.</b>
|
|
13
|
+
|
|
14
|
+
== Advantages over other libraries.
|
|
15
|
+
|
|
16
|
+
* Shortwave aims to be complete. The facade is autogenerated from the Last.fm api documentation
|
|
17
|
+
so as the last.fm api evolves it should be possible to keep it in sync relatively easily
|
|
18
|
+
* No enforced opinions - the Facade is a very thin layer, so you can use it directly and treat
|
|
19
|
+
the returned XML as you please.
|
|
20
|
+
* Supports all 3 authentication types Last.fm provide.
|
|
21
|
+
* Doesn't rely on static/class properties for things like session keys. This means you can build
|
|
22
|
+
applications which have multiple users signed in.
|
|
23
|
+
|
|
24
|
+
== Examples
|
|
25
|
+
|
|
26
|
+
You need to create a session using one of the Authentication mechanisms:
|
|
27
|
+
|
|
28
|
+
session = Shortwave::Authentication::Mobile.new("your_api_key", "your_app_secret")
|
|
29
|
+
session.authenticate("user","password")
|
|
30
|
+
|
|
31
|
+
See the documentation for each of the Session types for how to authenticate for your
|
|
32
|
+
platform. Once you have a session, you can get model objects from it:
|
|
33
|
+
|
|
34
|
+
session.artist.search("The feelies") # => returns a list of Artists
|
|
35
|
+
session.user.logged_in_user # => the current user
|
|
36
|
+
session.tag.popular # => the most popular tags
|
|
37
|
+
|
|
38
|
+
See the Provider docs for more details of how you can access models. Models have attributes
|
|
39
|
+
and links to other models.
|
|
40
|
+
|
|
41
|
+
venue = session.venue.search("Koko").first
|
|
42
|
+
venue.city # => "London"
|
|
43
|
+
venue.events # => "list of events"
|
|
44
|
+
venue.events.first.artist # => an Artist
|
|
45
|
+
|
|
46
|
+
== Current limiations & TODOs
|
|
47
|
+
|
|
48
|
+
* Paged methods only provide the first page of results when accessed through the model. The goal
|
|
49
|
+
is to make it transparent that you may have to make n remote calls to get a certain number of
|
|
50
|
+
results. You can, of course, access arbitrary numbers of results through the Facade methods.
|
|
51
|
+
* Not all attributes will be populated on Models when accessed from different method calls. This
|
|
52
|
+
is down to the set of data Last.fm returns. The goal is to have missing attributes be populated
|
|
53
|
+
as needed by further api calls.
|
|
54
|
+
* Might be nice to have Playlists use another ruby XSPF library for more functionality.
|
|
55
|
+
* Radio tuning not done yet.
|
|
56
|
+
* HTTP caching
|
|
57
|
+
* in-built request throttling
|
|
58
|
+
|
|
59
|
+
== Contributions
|
|
60
|
+
|
|
61
|
+
Contributions are welcome. Source is on github at http://github.com/knaveofdiamonds/shortwave
|
|
62
|
+
|
|
63
|
+
== Dependencies & Credits
|
|
64
|
+
|
|
65
|
+
Shortwave depends on the following gems:
|
|
66
|
+
|
|
67
|
+
* Nokogiri
|
|
68
|
+
* Restclient
|
|
69
|
+
|
|
70
|
+
Shortwave also includes a version of HappyMapper (under vendor) copyright John Nunemaker, hacked about to work directly with Nokogiri documents and fix some bugs. See vendor/happymapper/License for details.
|
|
71
|
+
|
|
72
|
+
== Copyright
|
|
73
|
+
|
|
74
|
+
Copyright (c) 2009 Roland Swingler. See LICENSE for details.
|
data/Rakefile
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
require 'rubygems'
|
|
2
|
+
require 'rake'
|
|
3
|
+
|
|
4
|
+
begin
|
|
5
|
+
require "lib/shortwave/facade/build/facade_builder"
|
|
6
|
+
|
|
7
|
+
include Shortwave::Facade::Build
|
|
8
|
+
namespace :facade do
|
|
9
|
+
directory "tmp/lastfm"
|
|
10
|
+
|
|
11
|
+
task :scrape_method_index => "tmp/lastfm" do
|
|
12
|
+
REMOTE_METHOD_DEFINITIONS = FacadeBuilder.new.remote_method_definitions("tmp/lastfm/intro.yml")
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
task :scrape => :scrape_method_index do
|
|
16
|
+
methods = REMOTE_METHOD_DEFINITIONS.values.inject {|a, b| a.merge(b) }
|
|
17
|
+
methods.each do |name, uri|
|
|
18
|
+
if ! File.exists?("tmp/lastfm/#{name}.html")
|
|
19
|
+
response = DocumentationRemote.get(uri)
|
|
20
|
+
File.open("tmp/lastfm/#{name}.html", "w") {|fh| fh.write(response) }
|
|
21
|
+
warn "Got HTML documentation for #{name}"
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
task :parse => :scrape do
|
|
27
|
+
REMOTE_METHODS = {}
|
|
28
|
+
REMOTE_METHOD_DEFINITIONS.each do |klass_name, methods|
|
|
29
|
+
REMOTE_METHODS[klass_name] = []
|
|
30
|
+
methods.keys.each do |method_name|
|
|
31
|
+
warn "parsing #{method_name}"
|
|
32
|
+
REMOTE_METHODS[klass_name] << RemoteMethod.new( File.read("tmp/lastfm/#{method_name}.html"))
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Fixes any ommissions or mistakes in the HTML documentation
|
|
38
|
+
task :patch => :parse do
|
|
39
|
+
patch_methods "Tag", :top_artists, :top_tracks, :top_albums do |method|
|
|
40
|
+
method.parameters << Parameter.new(:tag, true, "Last.fm tag")
|
|
41
|
+
end
|
|
42
|
+
patch_methods "Group", :weekly_album_chart do |method|
|
|
43
|
+
method.parameters.delete_if {|p| p.name == :user}
|
|
44
|
+
method.parameters << Parameter.new(:group, true, "Group name")
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def patch_methods(klass, *methods, &block)
|
|
49
|
+
REMOTE_METHODS[klass].select {|m| methods.include?(m.name) }.each do |method|
|
|
50
|
+
yield method
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
task :build => :patch do
|
|
55
|
+
KLASSES = REMOTE_METHODS.map do |klass_name, methods|
|
|
56
|
+
warn "Building #{klass_name}"
|
|
57
|
+
methods.inject(RubyClass.new(klass_name)) do |klass, method|
|
|
58
|
+
klass.methods << RubyMethod.new(method)
|
|
59
|
+
klass
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
desc "Scrapes the HTML documentation from the Last.FM site and uses it to construct ruby facade objects"
|
|
65
|
+
task :compile => :build do
|
|
66
|
+
klasses = KLASSES
|
|
67
|
+
File.open("lib/shortwave/facade/lastfm.rb", "w") do |fh|
|
|
68
|
+
fh.write ERB.new(File.read("lib/shortwave/facade/build/facade_template.erb")).result(binding)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
rescue LoadError
|
|
74
|
+
warn "Cannot build a fresh Facade::Remote - missing gems (nokogiri)?"
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
begin
|
|
79
|
+
require 'jeweler'
|
|
80
|
+
Jeweler::Tasks.new do |gem|
|
|
81
|
+
gem.name = "shortwave"
|
|
82
|
+
gem.summary = "A Last.fm API wrapper"
|
|
83
|
+
gem.email = "roland.swingler@gmail.com"
|
|
84
|
+
gem.homepage = "http://shortwave.rubyforge.org"
|
|
85
|
+
gem.authors = ["Roland Swingler"]
|
|
86
|
+
gem.add_dependency("rest-client", ">= 0.9.2")
|
|
87
|
+
gem.add_dependency("nokogiri", ">= 1.2.3")
|
|
88
|
+
gem.rubyforge_project = "shortwave"
|
|
89
|
+
end
|
|
90
|
+
rescue LoadError
|
|
91
|
+
puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
require 'rake/testtask'
|
|
95
|
+
Rake::TestTask.new(:test) do |test|
|
|
96
|
+
test.libs << 'lib' << 'test'
|
|
97
|
+
test.pattern = 'test/**/*_test.rb'
|
|
98
|
+
test.verbose = true
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
begin
|
|
102
|
+
require 'rcov/rcovtask'
|
|
103
|
+
Rcov::RcovTask.new do |test|
|
|
104
|
+
test.libs << 'test'
|
|
105
|
+
test.pattern = 'test/**/*_test.rb'
|
|
106
|
+
test.verbose = true
|
|
107
|
+
end
|
|
108
|
+
rescue LoadError
|
|
109
|
+
task :rcov do
|
|
110
|
+
abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
task :default => :test
|
|
116
|
+
|
|
117
|
+
require 'rake/rdoctask'
|
|
118
|
+
Rake::RDocTask.new do |rdoc|
|
|
119
|
+
if File.exist?('VERSION.yml')
|
|
120
|
+
config = YAML.load(File.read('VERSION.yml'))
|
|
121
|
+
version = "#{config[:major]}.#{config[:minor]}.#{config[:patch]}"
|
|
122
|
+
else
|
|
123
|
+
version = ""
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
rdoc.rdoc_dir = 'doc'
|
|
127
|
+
rdoc.title = "shortwave #{version}"
|
|
128
|
+
rdoc.rdoc_files.include('README*')
|
|
129
|
+
rdoc.rdoc_files.include('lib/*.rb')
|
|
130
|
+
rdoc.rdoc_files.include('lib/shortwave/*.rb')
|
|
131
|
+
rdoc.rdoc_files.include('lib/shortwave/facade/*.rb')
|
|
132
|
+
rdoc.rdoc_files.include('lib/shortwave/model/*.rb')
|
|
133
|
+
end
|
data/VERSION
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.0.1
|
data/lib/shortwave.rb
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__) + "/shortwave")
|
|
2
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__) + "/../vendor/happymapper/lib")
|
|
3
|
+
require 'happymapper'
|
|
4
|
+
require 'authentication'
|
|
5
|
+
require 'facade'
|
|
6
|
+
require 'providers'
|
|
7
|
+
require 'model/base_model'
|
|
8
|
+
Dir[File.dirname(__FILE__) + "/shortwave/model/*.rb"].each {|model| require model }
|
|
9
|
+
|
|
10
|
+
module Shortwave
|
|
11
|
+
Authentication::Session.send(:include, Provider::ProviderMethods)
|
|
12
|
+
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
require 'nokogiri'
|
|
2
|
+
require 'digest/md5'
|
|
3
|
+
include Digest
|
|
4
|
+
|
|
5
|
+
module Shortwave
|
|
6
|
+
# Authentication classes.
|
|
7
|
+
#
|
|
8
|
+
# See http://www.last.fm/api/authentication for more details on how Last.fm expects
|
|
9
|
+
# you to authenticate. The Sessions take care of method signatures and so on for
|
|
10
|
+
# you.
|
|
11
|
+
module Authentication
|
|
12
|
+
# Indicates that you have tried to call a remote method that require authentication,
|
|
13
|
+
# but have not authenticated yet.
|
|
14
|
+
class NotAuthenticated < StandardError
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Base functionality for session-based authentication mechanisms. Don't use this
|
|
18
|
+
# directly - use one of its subclasses: Web, Desktop or Mobile.
|
|
19
|
+
class Session
|
|
20
|
+
attr_reader :session_key
|
|
21
|
+
|
|
22
|
+
# Creates a new session with your api account key and secret. If you have already
|
|
23
|
+
# authenticated and stored a session key, you can provide it to save having to
|
|
24
|
+
# authenticate again.
|
|
25
|
+
def initialize(api_key, secret, session_key=nil)
|
|
26
|
+
@api_key, @secret, @session_key = api_key, secret, session_key
|
|
27
|
+
@facade = Facade::Auth.new(self)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Is the user signed in to Last.fm?
|
|
31
|
+
def signed_in?
|
|
32
|
+
! @session_key.nil?
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Merges relevant authentication details with method parameters.
|
|
36
|
+
def merge!(type, params)
|
|
37
|
+
raise NotAuthenticated.new("Requires authentication!") if type == :session && @session_key.nil?
|
|
38
|
+
|
|
39
|
+
params.merge!(:api_key => @api_key)
|
|
40
|
+
params.merge!(:sk => @session_key) if type == :session
|
|
41
|
+
params.merge!(:api_sig => signature(params)) if type == :session || type == :signed
|
|
42
|
+
params
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Generates a method signature for method parameters.
|
|
46
|
+
def signature(params)
|
|
47
|
+
sorted_params = params.map {|k,v| [k.to_s, v.to_s] }.sort_by {|a| a[0] }
|
|
48
|
+
MD5.hexdigest(sorted_params.flatten.join("") + @secret)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
protected
|
|
52
|
+
|
|
53
|
+
# Parses the response to Auth.getSession
|
|
54
|
+
def parse_session_response(response)
|
|
55
|
+
doc = Nokogiri::XML(response)
|
|
56
|
+
if doc.root['status'] = 'ok'
|
|
57
|
+
@session_key = doc.css("key").text.strip
|
|
58
|
+
else
|
|
59
|
+
raise doc.css("error").text
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Authentication for mobile applications. Don't use this for web/desktop applications
|
|
65
|
+
# use either Authentication::Web or Authentication::Desktop instead
|
|
66
|
+
class Mobile < Session
|
|
67
|
+
# Authenticates with a user's username and password
|
|
68
|
+
def authenticate(username, password)
|
|
69
|
+
response = @facade.mobile_session(username, MD5.hexdigest(username + MD5.hexdigest(password)))
|
|
70
|
+
parse_session_response(response)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Authentication for web applications. Send your user to the page given by +uri+
|
|
75
|
+
# and use the token provided to your callback url as an argument to +authenticate+
|
|
76
|
+
class Web < Session
|
|
77
|
+
# The uri you should direct users to in their web browser, so they can authenticate. If successful,
|
|
78
|
+
# the callback url defined in your api account will be called, with a token parameter. Pass this
|
|
79
|
+
# token to the authenticate method.
|
|
80
|
+
def uri
|
|
81
|
+
"http://www.last.fm/api/auth/?api_key=#{@api_key}"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Gets an authenticated session key. Call after the user has logged in at +uri+ with the
|
|
85
|
+
# token returned to your callback uri.
|
|
86
|
+
def authenticate(token)
|
|
87
|
+
parse_session_response(@facade.session(token))
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Authentication for destop applications. Send your user to the page given by +uri+
|
|
92
|
+
# and then call +authenticate+.
|
|
93
|
+
class Desktop < Session
|
|
94
|
+
# A uri the user should log in at.
|
|
95
|
+
def uri
|
|
96
|
+
response = @facade.token
|
|
97
|
+
@token = Nokogiri::XML(response).css("token").text.strip
|
|
98
|
+
"http://www.last.fm/api/auth/?api_key=#{@api_key}&token=#{@token}"
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Gets an authenticated session key. Call after the user has logged in at +uri+.
|
|
102
|
+
def authenticate
|
|
103
|
+
parse_session_response(@facade.session(token))
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
require 'nokogiri'
|
|
2
|
+
require 'httparty'
|
|
3
|
+
require 'yaml'
|
|
4
|
+
|
|
5
|
+
module Shortwave
|
|
6
|
+
module Facade
|
|
7
|
+
module Build
|
|
8
|
+
METHOD_NAME_CSS = "#wstitle ~ h1"
|
|
9
|
+
PARAMETER_CSS = "#wsdescriptor h2 ~ .param"
|
|
10
|
+
SAMPLE_CSS = "#sample pre"
|
|
11
|
+
DESCRIPTION_CSS = ".wsdescription"
|
|
12
|
+
METHOD_TYPE_CSS = "#wsdescriptor"
|
|
13
|
+
REMOTE_CLASS = "li.package"
|
|
14
|
+
|
|
15
|
+
COMBINED_PARAMS = /^(.+)\[(.+)\]$/
|
|
16
|
+
PARAMETER_TEXT = /\s*\(([^\)]+)\)\s*:\s*(.*)/
|
|
17
|
+
IN_WORD_CAPS = /(.)([A-Z])/
|
|
18
|
+
STARTS_WITH_GET = /^.+\.(get)?/
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# Various Helper methods that belong on String
|
|
22
|
+
module StringExtensions
|
|
23
|
+
# Convert a string like FooBar to foo_bar
|
|
24
|
+
def camel_to_snake
|
|
25
|
+
gsub(IN_WORD_CAPS,"\\1_\\2").tr('A-Z','a-z')
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
String.send(:include, StringExtensions)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# Helper methods for constructing the Facades
|
|
32
|
+
class FacadeBuilder
|
|
33
|
+
def remote_method_definitions(location)
|
|
34
|
+
return @method_definitions if @method_definitions
|
|
35
|
+
if File.exists?( location )
|
|
36
|
+
@method_definitions = YAML.load(File.read(location))
|
|
37
|
+
else
|
|
38
|
+
response = Build::DocumentationRemote.scrape_remote_method_index
|
|
39
|
+
File.open(location, "w") {|fh| fh.write(response.to_yaml) }
|
|
40
|
+
@method_definitions = response
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# A remote facade for the Last FM html documentation
|
|
46
|
+
class DocumentationRemote
|
|
47
|
+
include HTTParty
|
|
48
|
+
base_uri "http://last.fm"
|
|
49
|
+
|
|
50
|
+
def self.scrape_remote_method_index
|
|
51
|
+
html = get("/api/intro")
|
|
52
|
+
Nokogiri::HTML(html).css(REMOTE_CLASS).inject({}) do |hsh, node|
|
|
53
|
+
hsh[node.text] = node.next.next.css("a").inject({}) do |h, anchor|
|
|
54
|
+
h[anchor.text] = anchor["href"]
|
|
55
|
+
h
|
|
56
|
+
end
|
|
57
|
+
hsh
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# The class of a RemoteFacade to be generated
|
|
64
|
+
class RubyClass
|
|
65
|
+
attr_reader :name, :methods
|
|
66
|
+
|
|
67
|
+
def initialize(name)
|
|
68
|
+
@name = name
|
|
69
|
+
@methods = []
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# A ruby method in a RemoteFacade that will be generated
|
|
75
|
+
class RubyMethod
|
|
76
|
+
attr_accessor :signature
|
|
77
|
+
attr_reader :comment, :body, :name
|
|
78
|
+
|
|
79
|
+
def initialize(node)
|
|
80
|
+
@comment = []
|
|
81
|
+
@body = []
|
|
82
|
+
@node = node
|
|
83
|
+
@parameters = node.parameters || []
|
|
84
|
+
@required, @optional = (@parameters).partition {|p| p.required? }
|
|
85
|
+
@required.reject! {|p| [:api_key, :api_sig, :sk].include?(p.name) }
|
|
86
|
+
|
|
87
|
+
build_comment
|
|
88
|
+
build_signature
|
|
89
|
+
build_body
|
|
90
|
+
self
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
private
|
|
94
|
+
|
|
95
|
+
def build_body
|
|
96
|
+
mode = if @parameters.any? {|p| p.name == :sk }
|
|
97
|
+
:session
|
|
98
|
+
elsif @parameters.any? {|p| p.name == :api_sig }
|
|
99
|
+
:signed
|
|
100
|
+
else
|
|
101
|
+
:standard
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
get_line = "#{@node.http_method}(:#{mode}, {:method => \"#{@node.remote_name}\""
|
|
105
|
+
@required.each {|p| get_line << ", :#{p.name} => #{p.name}" }
|
|
106
|
+
get_line << "}"
|
|
107
|
+
get_line << ".merge(options)" unless @optional.empty?
|
|
108
|
+
get_line << ")"
|
|
109
|
+
@body << get_line
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def build_signature
|
|
113
|
+
@signature = "#{@node.name}"
|
|
114
|
+
unless @node.parameters.nil? || @node.parameters.empty?
|
|
115
|
+
params = @required.map {|p| p.name }
|
|
116
|
+
params << "options={}" unless @optional.empty?
|
|
117
|
+
@signature << "(" << params.join(", ") << ")"
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def build_comment
|
|
122
|
+
comment << "# #{@node.description}" if @node.description
|
|
123
|
+
|
|
124
|
+
unless @required.empty?
|
|
125
|
+
comment << "#"
|
|
126
|
+
@required.each {|p| comment << "# +#{p.name}+:: #{p.description}" }
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
unless @optional.empty?
|
|
130
|
+
comment << "#"
|
|
131
|
+
comment << "# <b>Options</b>"
|
|
132
|
+
@optional.each {|p| comment << "# +#{p.name}+:: #{p.description}" }
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
# A parameter used in a Last FM api method call.
|
|
139
|
+
class Parameter
|
|
140
|
+
attr_reader :name, :description
|
|
141
|
+
|
|
142
|
+
def initialize(name, required, description)
|
|
143
|
+
@name, @required, @description = name, required, description.gsub(/\s+/, ' ')
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Returns an array of Parameters, given an HTML page from the Last FM API
|
|
147
|
+
# method documentation
|
|
148
|
+
def self.parse(html)
|
|
149
|
+
doc = html.kind_of?(Nokogiri::HTML::Document) ? html : Nokogiri::HTML(html)
|
|
150
|
+
|
|
151
|
+
doc.css(PARAMETER_CSS).map do |node|
|
|
152
|
+
name = node.text.strip
|
|
153
|
+
unless name.nil? || name.empty?
|
|
154
|
+
if match = name.match(COMBINED_PARAMS)
|
|
155
|
+
match[2].split("|").map {|v| make_parameter(node, match[1] + v) }
|
|
156
|
+
else
|
|
157
|
+
make_parameter(node, name)
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end.flatten.compact
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Returns true if this parameter is required
|
|
164
|
+
def required?
|
|
165
|
+
@required
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
private
|
|
169
|
+
|
|
170
|
+
def self.make_parameter(node, name)
|
|
171
|
+
if match = node.next.text.match(PARAMETER_TEXT)
|
|
172
|
+
self.new(name.to_sym, match[1].start_with?("Required"), match[2])
|
|
173
|
+
else
|
|
174
|
+
self.new(name.to_sym, true, "")
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
# Represents an available method in the Last FM 'rest' api. Provides information
|
|
181
|
+
# about how to call the method.
|
|
182
|
+
class RemoteMethod
|
|
183
|
+
attr_reader :name, :remote_name, :parameters, :description, :http_method, :sample_response
|
|
184
|
+
|
|
185
|
+
# Creates a RemoteMethod, given an html page from the Last FM API documentation.
|
|
186
|
+
def initialize(html)
|
|
187
|
+
doc = Nokogiri::HTML(html)
|
|
188
|
+
|
|
189
|
+
@remote_name = doc.css(METHOD_NAME_CSS).text.strip
|
|
190
|
+
@name = @remote_name.sub(STARTS_WITH_GET,'').camel_to_snake.to_sym
|
|
191
|
+
@parameters = Parameter.parse(doc)
|
|
192
|
+
@description = doc.css(DESCRIPTION_CSS).text.strip.gsub(/\s+/, ' ')
|
|
193
|
+
@http_method = doc.css(METHOD_TYPE_CSS).text.include?("HTTP POST") ? :post : :get
|
|
194
|
+
@sample_response = doc.css(SAMPLE_CSS).text.strip
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|