hornairs-scrobbler 0.2.3

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 (161) hide show
  1. data/.gitignore +13 -0
  2. data/.loadpath +5 -0
  3. data/History.txt +5 -0
  4. data/MIT-LICENSE +19 -0
  5. data/Manifest +75 -0
  6. data/README.rdoc +104 -0
  7. data/Rakefile +10 -0
  8. data/VERSION.yml +5 -0
  9. data/examples/album.rb +20 -0
  10. data/examples/artist.rb +13 -0
  11. data/examples/playlist.rb +7 -0
  12. data/examples/scrobble.rb +31 -0
  13. data/examples/tag.rb +11 -0
  14. data/examples/track.rb +12 -0
  15. data/examples/user.rb +15 -0
  16. data/lib/scrobbler.rb +33 -0
  17. data/lib/scrobbler/album.rb +181 -0
  18. data/lib/scrobbler/artist.rb +165 -0
  19. data/lib/scrobbler/auth.rb +47 -0
  20. data/lib/scrobbler/base.rb +105 -0
  21. data/lib/scrobbler/event.rb +105 -0
  22. data/lib/scrobbler/geo.rb +27 -0
  23. data/lib/scrobbler/helper/image.rb +52 -0
  24. data/lib/scrobbler/helper/streamable.rb +40 -0
  25. data/lib/scrobbler/library.rb +112 -0
  26. data/lib/scrobbler/playing.rb +49 -0
  27. data/lib/scrobbler/playlist.rb +87 -0
  28. data/lib/scrobbler/radio.rb +12 -0
  29. data/lib/scrobbler/rest.rb +47 -0
  30. data/lib/scrobbler/scrobble.rb +116 -0
  31. data/lib/scrobbler/session.rb +9 -0
  32. data/lib/scrobbler/shout.rb +21 -0
  33. data/lib/scrobbler/simpleauth.rb +60 -0
  34. data/lib/scrobbler/tag.rb +113 -0
  35. data/lib/scrobbler/track.rb +152 -0
  36. data/lib/scrobbler/user.rb +222 -0
  37. data/lib/scrobbler/venue.rb +67 -0
  38. data/setup.rb +1585 -0
  39. data/tasks/jeweler.rake +15 -0
  40. data/tasks/rdoc.rake +7 -0
  41. data/tasks/tests.rake +13 -0
  42. data/tasks/yardoc.rake +8 -0
  43. data/test/fixtures/xml/album/info.xml +43 -0
  44. data/test/fixtures/xml/artist/fans.xml +52 -0
  45. data/test/fixtures/xml/artist/info.xml +58 -0
  46. data/test/fixtures/xml/artist/similar.xml +1004 -0
  47. data/test/fixtures/xml/artist/topalbums.xml +61 -0
  48. data/test/fixtures/xml/artist/toptags.xml +19 -0
  49. data/test/fixtures/xml/artist/toptracks.xml +62 -0
  50. data/test/fixtures/xml/auth/session.xml +7 -0
  51. data/test/fixtures/xml/auth/token.xml +3 -0
  52. data/test/fixtures/xml/event/attend.xml +3 -0
  53. data/test/fixtures/xml/event/attendees.xml +53 -0
  54. data/test/fixtures/xml/event/event.xml +35 -0
  55. data/test/fixtures/xml/event/shouts.xml +35 -0
  56. data/test/fixtures/xml/geo/events-distance-p1.xml +151 -0
  57. data/test/fixtures/xml/geo/events-lat-long.xml +167 -0
  58. data/test/fixtures/xml/geo/events-p1.xml +152 -0
  59. data/test/fixtures/xml/geo/events-p2.xml +380 -0
  60. data/test/fixtures/xml/geo/events-p3.xml +427 -0
  61. data/test/fixtures/xml/geo/top_artists-p1.xml +76 -0
  62. data/test/fixtures/xml/geo/top_tracks-p1.xml +77 -0
  63. data/test/fixtures/xml/library/albums-f30.xml +454 -0
  64. data/test/fixtures/xml/library/albums-p1.xml +754 -0
  65. data/test/fixtures/xml/library/albums-p2.xml +754 -0
  66. data/test/fixtures/xml/library/albums-p3.xml +754 -0
  67. data/test/fixtures/xml/library/albums-p4.xml +739 -0
  68. data/test/fixtures/xml/library/albums-p5.xml +754 -0
  69. data/test/fixtures/xml/library/albums-p6.xml +754 -0
  70. data/test/fixtures/xml/library/albums-p7.xml +739 -0
  71. data/test/fixtures/xml/library/albums-p8.xml +724 -0
  72. data/test/fixtures/xml/library/artists-f30.xml +334 -0
  73. data/test/fixtures/xml/library/artists-p1.xml +554 -0
  74. data/test/fixtures/xml/library/artists-p2.xml +554 -0
  75. data/test/fixtures/xml/library/artists-p3.xml +554 -0
  76. data/test/fixtures/xml/library/artists-p4.xml +554 -0
  77. data/test/fixtures/xml/library/artists-p5.xml +554 -0
  78. data/test/fixtures/xml/library/artists-p6.xml +554 -0
  79. data/test/fixtures/xml/library/artists-p7.xml +444 -0
  80. data/test/fixtures/xml/library/tracks-f30.xml +472 -0
  81. data/test/fixtures/xml/library/tracks-p1.xml +789 -0
  82. data/test/fixtures/xml/library/tracks-p10.xml +771 -0
  83. data/test/fixtures/xml/library/tracks-p11.xml +792 -0
  84. data/test/fixtures/xml/library/tracks-p12.xml +777 -0
  85. data/test/fixtures/xml/library/tracks-p13.xml +762 -0
  86. data/test/fixtures/xml/library/tracks-p14.xml +759 -0
  87. data/test/fixtures/xml/library/tracks-p15.xml +762 -0
  88. data/test/fixtures/xml/library/tracks-p16.xml +756 -0
  89. data/test/fixtures/xml/library/tracks-p17.xml +780 -0
  90. data/test/fixtures/xml/library/tracks-p18.xml +756 -0
  91. data/test/fixtures/xml/library/tracks-p19.xml +774 -0
  92. data/test/fixtures/xml/library/tracks-p2.xml +765 -0
  93. data/test/fixtures/xml/library/tracks-p20.xml +777 -0
  94. data/test/fixtures/xml/library/tracks-p21.xml +777 -0
  95. data/test/fixtures/xml/library/tracks-p22.xml +771 -0
  96. data/test/fixtures/xml/library/tracks-p23.xml +765 -0
  97. data/test/fixtures/xml/library/tracks-p24.xml +783 -0
  98. data/test/fixtures/xml/library/tracks-p25.xml +777 -0
  99. data/test/fixtures/xml/library/tracks-p26.xml +777 -0
  100. data/test/fixtures/xml/library/tracks-p27.xml +756 -0
  101. data/test/fixtures/xml/library/tracks-p28.xml +771 -0
  102. data/test/fixtures/xml/library/tracks-p29.xml +753 -0
  103. data/test/fixtures/xml/library/tracks-p3.xml +771 -0
  104. data/test/fixtures/xml/library/tracks-p30.xml +780 -0
  105. data/test/fixtures/xml/library/tracks-p31.xml +753 -0
  106. data/test/fixtures/xml/library/tracks-p32.xml +771 -0
  107. data/test/fixtures/xml/library/tracks-p33.xml +762 -0
  108. data/test/fixtures/xml/library/tracks-p34.xml +538 -0
  109. data/test/fixtures/xml/library/tracks-p4.xml +792 -0
  110. data/test/fixtures/xml/library/tracks-p5.xml +780 -0
  111. data/test/fixtures/xml/library/tracks-p6.xml +789 -0
  112. data/test/fixtures/xml/library/tracks-p7.xml +789 -0
  113. data/test/fixtures/xml/library/tracks-p8.xml +780 -0
  114. data/test/fixtures/xml/library/tracks-p9.xml +774 -0
  115. data/test/fixtures/xml/tag/similar.xml +254 -0
  116. data/test/fixtures/xml/tag/topalbums.xml +805 -0
  117. data/test/fixtures/xml/tag/topartists.xml +605 -0
  118. data/test/fixtures/xml/tag/toptags.xml +1254 -0
  119. data/test/fixtures/xml/tag/toptracks.xml +852 -0
  120. data/test/fixtures/xml/track/fans.xml +34 -0
  121. data/test/fixtures/xml/track/info.xml +53 -0
  122. data/test/fixtures/xml/track/toptags.xml +504 -0
  123. data/test/fixtures/xml/user/events.xml +401 -0
  124. data/test/fixtures/xml/user/friends.xml +30 -0
  125. data/test/fixtures/xml/user/lovedtracks.xml +678 -0
  126. data/test/fixtures/xml/user/neighbours.xml +23 -0
  127. data/test/fixtures/xml/user/playlists.xml +61 -0
  128. data/test/fixtures/xml/user/profile.xml +12 -0
  129. data/test/fixtures/xml/user/recentbannedtracks.xml +24 -0
  130. data/test/fixtures/xml/user/recentlovedtracks.xml +24 -0
  131. data/test/fixtures/xml/user/recenttracks.xml +124 -0
  132. data/test/fixtures/xml/user/systemrecs.xml +18 -0
  133. data/test/fixtures/xml/user/topalbums.xml +61 -0
  134. data/test/fixtures/xml/user/topartists.xml +41 -0
  135. data/test/fixtures/xml/user/toptags.xml +44 -0
  136. data/test/fixtures/xml/user/toptracks.xml +65 -0
  137. data/test/fixtures/xml/user/weeklyalbumchart.xml +256 -0
  138. data/test/fixtures/xml/user/weeklyartistchart.xml +220 -0
  139. data/test/fixtures/xml/user/weeklytrackchart.xml +746 -0
  140. data/test/fixtures/xml/venue/events.xml +151 -0
  141. data/test/fixtures/xml/venue/venue.xml +16 -0
  142. data/test/mocks/rest.rb +212 -0
  143. data/test/spec_helper.rb +7 -0
  144. data/test/test_helper.rb +20 -0
  145. data/test/unit/album_spec.rb +52 -0
  146. data/test/unit/artist_spec.rb +130 -0
  147. data/test/unit/auth_spec.rb +36 -0
  148. data/test/unit/event_spec.rb +109 -0
  149. data/test/unit/geo_spec.rb +148 -0
  150. data/test/unit/library_spec.rb +133 -0
  151. data/test/unit/playing_test.rb +53 -0
  152. data/test/unit/playlist_spec.rb +25 -0
  153. data/test/unit/radio_spec.rb +22 -0
  154. data/test/unit/scrobble_spec.rb +55 -0
  155. data/test/unit/scrobble_test.rb +69 -0
  156. data/test/unit/simpleauth_test.rb +45 -0
  157. data/test/unit/tag_spec.rb +101 -0
  158. data/test/unit/track_spec.rb +95 -0
  159. data/test/unit/user_spec.rb +264 -0
  160. data/test/unit/venue_spec.rb +104 -0
  161. metadata +265 -0
