scrobbler-ng 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (156) hide show
  1. data/.gitignore +11 -0
  2. data/MIT-LICENSE +19 -0
  3. data/README.rdoc +104 -0
  4. data/Rakefile +11 -0
  5. data/VERSION.yml +4 -0
  6. data/examples/album.rb +19 -0
  7. data/examples/artist.rb +13 -0
  8. data/examples/scrobble.rb +31 -0
  9. data/examples/tag.rb +11 -0
  10. data/examples/track.rb +6 -0
  11. data/examples/user.rb +14 -0
  12. data/lib/scrobbler/album.rb +165 -0
  13. data/lib/scrobbler/artist.rb +169 -0
  14. data/lib/scrobbler/auth.rb +47 -0
  15. data/lib/scrobbler/base.rb +124 -0
  16. data/lib/scrobbler/event.rb +105 -0
  17. data/lib/scrobbler/geo.rb +27 -0
  18. data/lib/scrobbler/helper/image.rb +52 -0
  19. data/lib/scrobbler/helper/streamable.rb +40 -0
  20. data/lib/scrobbler/library.rb +123 -0
  21. data/lib/scrobbler/playing.rb +49 -0
  22. data/lib/scrobbler/playlist.rb +45 -0
  23. data/lib/scrobbler/radio.rb +12 -0
  24. data/lib/scrobbler/rest.rb +47 -0
  25. data/lib/scrobbler/scrobble.rb +116 -0
  26. data/lib/scrobbler/session.rb +9 -0
  27. data/lib/scrobbler/shout.rb +21 -0
  28. data/lib/scrobbler/simpleauth.rb +60 -0
  29. data/lib/scrobbler/tag.rb +113 -0
  30. data/lib/scrobbler/track.rb +138 -0
  31. data/lib/scrobbler/user.rb +222 -0
  32. data/lib/scrobbler/venue.rb +67 -0
  33. data/lib/scrobbler.rb +34 -0
  34. data/tasks/jeweler.rake +18 -0
  35. data/tasks/rdoc.rake +7 -0
  36. data/tasks/tests.rake +13 -0
  37. data/tasks/yardoc.rake +8 -0
  38. data/test/fixtures/xml/album/info.xml +43 -0
  39. data/test/fixtures/xml/artist/fans.xml +52 -0
  40. data/test/fixtures/xml/artist/info.xml +58 -0
  41. data/test/fixtures/xml/artist/similar.xml +1004 -0
  42. data/test/fixtures/xml/artist/topalbums.xml +61 -0
  43. data/test/fixtures/xml/artist/toptags.xml +19 -0
  44. data/test/fixtures/xml/artist/toptracks.xml +62 -0
  45. data/test/fixtures/xml/auth/session.xml +7 -0
  46. data/test/fixtures/xml/auth/token.xml +3 -0
  47. data/test/fixtures/xml/event/attend.xml +3 -0
  48. data/test/fixtures/xml/event/attendees.xml +53 -0
  49. data/test/fixtures/xml/event/event.xml +35 -0
  50. data/test/fixtures/xml/event/shouts.xml +35 -0
  51. data/test/fixtures/xml/geo/events-distance-p1.xml +151 -0
  52. data/test/fixtures/xml/geo/events-lat-long.xml +167 -0
  53. data/test/fixtures/xml/geo/events-p1.xml +152 -0
  54. data/test/fixtures/xml/geo/events-p2.xml +380 -0
  55. data/test/fixtures/xml/geo/events-p3.xml +427 -0
  56. data/test/fixtures/xml/geo/top_artists-p1.xml +76 -0
  57. data/test/fixtures/xml/geo/top_tracks-p1.xml +77 -0
  58. data/test/fixtures/xml/library/albums-f30.xml +454 -0
  59. data/test/fixtures/xml/library/albums-p1.xml +754 -0
  60. data/test/fixtures/xml/library/albums-p2.xml +754 -0
  61. data/test/fixtures/xml/library/albums-p3.xml +754 -0
  62. data/test/fixtures/xml/library/albums-p4.xml +739 -0
  63. data/test/fixtures/xml/library/albums-p5.xml +754 -0
  64. data/test/fixtures/xml/library/albums-p6.xml +754 -0
  65. data/test/fixtures/xml/library/albums-p7.xml +739 -0
  66. data/test/fixtures/xml/library/albums-p8.xml +724 -0
  67. data/test/fixtures/xml/library/artists-f30.xml +334 -0
  68. data/test/fixtures/xml/library/artists-p1.xml +554 -0
  69. data/test/fixtures/xml/library/artists-p2.xml +554 -0
  70. data/test/fixtures/xml/library/artists-p3.xml +554 -0
  71. data/test/fixtures/xml/library/artists-p4.xml +554 -0
  72. data/test/fixtures/xml/library/artists-p5.xml +554 -0
  73. data/test/fixtures/xml/library/artists-p6.xml +554 -0
  74. data/test/fixtures/xml/library/artists-p7.xml +444 -0
  75. data/test/fixtures/xml/library/tracks-f30.xml +472 -0
  76. data/test/fixtures/xml/library/tracks-p1.xml +789 -0
  77. data/test/fixtures/xml/library/tracks-p10.xml +771 -0
  78. data/test/fixtures/xml/library/tracks-p11.xml +792 -0
  79. data/test/fixtures/xml/library/tracks-p12.xml +777 -0
  80. data/test/fixtures/xml/library/tracks-p13.xml +762 -0
  81. data/test/fixtures/xml/library/tracks-p14.xml +759 -0
  82. data/test/fixtures/xml/library/tracks-p15.xml +762 -0
  83. data/test/fixtures/xml/library/tracks-p16.xml +756 -0
  84. data/test/fixtures/xml/library/tracks-p17.xml +780 -0
  85. data/test/fixtures/xml/library/tracks-p18.xml +756 -0
  86. data/test/fixtures/xml/library/tracks-p19.xml +774 -0
  87. data/test/fixtures/xml/library/tracks-p2.xml +765 -0
  88. data/test/fixtures/xml/library/tracks-p20.xml +777 -0
  89. data/test/fixtures/xml/library/tracks-p21.xml +777 -0
  90. data/test/fixtures/xml/library/tracks-p22.xml +771 -0
  91. data/test/fixtures/xml/library/tracks-p23.xml +765 -0
  92. data/test/fixtures/xml/library/tracks-p24.xml +783 -0
  93. data/test/fixtures/xml/library/tracks-p25.xml +777 -0
  94. data/test/fixtures/xml/library/tracks-p26.xml +777 -0
  95. data/test/fixtures/xml/library/tracks-p27.xml +756 -0
  96. data/test/fixtures/xml/library/tracks-p28.xml +771 -0
  97. data/test/fixtures/xml/library/tracks-p29.xml +753 -0
  98. data/test/fixtures/xml/library/tracks-p3.xml +771 -0
  99. data/test/fixtures/xml/library/tracks-p30.xml +780 -0
  100. data/test/fixtures/xml/library/tracks-p31.xml +753 -0
  101. data/test/fixtures/xml/library/tracks-p32.xml +771 -0
  102. data/test/fixtures/xml/library/tracks-p33.xml +762 -0
  103. data/test/fixtures/xml/library/tracks-p34.xml +538 -0
  104. data/test/fixtures/xml/library/tracks-p4.xml +792 -0
  105. data/test/fixtures/xml/library/tracks-p5.xml +780 -0
  106. data/test/fixtures/xml/library/tracks-p6.xml +789 -0
  107. data/test/fixtures/xml/library/tracks-p7.xml +789 -0
  108. data/test/fixtures/xml/library/tracks-p8.xml +780 -0
  109. data/test/fixtures/xml/library/tracks-p9.xml +774 -0
  110. data/test/fixtures/xml/tag/similar.xml +254 -0
  111. data/test/fixtures/xml/tag/topalbums.xml +805 -0
  112. data/test/fixtures/xml/tag/topartists.xml +605 -0
  113. data/test/fixtures/xml/tag/toptags.xml +1254 -0
  114. data/test/fixtures/xml/tag/toptracks.xml +852 -0
  115. data/test/fixtures/xml/track/fans.xml +34 -0
  116. data/test/fixtures/xml/track/info.xml +53 -0
  117. data/test/fixtures/xml/track/toptags.xml +504 -0
  118. data/test/fixtures/xml/user/events.xml +401 -0
  119. data/test/fixtures/xml/user/friends.xml +30 -0
  120. data/test/fixtures/xml/user/lovedtracks.xml +678 -0
  121. data/test/fixtures/xml/user/neighbours.xml +23 -0
  122. data/test/fixtures/xml/user/playlists.xml +61 -0
  123. data/test/fixtures/xml/user/profile.xml +12 -0
  124. data/test/fixtures/xml/user/recentbannedtracks.xml +24 -0
  125. data/test/fixtures/xml/user/recentlovedtracks.xml +24 -0
  126. data/test/fixtures/xml/user/recenttracks.xml +124 -0
  127. data/test/fixtures/xml/user/systemrecs.xml +18 -0
  128. data/test/fixtures/xml/user/topalbums.xml +61 -0
  129. data/test/fixtures/xml/user/topartists.xml +41 -0
  130. data/test/fixtures/xml/user/toptags.xml +44 -0
  131. data/test/fixtures/xml/user/toptracks.xml +65 -0
  132. data/test/fixtures/xml/user/weeklyalbumchart.xml +256 -0
  133. data/test/fixtures/xml/user/weeklyartistchart.xml +220 -0
  134. data/test/fixtures/xml/user/weeklytrackchart.xml +746 -0
  135. data/test/fixtures/xml/venue/events.xml +151 -0
  136. data/test/fixtures/xml/venue/venue.xml +16 -0
  137. data/test/mocks/rest.rb +212 -0
  138. data/test/spec_helper.rb +7 -0
  139. data/test/test_helper.rb +20 -0
  140. data/test/unit/album_spec.rb +51 -0
  141. data/test/unit/artist_spec.rb +130 -0
  142. data/test/unit/auth_spec.rb +36 -0
  143. data/test/unit/event_spec.rb +109 -0
  144. data/test/unit/geo_spec.rb +148 -0
  145. data/test/unit/library_spec.rb +133 -0
  146. data/test/unit/playing_test.rb +53 -0
  147. data/test/unit/playlist_spec.rb +25 -0
  148. data/test/unit/radio_spec.rb +22 -0
  149. data/test/unit/scrobble_spec.rb +55 -0
  150. data/test/unit/scrobble_test.rb +69 -0
  151. data/test/unit/simpleauth_test.rb +45 -0
  152. data/test/unit/tag_spec.rb +101 -0
  153. data/test/unit/track_spec.rb +95 -0
  154. data/test/unit/user_spec.rb +264 -0
  155. data/test/unit/venue_spec.rb +104 -0
  156. metadata +253 -0
