scrobbler-ng 2.0.0

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.
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