shortwave 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (105) hide show
  1. data/.document +5 -0
  2. data/.gitignore +10 -0
  3. data/.gitmodules +3 -0
  4. data/LICENSE +20 -0
  5. data/README.rdoc +74 -0
  6. data/Rakefile +133 -0
  7. data/VERSION +1 -0
  8. data/lib/shortwave.rb +12 -0
  9. data/lib/shortwave/authentication.rb +107 -0
  10. data/lib/shortwave/facade.rb +3 -0
  11. data/lib/shortwave/facade/build/facade_builder.rb +199 -0
  12. data/lib/shortwave/facade/build/facade_template.erb +17 -0
  13. data/lib/shortwave/facade/lastfm.rb +878 -0
  14. data/lib/shortwave/facade/remote.rb +86 -0
  15. data/lib/shortwave/model/album.rb +33 -0
  16. data/lib/shortwave/model/artist.rb +62 -0
  17. data/lib/shortwave/model/base_model.rb +112 -0
  18. data/lib/shortwave/model/chart_dates.rb +15 -0
  19. data/lib/shortwave/model/comparison.rb +15 -0
  20. data/lib/shortwave/model/event.rb +55 -0
  21. data/lib/shortwave/model/group.rb +26 -0
  22. data/lib/shortwave/model/location.rb +45 -0
  23. data/lib/shortwave/model/playlist.rb +36 -0
  24. data/lib/shortwave/model/shout.rb +21 -0
  25. data/lib/shortwave/model/tag.rb +47 -0
  26. data/lib/shortwave/model/track.rb +71 -0
  27. data/lib/shortwave/model/user.rb +70 -0
  28. data/lib/shortwave/model/venue.rb +37 -0
  29. data/lib/shortwave/model/weekly_charts.rb +31 -0
  30. data/lib/shortwave/providers.rb +161 -0
  31. data/shortwave.gemspec +178 -0
  32. data/test/authentication_test.rb +64 -0
  33. data/test/build/build_test.rb +25 -0
  34. data/test/build/data/intro.yml +2 -0
  35. data/test/build/data/screens/album_addTags.html +1238 -0
  36. data/test/build/data/screens/intro_truncated.html +426 -0
  37. data/test/build/data/screens/tasteometer_compare.html +1274 -0
  38. data/test/build/data/screens/user_getLovedTracks.html +1278 -0
  39. data/test/build/data/screens/venue_search.html +1261 -0
  40. data/test/build/facade_builder_test.rb +23 -0
  41. data/test/build/parameter_test.rb +43 -0
  42. data/test/build/remote_method_test.rb +47 -0
  43. data/test/build/ruby_class_test.rb +12 -0
  44. data/test/build/ruby_method_test.rb +137 -0
  45. data/test/helper.rb +35 -0
  46. data/test/model/album_test.rb +62 -0
  47. data/test/model/artist_test.rb +103 -0
  48. data/test/model/chart_dates_test.rb +11 -0
  49. data/test/model/comparison_test.rb +18 -0
  50. data/test/model/data/album_info.xml +38 -0
  51. data/test/model/data/album_search.xml +210 -0
  52. data/test/model/data/artist_info.xml +58 -0
  53. data/test/model/data/artist_search.xml +109 -0
  54. data/test/model/data/artist_shouts.xml +546 -0
  55. data/test/model/data/artist_top_fans.xml +405 -0
  56. data/test/model/data/event_info.xml +47 -0
  57. data/test/model/data/group_members.xml +242 -0
  58. data/test/model/data/group_weekly_album_chart.xml +1754 -0
  59. data/test/model/data/group_weekly_artist_chart.xml +604 -0
  60. data/test/model/data/group_weekly_track_chart.xml +1005 -0
  61. data/test/model/data/location_events.xml +383 -0
  62. data/test/model/data/ok.xml +2 -0
  63. data/test/model/data/playlist_fetch.xml +227 -0
  64. data/test/model/data/tag_search.xml +110 -0
  65. data/test/model/data/tag_similar.xml +254 -0
  66. data/test/model/data/tag_top_albums.xml +805 -0
  67. data/test/model/data/tag_top_artists.xml +605 -0
  68. data/test/model/data/tag_top_tags.xml +1254 -0
  69. data/test/model/data/tag_top_tracks.xml +843 -0
  70. data/test/model/data/tag_weekly_chart_list.xml +57 -0
  71. data/test/model/data/tasteometer_compare.xml +54 -0
  72. data/test/model/data/track_info.xml +53 -0
  73. data/test/model/data/track_search.xml +195 -0
  74. data/test/model/data/user_chartlist.xml +90 -0
  75. data/test/model/data/user_info.xml +16 -0
  76. data/test/model/data/user_neighbours.xml +484 -0
  77. data/test/model/data/user_playlists.xml +17 -0
  78. data/test/model/data/user_recent_tracks.xml +124 -0
  79. data/test/model/data/user_recommended_artists.xml +454 -0
  80. data/test/model/data/user_shouts.xml +9 -0
  81. data/test/model/data/user_weekly_artist_chart.xml +478 -0
  82. data/test/model/data/venue_events.xml +556 -0
  83. data/test/model/data/venue_past_events.xml +1778 -0
  84. data/test/model/data/venue_search.xml +355 -0
  85. data/test/model/event_test.rb +63 -0
  86. data/test/model/group_test.rb +45 -0
  87. data/test/model/location_test.rb +25 -0
  88. data/test/model/playlist_test.rb +51 -0
  89. data/test/model/shout_test.rb +23 -0
  90. data/test/model/tag_test.rb +39 -0
  91. data/test/model/track_test.rb +67 -0
  92. data/test/model/user_test.rb +125 -0
  93. data/test/model/venue_test.rb +60 -0
  94. data/test/provider/album_provider_test.rb +26 -0
  95. data/test/provider/artist_provider_test.rb +25 -0
  96. data/test/provider/group_provider_test.rb +9 -0
  97. data/test/provider/location_provider_test.rb +9 -0
  98. data/test/provider/playlist_provider_test.rb +12 -0
  99. data/test/provider/tag_provider_test.rb +24 -0
  100. data/test/provider/track_provider_test.rb +26 -0
  101. data/test/provider/user_provider_test.rb +11 -0
  102. data/test/provider/venue_provider_test.rb +15 -0
  103. data/test/provider_test_helper.rb +27 -0
  104. data/test/remote_test.rb +26 -0
  105. metadata +209 -0
