hornairs-scrobbler 0.2.3

Sign up to get free protection for your applications and to get access to all the features.
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