@@ -0,0 +1,124 @@
1
+ require 'digest/md5'
2
+
3
+ $KCODE = 'u'
4
+
5
+ include LibXML
6
+
7
+ module Scrobbler
8
+
9
+ API_URL = 'http://ws.audioscrobbler.com/'
10
+
11
+ class Base
12
+ def Base.api_key=(api_key)
13
+ @@api_key = api_key
14
+ end
15
+
16
+ def Base.secret=(secret)
17
+ @@secret = secret
18
+ end
19
+
20
+ def Base.connection
21
+ @connection ||= REST::Connection.new(API_URL)
22
+ end
23
+
24
+ def Base.sanitize(param)
25
+ URI.escape(param.to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
26
+ end
27
+
28
+ def Base.camelize(lower_case_and_underscored_word, first_letter_in_uppercase = true)
29
+ if first_letter_in_uppercase
30
+ lower_case_and_underscored_word.to_s.gsub(/\/(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase }
31
+ else
32
+ lower_case_and_underscored_word.first.downcase + camelize(lower_case_and_underscored_word)[1..-1]
33
+ end
34
+ end
35
+
36
+ def Base.constanize(camel_cased_word)
37
+ names = camel_cased_word.split('::')
38
+ names.shift if names.empty? || names.first.empty?
39
+
40
+ constant = Object
41
+ names.each do |name|
42
+ constant = constant.const_defined?(name) ? constant.const_get(name) : constant.const_missing(name)
43
+ end
44
+ constant
45
+ end
46
+
47
+ def Base.get(api_method, parent, element, parameters = {})
48
+ scrobbler_class = constanize(camelize("scrobbler/#{element.to_s}"))
49
+ doc = request(api_method, parameters)
50
+ elements = []
51
+ doc.root.children.each do |child|
52
+ next unless child.name == parent.to_s
53
+ child.children.each do |child2|
54
+ next unless child2.name == element.to_s
55
+ elements << scrobbler_class.new_from_libxml(child2)
56
+ end
57
+ end
58
+ elements
59
+ end
60
+
61
+ def Base.post_request(api_method, parameters = {}, request_method = 'get')
62
+ Base.request(api_method, parameters, 'post')
63
+ end
64
+
65
+ def Base.request(api_method, parameters = {}, request_method = 'get')
66
+ parameters = {:signed => false}.merge(parameters)
67
+ parameters['api_key'] = @@api_key
68
+ parameters['method'] = api_method.to_s
69
+ paramlist = []
70
+ # Check if we want a signed call and pop :signed
71
+ if parameters.delete :signed
72
+ #1: Sort alphabetically
73
+ params = parameters.sort{|a,b| a[0].to_s<=>b[0].to_s}
74
+ #2: concat them into one string
75
+ str = params.join('')
76
+ #3: Append secret
77
+ str = str + @@secret
78
+ #4: Make a md5 hash
79
+ md5 = Digest::MD5.hexdigest(str)
80
+ params << [:api_sig, md5]
81
+ params.each do |a|
82
+ paramlist << "#{sanitize(a[0])}=#{sanitize(a[1])}"
83
+ end
84
+ else
85
+ parameters.each do |key, value|
86
+ paramlist << "#{sanitize(key)}=#{sanitize(value)}"
87
+ end
88
+ end
89
+ url = '/2.0/?' + paramlist.join('&')
90
+ XML::Document.string(self.connection.send(request_method,url))
91
+ end
92
+
93
+
94
+ private
95
+
96
+ def Base.mixins(*args)
97
+ args.each do |arg|
98
+ if arg == :image
99
+ extend Scrobbler::ImageClassFuncs
100
+ include Scrobbler::ImageObjectFuncs
101
+ elsif arg == :streamable
102
+ attr_reader :streamable
103
+ extend StreamableClassFuncs
104
+ include StreamableObjectFuncs
105
+ else
106
+ raise ArgumentError, "#{arg} is not a known mixin"
107
+ end
108
+ end
109
+ end
110
+
111
+ def populate_data(data = {})
112
+ data.each do |key, value|
113
+ instance_variable_set("@#{key.to_s}", value)
114
+ end
115
+ end
116
+
117
+ def get_response(api_method, instance_name, parent, element, params, force=false)
118
+ if instance_variable_get("@#{instance_name}").nil? || force
119
+ instance_variable_set("@#{instance_name}", Base.get(api_method, parent, element, params))
120
+ end
121
+ instance_variable_get("@#{instance_name}")
122
+ end
123
+ end # class Base
124
+ end # module Scrobbler
@@ -0,0 +1,105 @@
1
+ module Scrobbler
2
+ class Event < Base
3
+ # Load Helper modules
4
+ include ImageObjectFuncs
5
+ extend ImageClassFuncs
6
+
7
+ attr_accessor :id, :title, :start_date, :start_time, :description
8
+ attr_accessor :reviews, :tag, :url, :artists, :headliner, :image_small
9
+ attr_accessor :image_medium, :image_large, :attendance, :venue
10
+
11
+ class << self
12
+
13
+ def update_or_create_from_xml(xml, event=nil)
14
+ data = {}
15
+ artists = []
16
+ headliner = nil
17
+ venue = nil
18
+
19
+ xml.children.each do |child|
20
+ data[:id] = child.content.to_i if child.name == 'id'
21
+ data[:title] = child.content if child.name == 'title'
22
+
23
+ if child.name == 'artists'
24
+ child.children.each do |artist_element|
25
+ artists << Artist.new(artist_element.content) if artist_element.name == 'artist'
26
+ headliner = Artist.new(artist_element.content) if artist_element.name == 'headliner'
27
+ end
28
+ artists << headliner unless headliner.nil? || headliner_alrady_listed_in_artist_list?(artists,headliner)
29
+ end
30
+
31
+ maybe_image_node(data, child)
32
+ data[:url] = child.content if child.name == 'url'
33
+ data[:description] = child.content if child.name == 'description'
34
+ data[:attendance] = child.content.to_i if child.name == 'attendance'
35
+ data[:reviews] = child.content.to_i if child.name == 'reviews'
36
+ data[:tag] = child.content if child.name == 'tag'
37
+ data[:start_date] = Time.parse(child.content) if child.name == 'startDate'
38
+ data[:start_time] = child.content if child.name == 'startTime'
39
+ venue = Venue.new_from_xml(child) if child.name == 'venue'
40
+ end
41
+
42
+ if event.nil?
43
+ event = Event.new(data[:id],data)
44
+ else
45
+ event.send :populate_data, data
46
+ end
47
+
48
+ event.artists = artists.uniq
49
+ event.headliner = headliner
50
+ event.venue = venue
51
+ event
52
+ end
53
+
54
+ def headliner_alrady_listed_in_artist_list?(artists,headliner)
55
+ artists.each do |artist|
56
+ return true if artist.name == headliner.name
57
+ end
58
+ false
59
+ end
60
+
61
+ def new_from_libxml(xml)
62
+ update_or_create_from_xml(xml)
63
+ end
64
+ end
65
+
66
+ def initialize(id,input={})
67
+ raise ArgumentError if id.nil?
68
+ @id = id
69
+ populate_data(input)
70
+ load_info() if input[:include_info]
71
+ end
72
+
73
+ def shouts(force = false)
74
+ get_response('event.getshouts', :shouts, 'shouts', 'shout', {'event'=>@id}, force)
75
+ end
76
+
77
+ def attendees(force = false)
78
+ get_response('event.getattendees', :attendees, 'attendees', 'user', {'event'=>@id}, force)
79
+ end
80
+
81
+ def shout
82
+ # This function require authentication, but SimpleAuth is not yet 2.0
83
+ raise NotImplementedError
84
+ end
85
+
86
+ def attend(session, attendance_status)
87
+ doc = Base.post_request('event.attend',{:event => @id, :signed => true, :status => attendance_status, :sk => session.key})
88
+ end
89
+
90
+ def share
91
+ # This function require authentication, but SimpleAuth is not yet 2.0
92
+ raise NotImplementedError
93
+ end
94
+
95
+ # Load additional informatalbumion about this event
96
+ #
97
+ # Calls "event.getinfo" REST method
98
+ def load_info
99
+ doc = Base.request('event.getinfo', {'event' => @id})
100
+ doc.root.children.each do |child|
101
+ Event.update_or_create_from_xml(child, self) if child.name == 'event'
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,27 @@
1
+ module Scrobbler
2
+ class Geo < Base
3
+
4
+ # Gets a list of events based on the location that
5
+ # the Geo object is set to
6
+ def events(options={})
7
+ options = set_default_options(options)
8
+ get_response('geo.getevents', :events, 'events', 'event', options, options[:force])
9
+ end
10
+
11
+ def top_artists(options={})
12
+ options = set_default_options(options)
13
+ get_response('geo.gettopartists', :artists, 'topartists', 'artist', options, options[:force])
14
+ end
15
+
16
+ def top_tracks(options={})
17
+ options = set_default_options(options)
18
+ get_response('geo.gettoptracks', :tracks, 'toptracks', 'track', options, options[:force])
19
+ end
20
+
21
+ private
22
+
23
+ def set_default_options(options={})
24
+ {:force => false, :page => 1}.merge options
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,52 @@
1
+ module Scrobbler
2
+ # Defines some functions that are used nearly all Scrobbler classes which
3
+ # have to deal with image references.
4
+ #
5
+ # This module defines the class functions, use "extend ImageClassFuncs" in
6
+ # class.
7
+ module ImageClassFuncs
8
+ # Check if the given libxml node is an image referencing node and in case
9
+ # of, read it into that given hash of data
10
+ def maybe_image_node(data, node)
11
+ if node.name == 'image'
12
+ data[:image_small] = node.content if node['size'] == 'small'
13
+ data[:image_medium] = node.content if node['size'] == 'medium'
14
+ data[:image_large] = node.content if node['size'] == 'large'
15
+ data[:image_extralarge] = node.content if node['size'] == 'extralarge'
16
+ end
17
+ end
18
+ end
19
+
20
+ # Defines some functions that are used nearly all Scrobbler classes which
21
+ # have to deal with image references.
22
+ #
23
+ # This module defines the object functions, use "include ImageObjectFuncs" in
24
+ # class.
25
+ module ImageObjectFuncs
26
+ # Check if the given libxml node is an image referencing node and in case
27
+ # of, read it into the object
28
+ def check_image_node(node)
29
+ if node.name == 'image'
30
+ @image_small = node.content if node['size'] == 'small'
31
+ @image_medium = node.content if node['size'] == 'medium'
32
+ @image_large = node.content if node['size'] == 'large'
33
+ @image_extralarge = node.content if node['size'] == 'extralarge'
34
+ end
35
+ end
36
+
37
+ # Return the URL to the specified image size.
38
+ #
39
+ # If the URL to the given image size is not known and a
40
+ # 'load_info'-function is defined, it will be called.
41
+ def image(which=:small)
42
+ which = which.to_s
43
+ raise ArgumentError unless ['small', 'medium', 'large', 'extralarge'].include?(which)
44
+ img_url = instance_variable_get("@image_#{which}")
45
+ if img_url.nil? && responds_to?(:load_info)
46
+ load_info
47
+ img_url = instance_variable_get("@image_#{which}")
48
+ end
49
+ img_url
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,40 @@
1
+ module Scrobbler
2
+ # Defines some functions that are used nearly all Scrobbler classes which
3
+ # have to deal with the streamable flag.
4
+ #
5
+ # This module defines the class functions, use "extend StreamableClassFuncs" in
6
+ # class.
7
+ module StreamableClassFuncs
8
+ # Check if the given libxml node is defining if the given content is
9
+ # streamable. If so, set the flag in the given hash
10
+ def Base.maybe_streamable_node(data, node)
11
+ if node.name == 'streamable'
12
+ data[:streamable] = ['1', 'true'].include?(node.content)
13
+ end
14
+ end
15
+
16
+ # Check if the given libxml node has a streamable attribute defining if the
17
+ # given content is streamable. If so, set the flag in the given hash
18
+ def Base.maybe_streamable_attribute(data, node)
19
+ if node['streamable']
20
+ data[:streamable] = ['1', 'true'].include?(node['streamable'])
21
+ end
22
+ end
23
+
24
+ end
25
+
26
+ # Defines some functions that are used nearly all Scrobbler classes which
27
+ # have to deal with the streamable flag.
28
+ #
29
+ # This module defines the object functions, use "include StreamableObjectFuncs" in
30
+ # class.
31
+ module StreamableObjectFuncs
32
+ # Check if the given libxml node is defining if the given content is
33
+ # streamable. If so, set the flag in the object
34
+ def check_streamable_node(node)
35
+ if node.name == 'streamable'
36
+ @streamable = ['1', 'true'].include?(node.content)
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,123 @@
1
+ module Scrobbler
2
+ # Class for invocation of library.* API functions.
3
+ class Library < Base
4
+ attr_reader :user
5
+
6
+ # Create a new Scrobber::Library instance.
7
+ #
8
+ # @param [String, Symbol, Scrobbler::User] user The user who's library data
9
+ # is requested.
10
+ # @raise ArgumentError If a not supported type is given as user.
11
+ def initialize(user)
12
+ case user.class
13
+ when user.class == Scrobbler::User
14
+ @user = user
15
+ when [String, Symbol].contains?(user.class)
16
+ @user = Scrobbler::User.new(user.to_s)
17
+ else
18
+ raise ArgumentError("Invalid argument for user.")
19
+ end
20
+ end
21
+
22
+ def add_album
23
+ # This function require authentication, but SimpleAuth is not yet 2.0
24
+ raise NotImplementedError
25
+ end
26
+
27
+ def add_artist
28
+ # This function require authentication, but SimpleAuth is not yet 2.0
29
+ raise NotImplementedError
30
+ end
31
+
32
+ def add_track
33
+ # This function require authentication, but SimpleAuth is not yet 2.0
34
+ raise NotImplementedError
35
+ end
36
+
37
+ # A list of all the albums in a user's library, with play counts and tag
38
+ # counts.
39
+ def albums(options={})
40
+ options = {:force => false, :all => true}.merge options
41
+ options[:user] = @user.name
42
+ albums = []
43
+ if options[:all]
44
+ doc = Base.request('library.getalbums', options)
45
+ root = nil
46
+ doc.root.children.each do |child|
47
+ next unless child.name == 'albums'
48
+ root = child
49
+ end
50
+ total_pages = root['totalPages'].to_i
51
+ root.children.each do |child|
52
+ next unless child.name == 'album'
53
+ albums << Scrobbler::Album.new_from_libxml(child)
54
+ end
55
+ for i in 2..total_pages do
56
+ options[:page] = i
57
+ albums.concat get_response('library.getalbums', :none, 'albums', 'album', options, true)
58
+ end
59
+ else
60
+ albums = get_response('library.getalbums', :get_albums, 'albums', 'album', options, true)
61
+ end
62
+ albums
63
+ end
64
+
65
+ # A list of all the artists in a user's library, with play counts and tag
66
+ # counts.
67
+ def artists(options={})
68
+ options = {:force => false, :all => true}.merge options
69
+ options[:user] = @user.name
70
+ artists = []
71
+ if options[:all]
72
+ doc = Base.request('library.getartists', options)
73
+ root = nil
74
+ doc.root.children.each do |child|
75
+ next unless child.name == 'artists'
76
+ root = child
77
+ end
78
+ total_pages = root['totalPages'].to_i
79
+ root.children.each do |child|
80
+ next unless child.name == 'artist'
81
+ artists << Scrobbler::Artist.new_from_libxml(child)
82
+ end
83
+ for i in 2..total_pages do
84
+ options[:page] = i
85
+ artists.concat get_response('library.getartists', :none, 'artists', 'artist', options, true)
86
+ end
87
+ else
88
+ artists = get_response('library.getartists', :get_albums, 'artists', 'artist', options, true)
89
+ end
90
+ artists
91
+ end
92
+
93
+ # A list of all the tracks in a user's library, with play counts and tag
94
+ # counts.
95
+ def tracks(options={})
96
+ options = {:force => false, :all => true}.merge options
97
+ options[:user] = @user.name
98
+ tracks = []
99
+ if options[:all]
100
+ doc = Base.request('library.gettracks', options)
101
+ root = nil
102
+ doc.root.children.each do |child|
103
+ next unless child.name == 'tracks'
104
+ root = child
105
+ end
106
+ total_pages = root['totalPages'].to_i
107
+ root.children.each do |child|
108
+ next unless child.name == 'track'
109
+ tracks << Scrobbler::Track.new_from_libxml(child)
110
+ end
111
+ for i in 2..total_pages do
112
+ options[:page] = i
113
+ tracks.concat get_response('library.gettracks', :none, 'tracks', 'track', options, true)
114
+ end
115
+ else
116
+ tracks = get_response('library.gettracks', :get_albums, 'tracks', 'track', options, true)
117
+ end
118
+ tracks
119
+ end
120
+
121
+ end
122
+ end
123
+
@@ -0,0 +1,49 @@
1
+ module Scrobbler
2
+ class Playing
3
+ # you should read last.fm/api/submissions#np first!
4
+
5
+ attr_accessor :session_id, :now_playing_url, :artist, :track,
6
+ :album, :length, :track_number, :mb_track_id
7
+ attr_reader :status
8
+
9
+ def initialize(args = {})
10
+ @session_id = args[:session_id] # from Scrobbler::SimpleAuth
11
+ @now_playing_url = args[:now_playing_url] # from Scrobbler::SimpleAuth (can change)
12
+ @artist = args[:artist] # track artist
13
+ @track = args[:track] # track name
14
+ @album = args[:album] || '' # track album (optional)
15
+ @length = args[:length] || '' # track length in seconds (optional)
16
+ @track_number = args[:track_number] || '' # track number (optional)
17
+ @mb_track_id = args[:mb_track_id] || '' # MusicBrainz track ID (optional)
18
+
19
+ if [@session_id, @now_playing_url, @artist, @track].any?(&:empty?)
20
+ raise ArgumentError, 'Missing required argument'
21
+ elsif !@length.to_s.empty? && @length.to_i <= 30 # see last.fm/api
22
+ raise ArgumentError, 'Length must be greater than 30 seconds'
23
+ end
24
+
25
+ @connection = REST::Connection.new(@now_playing_url)
26
+ end
27
+
28
+ def submit!
29
+ query = { :s => @session_id,
30
+ :a => @artist,
31
+ :t => @track,
32
+ :b => @album,
33
+ :l => @length,
34
+ :n => @track_number,
35
+ :m => @mb_track_id }
36
+
37
+ @status = @connection.post('', query)
38
+
39
+ case @status
40
+ when /OK/
41
+
42
+ when /BADSESSION/
43
+ raise BadSessionError # rerun Scrobbler::SimpleAuth#handshake!
44
+ else
45
+ raise RequestFailedError
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,45 @@
1
+ module Scrobbler
2
+ # @todo everything
3
+ class Playlist < Base
4
+ # Load Helper modules
5
+ include ImageObjectFuncs
6
+ extend ImageClassFuncs
7
+
8
+ attr_reader :url, :id, :title, :date, :creator
9
+ attr_reader :description, :size, :duration, :streamable
10
+ class << self
11
+ def new_from_libxml(xml)
12
+ data = {}
13
+
14
+ xml.children.each do |child|
15
+ data[:id] = child.content.to_i if child.name == 'id'
16
+ data[:title] = child.content if child.name == 'title'
17
+
18
+ maybe_image_node(data, child)
19
+ data[:date] = Time.parse(child.content) if child.name == 'date'
20
+
21
+ data[:size] = child.content.to_i if child.name == 'size'
22
+ data[:description] = child.content if child.name == 'description'
23
+ data[:duration] = child.content.to_i if child.name == 'duration'
24
+
25
+ if child.name == 'streamable'
26
+ if ['1', 'true'].include?(child.content)
27
+ data[:streamable] = true
28
+ else
29
+ data[:streamable] = false
30
+ end
31
+ end
32
+ data[:creator] = child.content if child.name == 'creator'
33
+ data[:url] = child.content if child.name == 'url'
34
+ end
35
+ Playlist.new(data[:url],data)
36
+ end
37
+ end
38
+
39
+ def initialize(url,data={})
40
+ @url = url
41
+ populate_data(data)
42
+ end
43
+ end
44
+ end
45
+
@@ -0,0 +1,12 @@
1
+ module Scrobbler
2
+ # @todo everything
3
+ class Radio < Base
4
+ attr_reader :station
5
+
6
+ def initialize(station)
7
+ @station = station
8
+ end
9
+
10
+ end
11
+ end
12
+
@@ -0,0 +1,47 @@
1
+ require 'net/https'
2
+
3
+ module Scrobbler
4
+ module REST
5
+ class Connection
6
+ def initialize(base_url, args = {})
7
+ @base_url = base_url
8
+ @username = args[:username]
9
+ @password = args[:password]
10
+ end
11
+
12
+ def get(resource, args = nil)
13
+ request(resource, "get", args)
14
+ end
15
+
16
+ def post(resource, args = nil)
17
+ request(resource, "post", args)
18
+ end
19
+
20
+ def request(resource, method = "get", args = nil)
21
+ url = URI.join(@base_url, resource)
22
+
23
+ if args
24
+ # TODO: What about keys without value?
25
+ url.query = args.map { |k,v| "%s=%s" % [URI.encode(k.to_s), URI.encode(v.to_s)] }.join("&")
26
+ end
27
+
28
+ case method
29
+ when "get"
30
+ req = Net::HTTP::Get.new(url.request_uri)
31
+ when "post"
32
+ req = Net::HTTP::Post.new(url.request_uri)
33
+ end
34
+
35
+ if @username and @password
36
+ req.basic_auth(@username, @password)
37
+ end
38
+
39
+ http = Net::HTTP.new(url.host, url.port)
40
+ http.use_ssl = (url.port == 443)
41
+
42
+ res = http.start() { |conn| conn.request(req) }
43
+ res.body
44
+ end
45
+ end
46
+ end
47
+ end