@@ -0,0 +1,86 @@
1
+ require 'restclient'
2
+ require 'uri'
3
+ require 'forwardable'
4
+
5
+ module Shortwave
6
+ # Includes thin wrapper classes around the Last.fm web services API.
7
+ #
8
+ # These classes map one-to-one to the Last.fm API, and are in fact generated from the
9
+ # HTML documentation the Last.fm provide. They make no attempt to OOify or XMLify the
10
+ # responses - the results of these methods are just the string responses from Last.fm.
11
+ # As such, they are a suitable base for building a "real" API on top of, which is
12
+ # what classes in Shortwave::Model do.
13
+ #
14
+ # The general use is:
15
+ #
16
+ # # Create a session for your platform
17
+ # session = Shortwave::Authentication::Mobile.new("api_key", "app_secret")
18
+ #
19
+ # # Authenticate if necessary (if you want to use write methods)
20
+ # session.authenticate("user", "password")
21
+ #
22
+ # # Create a facade, passing in the session
23
+ # facade = Shortwave::Facade::Artist.new(session)
24
+ #
25
+ # # Call methods on the facade
26
+ # facade.top_tags("The feelies")
27
+ # # => returns <lfm status="ok"> ... etc.
28
+ #
29
+ # Required method parameters in the web api are real method parameters - if the method
30
+ # has optional parameters, these appear in an options hash after all the required parameters.
31
+ # You do not need to provide any authentication parameters (+api_key+, +api_sig+ & +sk+) - these
32
+ # are merged in from the session for you automatically.
33
+ #
34
+ # If the request fails, the facade method will raise a RemoteError.
35
+ #
36
+ # Each of the facades has further documentation, taken directly from the Last.fm documentation
37
+ # at http://last.fm/api
38
+ module Facade
39
+ # Wraps a RestClient::RequestFailed exception, and delegates +message+, +http_code+
40
+ # and +response+ to the wrapped exception.
41
+ #
42
+ # This class is mainly provided for api stability in case we switch from using RestClient.
43
+ class RemoteError < RuntimeError
44
+ extend Forwardable
45
+ def_delegators :@cause, :message, :to_s, :http_code, :response
46
+
47
+ def initialize(cause)
48
+ @cause = cause
49
+ end
50
+ end
51
+
52
+ # Base class for each remote facade.
53
+ #
54
+ # Remote methods may raise a RemoteError if an http error response is returned from
55
+ # the Last.fm service.
56
+ class Remote
57
+ attr_reader :session
58
+
59
+ # Creates a new facade, given a user session
60
+ def initialize(session)
61
+ @session = session
62
+ end
63
+
64
+ protected
65
+
66
+ BASE_URI = "http://ws.audioscrobbler.com/2.0/"
67
+ DISALLOWED = Regexp.new("[^#{URI::PATTERN::UNRESERVED}]")
68
+ AGENT = "Shortwave 0.0.1 http://shortwave.rubyforge.org/"
69
+
70
+ def get(type, data)
71
+ @session.merge!(type, data)
72
+ uri = BASE_URI + "?" + data.map {|k,v| "#{k.to_s}=#{URI.escape(v.to_s, DISALLOWED)}"}.join("&")
73
+ RestClient.get uri, {"User-Agent" => AGENT}
74
+ rescue RestClient::RequestFailed => e
75
+ raise RemoteError.new(e)
76
+ end
77
+
78
+ def post(type, data)
79
+ @session.merge!(type, data, {"User-Agent" => AGENT})
80
+ RestClient.post BASE_URI, data
81
+ rescue RestClient::RequestFailed => e
82
+ raise RemoteError.new(e)
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,33 @@
1
+ module Shortwave
2
+ module Model
3
+ # An album on Last.fm
4
+ #
5
+ # === Attributes
6
+ #
7
+ # +name+:: Album name
8
+ # +url+:: Last.fm site url
9
+ # +id+:: Last.fm id
10
+ # +release_date+:: Release date
11
+ # +listeners+:: Number of listeners
12
+ # +play_count+:: Number of times tracks from this album have been played
13
+ # +images+:: An array of album images
14
+ # +mbid+:: Musicbrainz ID
15
+ # +artist_name+:: Album's artist
16
+ # +tags+:: Last.fm user tags
17
+ class Album < BaseModel
18
+ element :name, String, :tag => "name|title"
19
+ element :url, String
20
+ element :id, Integer
21
+ element :release_date, Time, :tag => "releasedate"
22
+ element :listeners, Integer
23
+ element :play_count, Integer, :tag => "playcount"
24
+ element :images, String, :tag => "image", :single => false
25
+ element :mbid, String
26
+ element :artist_name, String, :tag => "artist"
27
+ has_many :tags, "Shortwave::Model::Tag", :tag => "toptags/tag"
28
+
29
+ identified_by :artist_name, :name
30
+ taggable
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,62 @@
1
+ module Shortwave
2
+ module Model
3
+ # An artist. Artists are taggable, shoutable and sharable by a user.
4
+ #
5
+ # === Attributes
6
+ #
7
+ # +name+:: Artist name
8
+ # +mbid+:: Musicbrainz ID
9
+ # +url+:: Url of artist page on the Last.fm site
10
+ # +play_count+:: The number of times this artist has been played
11
+ # +listeners+:: The number of listeners this artist has
12
+ # +biography_summary+:: Textual description of this artist
13
+ # +biography+:: Full textual description of this artist
14
+ # +images+:: Images of this artist
15
+ class Artist < BaseModel
16
+ element :name, String
17
+ element :mbid, String
18
+ element :url, String
19
+ element :streamable, Boolean
20
+ element :play_count, Integer, :tag => "stats/playcount"
21
+ element :listeners, Integer, :tag => "stats/listeners"
22
+ element :biography_summary, String, :tag => "bio/content"
23
+ element :biography, String, :tag => "bio/content"
24
+ element :images, String, :tag => "image", :single => false
25
+
26
+ identified_by :name
27
+ taggable
28
+ shoutable
29
+ sharable
30
+
31
+ # Events this artist is playing at.
32
+ def events
33
+ link :events, :Event, name
34
+ end
35
+
36
+ # Artists similar to this one.
37
+ def similar
38
+ link :similar, :Artist, name
39
+ end
40
+
41
+ # This artist's top albums
42
+ def albums
43
+ link :top_albums, :Album, name
44
+ end
45
+
46
+ # This artist's top fans
47
+ def fans
48
+ link :top_fans, :User, name
49
+ end
50
+
51
+ # This artist's top tags
52
+ def tags
53
+ link :top_tags, :Tag, name
54
+ end
55
+
56
+ # This artist's top tracks
57
+ def tracks
58
+ link :top_tracks, :Track, name
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,112 @@
1
+ module Shortwave
2
+ module Model
3
+ # Root class for all Shortwave models.
4
+ class BaseModel
5
+ # Sets the session on this model, and all nested models
6
+ def session=(session)
7
+ @session = session
8
+ self.class.has_ones.each do |sym|
9
+ obj = send(sym)
10
+ obj.session = session if obj
11
+ end
12
+ self.class.has_manys.each {|s| (send(s) || []).each {|o| o.session = session } }
13
+ session
14
+ end
15
+
16
+ class << self
17
+ def facade_name
18
+ @facade_name ||= (name.split("::").last.downcase + "_facade").to_sym
19
+ end
20
+
21
+ def identified_by(*methods)
22
+ @lastfm_keys = methods
23
+ end
24
+
25
+ # Adds methods to a model to deal with shouts: shouts and shout
26
+ def shoutable
27
+ class_eval <<-EOV
28
+ def shout(message)
29
+ @session.#{facade_name}.shout(#{@lastfm_keys.join(",")}, message)
30
+ end
31
+
32
+ link_to "Shout", :shouts
33
+ EOV
34
+ end
35
+
36
+ # Adds methods to a model to deal with tags: tags, add_tags and remove_tag
37
+ #
38
+ # Up to ten tags can be added, either strings, or Tag models
39
+ # A single tag can be removed
40
+ def taggable
41
+ class_eval <<-EOV
42
+ def add_tags(*tags)
43
+ tag_param = tags[0...10].map {|t| t.to_s }.join(",")
44
+ @session.#{facade_name}.add_tags(#{@lastfm_keys.join(",")}, tag_param)
45
+ end
46
+
47
+ def remove_tag(tag)
48
+ @session.#{facade_name}.remove_tag(#{@lastfm_keys.join(",")}, tag.to_s)
49
+ end
50
+
51
+ link_to "Tag", :my_tags, :tags
52
+ EOV
53
+ end
54
+
55
+ def sharable
56
+ class_eval <<-EOV
57
+ def share(recipients, message=nil)
58
+ params = {}
59
+ params[:message] = message if message
60
+ @session.#{facade_name}.share(#{@lastfm_keys.join(",")}, recipients[0...10].map{|r| r.to_s }.join(','), params)
61
+ end
62
+ EOV
63
+ end
64
+
65
+ def link_to(klass, method, remote_method=nil)
66
+ remote_method ||= method
67
+ class_eval <<-EOV
68
+ def #{method}
69
+ response = @session.#{facade_name}.#{remote_method}(#{@lastfm_keys.join(',')})
70
+ #{klass}.parse(response).each {|obj| obj.session = @session }
71
+ end
72
+ EOV
73
+ end
74
+
75
+ def inherited(klass)
76
+ klass.send(:include, HappyMapper)
77
+ klass.send(:tag, klass.name.split("::").last.downcase)
78
+
79
+ # Store every has_one/has_many declaration and use this information
80
+ # to decide which attributes need a session adding
81
+ class << klass
82
+ def has_ones
83
+ @has_ones ||= []
84
+ end
85
+
86
+ def has_manys
87
+ @has_manys ||= []
88
+ end
89
+
90
+ def has_one(sym, *args)
91
+ super
92
+ has_ones << sym
93
+ end
94
+
95
+ def has_many(sym, *args)
96
+ super
97
+ has_manys << sym
98
+ end
99
+ end
100
+ end
101
+ end
102
+
103
+ protected
104
+
105
+ def link(remote_method, klass, *args)
106
+ response = @session.send(self.class.facade_name).send(remote_method, *args)
107
+ klass = ::Shortwave::Model.const_get(klass)
108
+ klass.parse(response).each {|obj| obj.session = @session }
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,15 @@
1
+ module Shortwave
2
+ module Model
3
+ # A chart date range. This is only really used internally.
4
+ #
5
+ # === Attributes
6
+ #
7
+ # from:: the date the chart starts
8
+ # to:: the date the chart finishes
9
+ class ChartDates < BaseModel
10
+ tag "chart"
11
+ attribute :from, Time
12
+ attribute :to, Time
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ module Shortwave
2
+ module Model
3
+ # A comparison between two Last.fm users
4
+ #
5
+ # === Attributes
6
+ #
7
+ # +score+:: A number between 0 and 1 representing how similar users are.
8
+ # +artists+:: Artists both users have in common
9
+ class Comparison < BaseModel
10
+ tag "comparison"
11
+ element :score, Float, :tag => "result/score"
12
+ has_many :artists, "Model::Artist", :tag => "result/artists/artist"
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,55 @@
1
+ module Shortwave
2
+ module Model
3
+ # An event.
4
+ #
5
+ # == Attributes
6
+ # +id+:: Last.fm id
7
+ # +name+:: Event name
8
+ # +description+:: Event description
9
+ # +attendance_count+:: Number of last.fm user's attending
10
+ # +review_count+:: Number of reviews
11
+ # +starts_at+:: The start time
12
+ # +url+:: Last.fm site url
13
+ # +venue+:: Event venue
14
+ class Event < BaseModel
15
+ element :id, Integer
16
+ element :description, String
17
+ element :url, String
18
+ element :attendance_count, Integer, :tag => "attendance"
19
+ element :review_count, Integer, :tag => "reviews"
20
+ element :starts_at, Time, :tag => "startDate"
21
+ element :name, String, :tag => "title"
22
+ element :headliner_raw, String, :tag => "artists/headliner"
23
+ element :artists_raw, String, :tag => "artists/artist", :single => false
24
+ has_one :venue, "Model::Venue"
25
+
26
+ identified_by :id
27
+ shoutable
28
+ sharable
29
+
30
+ # Returns the list of users attending this event
31
+ def attendees
32
+ link :attendees, :User, id
33
+ end
34
+
35
+ # Mark the current session user's attendance at this event
36
+ #
37
+ # Possible statuses are +:yes+, +:no+ and +:maybe+
38
+ def attend(status=:yes)
39
+ i = [:yes, :maybe, :no].index(status)
40
+ @session.event_facade.attend(id, i)
41
+ true
42
+ end
43
+
44
+ # Returns the headline act for this event
45
+ def headliner
46
+ @headliner ||= artists.detect {|a| a.name == headliner_raw }
47
+ end
48
+
49
+ # Returns the list of artists playing at this event
50
+ def artists
51
+ @artists ||= artists_raw.map {|a| @session.artist.build(:name => a) }
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,26 @@
1
+ require File.dirname(__FILE__) + "/weekly_charts"
2
+
3
+ module Shortwave
4
+ module Model
5
+ # A Last.fm user group.
6
+ #
7
+ # === Attributes
8
+ #
9
+ # name:: the group name
10
+ class Group < BaseModel
11
+ attr_accessor :name
12
+ identified_by :name
13
+ include WeeklyCharts
14
+
15
+ # Returns the group's members. Currently only 50 users are returned
16
+ def members
17
+ link :members, "User", name
18
+ end
19
+
20
+ # The group name
21
+ def to_s
22
+ name
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,45 @@
1
+ module Shortwave
2
+ module Model
3
+ # A location
4
+ #
5
+ # === Attributes
6
+ # +latitude+:: Latitude
7
+ # +longitude+:: Longitude
8
+ # +street_address+:: Street address
9
+ # +city+:: City
10
+ # +postcode+:: Post code
11
+ # +country+:: Country name
12
+ class Location < BaseModel
13
+ tag "location"
14
+ element :latitude, Float, :tag => "geo:point/geo:lat"
15
+ element :longitude, Float, :tag => "geo:point/geo:long"
16
+ element :city, String
17
+ element :country, String
18
+ element :street_address, String, :tag => "street"
19
+ element :postcode, String, :tag => "postalcode"
20
+
21
+ # Popular artists in this country. Needs country to be defined
22
+ def artists
23
+ link :top_artists, :Artist, country
24
+ end
25
+
26
+ # Popular tracks in this location
27
+ def tracks
28
+ args = [country]
29
+ args << {:location => city} if city
30
+ link :top_tracks, :Track, *args
31
+ end
32
+
33
+ # Return events near this location
34
+ def events
35
+ if latitude && longitude
36
+ link :events, :Event, :lat => latitude, :long => longitude
37
+ elsif city
38
+ link :events, :Event, :location => city
39
+ elsif country
40
+ link :events, :Event, :location => country
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end