@@ -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,112 @@
1
+ module Scrobbler
2
+ # @todo everything
3
+ class Library < Base
4
+ attr_reader :user
5
+
6
+ def initialize(user)
7
+ @user = user if user.class == Scrobbler::User
8
+ @user = Scrobbler::User.new(user.to_s) unless user.class == Scrobbler::User
9
+ end
10
+
11
+ def add_album
12
+ # This function require authentication, but SimpleAuth is not yet 2.0
13
+ raise NotImplementedError
14
+ end
15
+
16
+ def add_artist
17
+ # This function require authentication, but SimpleAuth is not yet 2.0
18
+ raise NotImplementedError
19
+ end
20
+
21
+ def add_track
22
+ # This function require authentication, but SimpleAuth is not yet 2.0
23
+ raise NotImplementedError
24
+ end
25
+
26
+ # A list of all the albums in a user's library, with play counts and tag
27
+ # counts.
28
+ def albums(options={})
29
+ options = {:force => false, :all => true}.merge options
30
+ options[:user] = @user.name
31
+ albums = []
32
+ if options[:all]
33
+ doc = Base.request('library.getalbums', options)
34
+ root = nil
35
+ doc.root.children.each do |child|
36
+ next unless child.name == 'albums'
37
+ root = child
38
+ end
39
+ total_pages = root['totalPages'].to_i
40
+ root.children.each do |child|
41
+ next unless child.name == 'album'
42
+ albums << Scrobbler::Album.new_from_libxml(child)
43
+ end
44
+ for i in 2..total_pages do
45
+ options[:page] = i
46
+ albums.concat get_response('library.getalbums', :none, 'albums', 'album', options, true)
47
+ end
48
+ else
49
+ albums = get_response('library.getalbums', :get_albums, 'albums', 'album', options, true)
50
+ end
51
+ albums
52
+ end
53
+
54
+ # A list of all the artists in a user's library, with play counts and tag
55
+ # counts.
56
+ def artists(options={})
57
+ options = {:force => false, :all => true}.merge options
58
+ options[:user] = @user.name
59
+ artists = []
60
+ if options[:all]
61
+ doc = Base.request('library.getartists', options)
62
+ root = nil
63
+ doc.root.children.each do |child|
64
+ next unless child.name == 'artists'
65
+ root = child
66
+ end
67
+ total_pages = root['totalPages'].to_i
68
+ root.children.each do |child|
69
+ next unless child.name == 'artist'
70
+ artists << Scrobbler::Artist.new_from_libxml(child)
71
+ end
72
+ for i in 2..total_pages do
73
+ options[:page] = i
74
+ artists.concat get_response('library.getartists', :none, 'artists', 'artist', options, true)
75
+ end
76
+ else
77
+ artists = get_response('library.getartists', :get_albums, 'artists', 'artist', options, true)
78
+ end
79
+ artists
80
+ end
81
+
82
+ # A list of all the tracks in a user's library, with play counts and tag
83
+ # counts.
84
+ def tracks(options={})
85
+ options = {:force => false, :all => true}.merge options
86
+ options[:user] = @user.name
87
+ tracks = []
88
+ if options[:all]
89
+ doc = Base.request('library.gettracks', options)
90
+ root = nil
91
+ doc.root.children.each do |child|
92
+ next unless child.name == 'tracks'
93
+ root = child
94
+ end
95
+ total_pages = root['totalPages'].to_i
96
+ root.children.each do |child|
97
+ next unless child.name == 'track'
98
+ tracks << Scrobbler::Track.new_from_libxml(child)
99
+ end
100
+ for i in 2..total_pages do
101
+ options[:page] = i
102
+ tracks.concat get_response('library.gettracks', :none, 'tracks', 'track', options, true)
103
+ end
104
+ else
105
+ tracks = get_response('library.gettracks', :get_albums, 'tracks', 'track', options, true)
106
+ end
107
+ tracks
108
+ end
109
+
110
+ end
111
+ end
112
+
@@ -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?(&:blank?)
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,87 @@
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
+
12
+ def url_for_album(album_id)
13
+ "lastfm://playlist/album/"+album_id.to_s
14
+ end
15
+
16
+ def url_for_playlist(playlist_id)
17
+ "lastfm://playlist/album/"+playlist_id.to_s
18
+ end
19
+
20
+ def new_from_album(album)
21
+ pp album.class
22
+ raise ArgumentError, "Must create a playlist from an album object" if album.class != Scrobbler::Album
23
+ Playlist.new(url_for_album(album.album_id))
24
+ end
25
+
26
+ def new_from_xml(xml)
27
+ data = data_from_xml(xml)
28
+ puts "Creating Playlist: #{data[:title]}"
29
+ return nil if data[:title].nil?
30
+ Playlist.new(data[:url],data)
31
+ end
32
+
33
+ def data_from_xml(xml, o = {})
34
+ o = {:include_track_info => true}.merge(o)
35
+ data = {}
36
+ xml.children.each do |child|
37
+ data[:id] = child.content.to_i if child.name == 'id'
38
+ data[:title] = child.content if child.name == 'title'
39
+
40
+ maybe_image_node(data, child)
41
+ data[:date] = Time.parse(child.content) if child.name == 'date'
42
+
43
+ data[:size] = child.content.to_i if child.name == 'size'
44
+ data[:description] = child.content if child.name == 'description'
45
+ data[:duration] = child.content.to_i if child.name == 'duration'
46
+
47
+ if child.name == 'streamable'
48
+ if ['1', 'true'].include?(child.content)
49
+ data[:streamable] = true
50
+ else
51
+ data[:streamable] = false
52
+ end
53
+ end
54
+
55
+ if child.name == 'trackList'
56
+ data[:tracks] = []
57
+ child.children.each do |grandchild|
58
+ data[:tracks] << Track.new_from_xml(grandchild, o) if grandchild.name == 'track'
59
+ end
60
+ end
61
+ data[:creator] = child.content if child.name == 'creator'
62
+ data[:url] = child.content if child.name == 'url'
63
+ end
64
+ data
65
+ end
66
+ end
67
+
68
+ def initialize(url,data={})
69
+ @url = url
70
+ fetch() unless data[:include_info] && data[:include_info] == false
71
+ end
72
+
73
+ def fetch()
74
+ #Since playlists aren't static in the same sense the other objects are, dont cache results, always fetch the lastest version.
75
+ xml = Base.request('playlist.fetch', {:playlistURL => @url});
76
+ unless xml.root['status'] == 'failed'
77
+ xml.root.children.each do |child|
78
+ next unless child.name == 'playlist'
79
+ data = self.class.data_from_xml(child, {:include_track_info => false, :include_album_info => false})
80
+ populate_data(data)
81
+ break
82
+ end # xml.children.each do |child|
83
+ end
84
+ end
85
+ end
86
+ end
87
+
@@ -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
@@ -0,0 +1,116 @@
1
+ # exception definitions
2
+ class BadAuthError < StandardError; end
3
+ class BadSessionError < StandardError; end
4
+ class BadTimeError < StandardError; end
5
+ class ClientBannedError < StandardError; end
6
+ class RequestFailedError < StandardError; end
7
+
8
+ module Scrobbler
9
+ class Scrobble
10
+ SUBMISSION_HOST = 'http://post.audioscrobbler.com:80/'
11
+ SUBMISSION_PORT = 80
12
+ SUBMISSION_VERSION = '1.2.1'
13
+
14
+ @@client_id = 'tst'
15
+ @@client_ver = '1.0'
16
+ @@default_options = {
17
+ :handshake_on_init => true
18
+ }
19
+
20
+ def Scrobble.client_id=(cid)
21
+ @@client_id = cid
22
+ end
23
+
24
+ def Scrobble.client_ver=(cver)
25
+ @@client_ver = cver
26
+ end
27
+
28
+ def initialize(options={})
29
+ params = @@default_options.merge(options)
30
+ warn 'Warning: Default client id used, this is for testing only' if @@client_id == 'tst'
31
+ raise ArgumentError, 'Session is required' if params[:session].nil?
32
+ raise ArgumentError, 'User is required' if params[:user].nil?
33
+ @session = params[:session]
34
+ @user = params[:user]
35
+ end
36
+
37
+ def handshake
38
+ ts = Time.now.to_i.to_s
39
+ auth_token = Digest::MD5.hexdigest(@session.key + ts)
40
+ params = {
41
+ :hs => 'true',
42
+ :ps => SUBMISSION_VERSION,
43
+ :c => @@client_id,
44
+ :v => @@client_ver,
45
+ :u => @user.name,
46
+ :t => ts,
47
+ :a => auth_token,
48
+ :sk => @session.key
49
+ }
50
+ url = URI.join(SUBMISSION_HOST)
51
+ query = []
52
+ params.each do |k,v|
53
+ query << [Base.sanitize(k), Base.sanitize(v)]
54
+ end
55
+ url.query = query.join('&')
56
+ req = Net::HTTP::Get.new(url.request_uri)
57
+ res = Net::HTTP.start(SUBMISSION_HOST, SUBMISSION_PORT) do |conn|
58
+ conn.request(req)
59
+ end
60
+ res = res.body.split("\n")
61
+ if res[0] == 'OK'
62
+ # Handshake done, parse and return information
63
+ @session_id = res[1]
64
+ @now_playing_url = res[2]
65
+ @submission_url = res[3]
66
+ elsif res[0] == 'BANNED'
67
+ # This indicates that this client version has been banned from the
68
+ # server. This usually happens if the client is violating the protocol
69
+ # in a destructive way. Users should be asked to upgrade their client
70
+ # application.
71
+ raise ClientBannedError, 'Please update your client to a newer version.'
72
+ elsif res[0] == 'BADAUTH'
73
+ # This indicates that the authentication details provided were incorrect.
74
+ # The client should not retry the handshake until the user has changed
75
+ # their details
76
+ raise BadAuthError
77
+ elsif res[0] == 'BADTIME'
78
+ # The timestamp provided was not close enough to the current time.
79
+ # The system clock must be corrected before re-handshaking.
80
+ raise BadTimeError, "Not close enough to current time: #{ts}"
81
+ else
82
+ # This indicates a temporary server failure. The reason indicates the
83
+ # cause of the failure. The client should proceed as directed in the
84
+ # failure handling section
85
+ raise RequestFailedError, res[0]
86
+ end
87
+ end #^ handshake
88
+
89
+ def now_playing(track)
90
+ params = {
91
+ 's' => @session_id,
92
+ 't' => track.name,
93
+ 'a' => track.artist.name,
94
+ 'b' => '',
95
+ 'l' => '',
96
+ 'n' => '',
97
+ 'm' => ''
98
+ }
99
+ params['b'] = track.album.name unless track.album.nil?
100
+ params['l'] = track.duration.to_s unless track.duration.nil? && track.duration > 30
101
+ # @todo params['n']
102
+ params['m'] = track.mbid unless track.mbid.nil?
103
+ res = Net::HTTP.post_form(URI.parse(SUBMISSION_HOST), params).body.split("\n")
104
+ if res[0] == 'BADSESSION'
105
+ # This indicates that the Session ID sent was somehow invalid, possibly
106
+ # because another client has performed a handshake for this user. On
107
+ # receiving this, the client should re-handshake with the server before
108
+ # continuing.
109
+ raise BadSessionError
110
+ end
111
+ end #^ now_playing
112
+
113
+ def submit(tracks={})
114
+ end #^ submit
115
+ end
116
+ end