simple_scrobbler 0.1.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.
- data/README.md +54 -0
- data/lib/simple_scrobbler/version.rb +3 -0
- data/lib/simple_scrobbler.rb +218 -0
- metadata +77 -0
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,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
|
+
|