simple_scrobbler 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md ADDED
@@ -0,0 +1,54 @@
1
+ SimpleScrobbler
2
+ ===============
3
+
4
+ Scrobble tracks to Last.fm without wanting to gnaw your own arm off.
5
+
6
+ Because, probably, all you want to do is just scrobble some tracks. I couldn't
7
+ find any Ruby libraries/gems that actually worked, so I took some code from
8
+ James Darling and Chris Mear's [Captor](http://github.com/james/captor) hack
9
+ and tidied it up into a self-contained package.
10
+
11
+ Usage
12
+ -----
13
+
14
+ Authorise.
15
+
16
+ require "simple_scrobbler"
17
+ ss = SimpleScrobbler.new(api_key, secret, user)
18
+ ss.fetch_session_key do |url|
19
+ puts "Go and visit #{url}"
20
+ gets
21
+ end
22
+
23
+ Tell Last.fm what you're listening to.
24
+
25
+ > This is used for realtime display of a user's currently playing track,
26
+ > and does not affect a user's musical profile.
27
+
28
+ ss.now_playing("Sex Pistols", "Anarchy in the UK")
29
+
30
+ Scrobble it!
31
+
32
+ ss.submit("Sex Pistols", "Anarchy in the UK", :length => 211)
33
+
34
+ Store the session_key for next time.
35
+
36
+ session_key = ss.session_key
37
+
38
+ And use it:
39
+
40
+ ss = SimpleScrobbler.new(api_key, secret, user, session_key)
41
+
42
+ You can pass in other information to `submit` and `now_playing`:
43
+
44
+ * `:time` (for `submit` only): Time at which the track started playing. Defaults to now
45
+ * `:length`: Length of the track in seconds (required for `submit` if the source is `P`, the default)
46
+ * `:album`: Album title
47
+ * `:track_number`: Track number
48
+ * `:mb_trackid`: MusicBrainz Track ID
49
+
50
+ If you're listening to a radio station, [you'll want to set the source](http://www.last.fm/api/submissions):
51
+
52
+ ss.source = "R" # Non-personalised broadcast (e.g. Shoutcast, BBC Radio 1).
53
+
54
+ This will also free you from needing to specify the track length on `submit`.
@@ -0,0 +1,3 @@
1
+ class SimpleScrobbler
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,218 @@
1
+ require "net/http"
2
+ require "digest/md5"
3
+ require "uri"
4
+ require "cgi"
5
+ require "rexml/document"
6
+ require "simple_scrobbler/version"
7
+
8
+ class SimpleScrobbler
9
+ CLIENT_ID = "tst"
10
+ CLIENT_VERSION = "1.0"
11
+
12
+ HandshakeError = Class.new(RuntimeError)
13
+ SubmissionError = Class.new(RuntimeError)
14
+ DataError = Class.new(RuntimeError)
15
+ SessionError = Class.new(RuntimeError)
16
+
17
+ # Instantiate a new SimpleScrobbler instance. If the session key is not
18
+ # supplied, it must be fetched using fetch_session_key before scrobbling is
19
+ # attempted.
20
+ #
21
+ # Your own API key and secret can be obtained from
22
+ # http://www.last.fm/api/account
23
+ #
24
+ def initialize(api_key, secret, user, session_key=nil)
25
+ @api_key = api_key
26
+ @secret = secret
27
+ @user = user
28
+ @session_key = session_key
29
+ @source = "P"
30
+ end
31
+
32
+ attr_reader :user, :api_key, :secret
33
+ private :api_key, :secret
34
+
35
+ def session_key
36
+ @session_key or raise SessionError, "The session key must be set or fetched"
37
+ end
38
+
39
+ # The source of the track. Required, must be one of the following codes:
40
+ # P :: Chosen by the user.
41
+ # R :: Non-personalised broadcast (e.g. Shoutcast, BBC Radio 1).
42
+ # E :: Personalised recommendation except Last.fm (e.g. Pandora, Launchcast).
43
+ # L :: Last.fm (any mode).
44
+ #
45
+ def source=(a)
46
+ unless %w[ P R E L ].include?(a)
47
+ raise DataError, "source must be one of P, R, E, L (see http://www.last.fm/api/submissions)"
48
+ end
49
+ @source = a
50
+ end
51
+
52
+ # Fetch the auth key needed for the application. This can be stored and
53
+ # supplied in the constructor on future occasions.
54
+ #
55
+ # Yields a URL which the user must visit. The block should not return until
56
+ # this is done.
57
+ #
58
+ def fetch_session_key(&blk)
59
+ doc = last_fm_web_service("method" => "auth.gettoken")
60
+ request_token = doc.value_at("//token")
61
+
62
+ yield "http://www.last.fm/api/auth/?api_key=#{api_key}&token=#{request_token}"
63
+
64
+ doc = last_fm_web_service("method" => "auth.getsession", "token" => request_token)
65
+
66
+ @session_key = doc.value_at("//key")
67
+ @user = doc.value_at("//name")
68
+
69
+ @session_key
70
+ end
71
+
72
+ # Scrobble a track.
73
+ #
74
+ # The artist and track are required parameters. Other parameters can be added
75
+ # as options:
76
+ #
77
+ # :time :: Time at which the track started playing. Defaults to now
78
+ # :length :: Length of the track in seconds (required if the source is "P", the default)
79
+ # :album :: Album title
80
+ # :track_number :: Track number
81
+ # :mb_trackid :: MusicBrainz Track ID
82
+ #
83
+ def submit(artist, track, options={})
84
+ if @source == "P" && !options[:length]
85
+ raise DataError, "Track length must be specified if source is P"
86
+ end
87
+ handshake
88
+ parameters =
89
+ generate_scrobbling_parameters(true,
90
+ options.merge(:artist => artist,
91
+ :track => track,
92
+ :source => @source,
93
+ :time => (options[:time] || Time.now).utc.to_i))
94
+ status, = split_plain_text_response(post(@submission_url, parameters))
95
+ raise SubmissionError, status unless status == "OK"
96
+ end
97
+ alias_method :scrobble, :submit
98
+
99
+ # "The Now-Playing notification is a lightweight mechanism for notifying
100
+ # Last.fm that a track has started playing. This is used for realtime display
101
+ # of a user's currently playing track, and does not affect a user's musical
102
+ # profile."
103
+ #
104
+ # The artist and track are required parameters. Other parameters can be added
105
+ # as options:
106
+ #
107
+ # :length :: Length of the track in seconds
108
+ # :album :: Album title
109
+ # :track_number :: Track number
110
+ # :mb_trackid :: MusicBrainz Track ID
111
+ #
112
+ def now_playing(artist, track, options={})
113
+ handshake
114
+ parameters =
115
+ generate_scrobbling_parameters(false,
116
+ options.merge(:artist => artist, :track => track))
117
+ status, = split_plain_text_response(post(@now_playing_url, parameters))
118
+ raise SubmissionError, status unless status == "OK"
119
+ end
120
+
121
+ # Perform handshake with the API.
122
+ #
123
+ # There is usually no need to call this, as it will be called
124
+ # automatically the first time a track is scrobbled.
125
+ #
126
+ def handshake
127
+ return if @scrobble_session_id
128
+ timestamp = Time.now.utc.to_i.to_s
129
+ authentication_token = md5(secret + timestamp)
130
+ parameters = {
131
+ "hs" => "true",
132
+ "p" => "1.2.1",
133
+ "c" => CLIENT_ID,
134
+ "v" => CLIENT_VERSION,
135
+ "u" => user,
136
+ "t" => timestamp,
137
+ "a" => authentication_token,
138
+ "api_key" => api_key,
139
+ "sk" => session_key }
140
+ body = get("http://post.audioscrobbler.com/", parameters)
141
+ status,
142
+ @scrobble_session_id,
143
+ @now_playing_url,
144
+ @submission_url, =
145
+ split_plain_text_response(body)
146
+ raise HandshakeError, status unless status == "OK"
147
+ end
148
+
149
+ private
150
+ module DocHelper
151
+ def value_at(xpath)
152
+ elements[xpath].first.to_s.strip
153
+ end
154
+ end
155
+
156
+ def last_fm_web_service(parameters={})
157
+ xml = get("http://ws.audioscrobbler.com/2.0/",
158
+ signed_parameters(parameters.merge("api_key" => api_key)))
159
+ REXML::Document.new(xml).extend(DocHelper)
160
+ end
161
+
162
+ def get(url, parameters)
163
+ query_string = sort_parameters(parameters).
164
+ map{ |k, v| "#{k}=#{CGI.escape(v)}" }.
165
+ join("&")
166
+ Net::HTTP.get_response(
167
+ URI.parse(url + "?" + query_string)
168
+ ).body
169
+ end
170
+
171
+ def post(url, parameters)
172
+ Net::HTTP.post_form(
173
+ URI.parse(url),
174
+ parameters
175
+ ).body
176
+ end
177
+
178
+ def md5(s)
179
+ Digest::MD5.hexdigest(s)
180
+ end
181
+
182
+ def signed_parameters(parameters)
183
+ sorted = sort_parameters(parameters)
184
+ signature = md5(sorted.flatten.join + secret)
185
+ parameters.merge("api_sig" => signature)
186
+ end
187
+
188
+ def split_plain_text_response(body)
189
+ body.split(/\n/)
190
+ end
191
+
192
+ def sort_parameters(parameters)
193
+ parameters.map{ |k, v| [k.to_s, v.to_s] }.sort
194
+ end
195
+
196
+ def generate_scrobbling_parameters(for_scrobble, details)
197
+ mapping = {
198
+ :artist => "a",
199
+ :track => "t",
200
+ :length => "l",
201
+ :album => "b",
202
+ :track_number => "n",
203
+ :mb_trackid => "m" }
204
+ if for_scrobble
205
+ mapping.merge!(
206
+ :source => "o",
207
+ :time => "i",
208
+ :rating => "r" )
209
+ mapping.keys.each do |k|
210
+ mapping[k] << "[0]"
211
+ end
212
+ end
213
+ mapping.inject({"s" => @scrobble_session_id}){ |hash, (key, param)|
214
+ hash[param] = details[key].to_s
215
+ hash
216
+ }
217
+ end
218
+ end
metadata ADDED
@@ -0,0 +1,77 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: simple_scrobbler
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Paul Battley
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2010-07-10 00:00:00 +01:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: mocha
17
+ type: :development
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: "0"
24
+ version:
25
+ - !ruby/object:Gem::Dependency
26
+ name: fakeweb
27
+ type: :development
28
+ version_requirement:
29
+ version_requirements: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: "0"
34
+ version:
35
+ description: Scrobble tracks to Last.fm without wanting to gnaw your own arm off.
36
+ email:
37
+ - pbattley@gmail.com
38
+ executables: []
39
+
40
+ extensions: []
41
+
42
+ extra_rdoc_files: []
43
+
44
+ files:
45
+ - lib/simple_scrobbler.rb
46
+ - lib/simple_scrobbler/version.rb
47
+ - README.md
48
+ has_rdoc: true
49
+ homepage: http://github.com/threedaymonk/simple_scrobbler
50
+ licenses: []
51
+
52
+ post_install_message:
53
+ rdoc_options: []
54
+
55
+ require_paths:
56
+ - lib
57
+ required_ruby_version: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: "0"
62
+ version:
63
+ required_rubygems_version: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: "0"
68
+ version:
69
+ requirements: []
70
+
71
+ rubyforge_project:
72
+ rubygems_version: 1.3.5
73
+ signing_key:
74
+ specification_version: 3
75
+ summary: A simple Last.fm scrobbler that works
76
+ test_files: []
77
+