audioscrobbler 0.0.1
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/COPYING +340 -0
- data/INSTALL +232 -0
- data/MANIFEST +9 -0
- data/README +6 -0
- data/audioscrobbler.gemspec +16 -0
- data/lib/audioscrobbler.rb +475 -0
- data/setup.rb +1551 -0
- data/test/test_audioscrobbler.rb +266 -0
- data/test/test_queue.rb +86 -0
- metadata +51 -0
@@ -0,0 +1,266 @@
|
|
1
|
+
#!/usr/bin/ruby -w
|
2
|
+
#
|
3
|
+
# = Name
|
4
|
+
# TestAudioscrobbler
|
5
|
+
#
|
6
|
+
# == Description
|
7
|
+
# This file contains a regression test for the Audioscrobbler class, in the
|
8
|
+
# unfortunate form of a small, useless implementation of an Audioscrobbler
|
9
|
+
# track submission server. :(
|
10
|
+
#
|
11
|
+
# == Author
|
12
|
+
# Daniel Erat <dan-ruby@erat.org>
|
13
|
+
#
|
14
|
+
# == Copyright
|
15
|
+
# Copyright 2005 Daniel Erat
|
16
|
+
#
|
17
|
+
# == License
|
18
|
+
# GNU GPL; see COPYING
|
19
|
+
|
20
|
+
require 'audioscrobbler'
|
21
|
+
require 'cgi'
|
22
|
+
require 'md5'
|
23
|
+
require 'test/unit'
|
24
|
+
require 'thread'
|
25
|
+
require 'time'
|
26
|
+
require 'webrick'
|
27
|
+
|
28
|
+
# Stores the outcome of handshakes received by a
|
29
|
+
# AudioscrobblerHandshakeServlet object.
|
30
|
+
class AudioscrobblerHandshakeStatus
|
31
|
+
def initialize
|
32
|
+
clear
|
33
|
+
end
|
34
|
+
attr_accessor :attempts, :successes, :failures
|
35
|
+
|
36
|
+
def clear
|
37
|
+
@attempts = 0
|
38
|
+
@successes = 0
|
39
|
+
@failures = 0
|
40
|
+
self
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
|
45
|
+
# Implements the handshake portion of the Audioscrobbler protocol.
|
46
|
+
class AudioscrobblerHandshakeServlet < WEBrick::HTTPServlet::AbstractServlet
|
47
|
+
def initialize(server, status, user, challenge, submit_url,
|
48
|
+
plugin_name='tst', plugin_version=0.1)
|
49
|
+
@status = status
|
50
|
+
@user = user
|
51
|
+
@challenge = challenge
|
52
|
+
@submit_url = submit_url
|
53
|
+
@plugin_name = plugin_name
|
54
|
+
@plugin_version = plugin_version
|
55
|
+
@failure_interval = 1
|
56
|
+
end
|
57
|
+
|
58
|
+
def do_GET(req, res)
|
59
|
+
@status.attempts += 1
|
60
|
+
res['Content-Type'] = "text/plain"
|
61
|
+
|
62
|
+
# Make sure that all of the parameters were supplied and match what we
|
63
|
+
# were expecting.
|
64
|
+
begin
|
65
|
+
if req.query['hs'] != 'true'
|
66
|
+
throw "FAILED Handshake not requested"
|
67
|
+
elsif req.query['p'] != '1.1'
|
68
|
+
throw "FAILED Wrong or missing protocol version"
|
69
|
+
elsif req.query['c'] != @plugin_name
|
70
|
+
throw "FAILED Wrong or missing plugin name"
|
71
|
+
elsif not req.query['v']
|
72
|
+
throw "FAILED Missing plugin version"
|
73
|
+
elsif req.query['u'] != @user
|
74
|
+
throw "BADUSER"
|
75
|
+
end
|
76
|
+
|
77
|
+
# TODO(derat): Test UPDATE responses?
|
78
|
+
@status.successes += 1
|
79
|
+
res.body = "UPTODATE\n#@challenge\n#@submit_url\nINTERVAL 0"
|
80
|
+
|
81
|
+
rescue RuntimeError
|
82
|
+
@status.failures += 1
|
83
|
+
res.body = "#{$!.message}\nINTERVAL #@failure_interval"
|
84
|
+
end
|
85
|
+
end # do_GET
|
86
|
+
end # AudioscrobblerHandshakeServlet
|
87
|
+
|
88
|
+
|
89
|
+
# Stores a submitted track. Used by AudioscrobblerSubmitStatus.
|
90
|
+
class Track
|
91
|
+
def initialize(artist=nil, title=nil, length=nil, start_time=nil,
|
92
|
+
album=nil, mbid=nil)
|
93
|
+
@artist = artist
|
94
|
+
@title = title
|
95
|
+
@length = length
|
96
|
+
@start_time = start_time
|
97
|
+
@album = album
|
98
|
+
@mbid = mbid
|
99
|
+
end
|
100
|
+
attr_accessor :artist, :title, :length, :start_time, :album, :mbid
|
101
|
+
|
102
|
+
def ==(other)
|
103
|
+
other.class == Track and @artist == other.artist and
|
104
|
+
@title == other.title and @length == other.length and
|
105
|
+
@start_time == other.start_time and @album == other.album and
|
106
|
+
@mbid == other.mbid
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
|
111
|
+
# Stores the outcome of track submissions received by a
|
112
|
+
# AudioscrobblerSubmitServlet object.
|
113
|
+
class AudioscrobblerSubmitStatus
|
114
|
+
def initialize
|
115
|
+
clear
|
116
|
+
end
|
117
|
+
attr_accessor :attempts, :successes, :failures, :tracks
|
118
|
+
|
119
|
+
def clear
|
120
|
+
@attempts = 0
|
121
|
+
@successes = 0
|
122
|
+
@failures = 0
|
123
|
+
@tracks = []
|
124
|
+
self
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
|
129
|
+
# Implements the track submission portion of the Audioscrobbler protocol.
|
130
|
+
class AudioscrobblerSubmitServlet < WEBrick::HTTPServlet::AbstractServlet
|
131
|
+
def initialize(server, status, user, password, challenge)
|
132
|
+
@status = status
|
133
|
+
@user = user
|
134
|
+
@md5 = MD5.hexdigest(MD5.hexdigest(password) + challenge)
|
135
|
+
@failure_interval = 1
|
136
|
+
end
|
137
|
+
|
138
|
+
# POST is supposed to be used rather than GET.
|
139
|
+
def do_GET(req, res)
|
140
|
+
@status.attempts += 1
|
141
|
+
@status.failures += 1
|
142
|
+
res.body = "FAILED Use POST, not GET\nINTERVAL #@failure_interval"
|
143
|
+
end
|
144
|
+
|
145
|
+
def do_POST(req, res)
|
146
|
+
@status.attempts += 1
|
147
|
+
res['Content-Type'] = "text/plain"
|
148
|
+
|
149
|
+
begin
|
150
|
+
# Make sure that they authenticated correctly.
|
151
|
+
if req.query['u'] != @user
|
152
|
+
raise "BADAUTH"
|
153
|
+
elsif req.query['s'] != @md5
|
154
|
+
raise "BADAUTH"
|
155
|
+
end
|
156
|
+
|
157
|
+
# Handle the track parameters.
|
158
|
+
tracks = []
|
159
|
+
req.query.each_pair do |k, v|
|
160
|
+
if k =~ /^([atbmli])\[(\d+)\]$/
|
161
|
+
track = (tracks[$2.to_i] ||= Track.new)
|
162
|
+
v = CGI.unescape(v)
|
163
|
+
case
|
164
|
+
when $1 == 'a': track.artist = v
|
165
|
+
when $1 == 't': track.title = v
|
166
|
+
when $1 == 'b': track.album = v
|
167
|
+
when $1 == 'm': track.mbid = v
|
168
|
+
when $1 == 'l': track.length = v.to_i
|
169
|
+
when $1 == 'i':
|
170
|
+
# We get start times in a strange format, but it's more
|
171
|
+
# convenient to store them as seconds since the epoch.
|
172
|
+
v =~ /^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})$/
|
173
|
+
track.start_time = Time.gm($1, $2, $3, $4, $5, $6).to_i
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
# Make sure that no data was missing from the submitted tracks.
|
179
|
+
tracks.each do |track|
|
180
|
+
if not track
|
181
|
+
raise "FAILED Missing track"
|
182
|
+
elsif not track.artist or not track.title or not track.album or
|
183
|
+
not track.mbid or not track.length or not track.start_time
|
184
|
+
raise "FAILED Missing parameter"
|
185
|
+
elsif track.artist.length == 0 or track.title.length == 0 or
|
186
|
+
track.length == 0 or track.start_time == 0
|
187
|
+
raise "FAILED Empty required parameter"
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
# Make sure that we didn't get too few or too many tracks.
|
192
|
+
if tracks.length == 0
|
193
|
+
raise "FAILED No tracks supplied"
|
194
|
+
elsif tracks.length > 10
|
195
|
+
raise "FAILED More than 10 tracks supplied"
|
196
|
+
end
|
197
|
+
|
198
|
+
# If we get here, then we didn't find any problems.
|
199
|
+
@status.tracks += tracks
|
200
|
+
@status.successes += 1
|
201
|
+
res.body = "OK\nINTERVAL 0"
|
202
|
+
|
203
|
+
# If we threw an exception, return an error.
|
204
|
+
rescue RuntimeError
|
205
|
+
@status.failures += 1
|
206
|
+
res.body = "#{$!.message}\nINTERVAL #@failure_interval"
|
207
|
+
end
|
208
|
+
end # do_POST
|
209
|
+
end # AudioscrobblerSubmitServlet
|
210
|
+
|
211
|
+
|
212
|
+
# Regression test for the Audioscrobbler class.
|
213
|
+
class TestAudioscrobbler < Test::Unit::TestCase
|
214
|
+
def test_audioscrobbler
|
215
|
+
@handshake_status = AudioscrobblerHandshakeStatus.new
|
216
|
+
@submit_status = AudioscrobblerSubmitStatus.new
|
217
|
+
# FIXME(derat): Is there some better way to choose a port here?
|
218
|
+
@http_port = 16349
|
219
|
+
|
220
|
+
@server_thread = Thread.new do
|
221
|
+
s = WEBrick::HTTPServer.new(:BindAddress => "127.0.0.1",
|
222
|
+
:Port => @http_port)
|
223
|
+
s.mount("/handshake", AudioscrobblerHandshakeServlet, @handshake_status,
|
224
|
+
"username", "challenge", "http://127.0.0.1:#@http_port/submit")
|
225
|
+
s.mount("/submit", AudioscrobblerSubmitServlet, @submit_status,
|
226
|
+
"username", "password", "challenge")
|
227
|
+
trap("INT") { s.shutdown }
|
228
|
+
s.start
|
229
|
+
end
|
230
|
+
|
231
|
+
a = Audioscrobbler.new("username", "password")
|
232
|
+
a.client = "tst"
|
233
|
+
a.version = "1.1"
|
234
|
+
a.handshake_uri = "http://127.0.0.1:#@http_port/handshake"
|
235
|
+
a.verbose = true
|
236
|
+
|
237
|
+
a.start_submitter_thread
|
238
|
+
|
239
|
+
tracks = []
|
240
|
+
tracks.push(Track.new("Beck", "Devil's Haircut", 100, 1128285297,
|
241
|
+
"Odelay", ''))
|
242
|
+
tracks.push(Track.new("Faith No More", "Midlife Crisis", 100, 1128285297,
|
243
|
+
"Angel Dust", ''))
|
244
|
+
tracks.push(Track.new("Hans Zimmer", "Greed", 100, 1128285297,
|
245
|
+
"Broken Arrow", ''))
|
246
|
+
tracks.push(Track.new("M�m", "Sleepswim", 100, 1128285297,
|
247
|
+
"Finally We Are No One", ''))
|
248
|
+
|
249
|
+
tracks.each do |t|
|
250
|
+
a.enqueue(t.artist, t.title, t.length, t.start_time, t.album, t.mbid)
|
251
|
+
end
|
252
|
+
|
253
|
+
# FIXME(derat): This is awful. I should add functionality to
|
254
|
+
# Audioscrobbler.enqueue to block until an attempt has been made to
|
255
|
+
# submit the just-enqueued track.
|
256
|
+
sleep 3
|
257
|
+
|
258
|
+
assert_equal(@handshake_status.failures, 0)
|
259
|
+
assert(@handshake_status.successes > 0)
|
260
|
+
|
261
|
+
assert_equal(@submit_status.failures, 0)
|
262
|
+
assert(@submit_status.successes > 0)
|
263
|
+
|
264
|
+
assert_equal(@submit_status.tracks, tracks)
|
265
|
+
end # test_audioscrobbler
|
266
|
+
end # TestAudioscrobbler
|
data/test/test_queue.rb
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
#!/usr/bin/ruby -w
|
2
|
+
#
|
3
|
+
# = Name
|
4
|
+
# TestPlayedTrack, TestSubmissionQueue
|
5
|
+
#
|
6
|
+
# == Description
|
7
|
+
# This file contains unit tests for the PlayedTrack and SubmissionQueue
|
8
|
+
# components of the Audioscrobbler class.
|
9
|
+
#
|
10
|
+
# == Author
|
11
|
+
# Daniel Erat <dan-ruby@erat.org>
|
12
|
+
#
|
13
|
+
# == Copyright
|
14
|
+
# Copyright 2005 Daniel Erat
|
15
|
+
#
|
16
|
+
# == License
|
17
|
+
# GNU GPL; see COPYING
|
18
|
+
|
19
|
+
require 'audioscrobbler'
|
20
|
+
require 'tempfile'
|
21
|
+
require 'test/unit'
|
22
|
+
|
23
|
+
class Audioscrobbler
|
24
|
+
class SubmissionQueue
|
25
|
+
class TestPlayedTrack < Test::Unit::TestCase
|
26
|
+
def test_members
|
27
|
+
t = PlayedTrack.new(
|
28
|
+
'Cylob', 'Stomping FM', 100, 1128285297, 'Lobster Tracks', nil)
|
29
|
+
assert_equal(t.artist, 'Cylob')
|
30
|
+
assert_equal(t.title, 'Stomping FM')
|
31
|
+
assert_equal(t.length, 100)
|
32
|
+
assert_equal(t.start_time, 1128285297)
|
33
|
+
assert_equal(t.album, 'Lobster Tracks')
|
34
|
+
assert_equal(t.mbid, '') # nil should be converted to ''
|
35
|
+
end
|
36
|
+
|
37
|
+
def test_serialization
|
38
|
+
t = PlayedTrack.new(
|
39
|
+
'Katamari Damacy', 'WANDA WANDA', 100, 1128285297, 'OST', 'blah')
|
40
|
+
assert_equal(t,
|
41
|
+
PlayedTrack.deserialize(t.serialize))
|
42
|
+
|
43
|
+
t = PlayedTrack.new(
|
44
|
+
'Def Leppard', 'Photograph', 100, 1128285297, nil, 'blah')
|
45
|
+
assert_equal(t,
|
46
|
+
PlayedTrack.deserialize(t.serialize))
|
47
|
+
end
|
48
|
+
end # class TestPlayedTrack
|
49
|
+
end # class SubmissionQueue
|
50
|
+
|
51
|
+
class TestSubmissionQueue < Test::Unit::TestCase
|
52
|
+
def setup
|
53
|
+
file = Tempfile.new('audioscrobbler_test_queue')
|
54
|
+
@filename = file.path
|
55
|
+
file.close
|
56
|
+
end
|
57
|
+
|
58
|
+
def test_it
|
59
|
+
q = SubmissionQueue.new(@filename)
|
60
|
+
|
61
|
+
q.append('Kraftwerk', 'Computer Love', 100, 1128285297,
|
62
|
+
'Computer World', nil)
|
63
|
+
q.append('Iron Maiden', 'Aces High', 100, 1128285297,
|
64
|
+
'Powerslave', nil)
|
65
|
+
q.append('Herbie Hancock', 'Cantaloupe Island', 100, 1128285297,
|
66
|
+
'Empyrean Isles', nil)
|
67
|
+
|
68
|
+
# Create a new queue, pointing at the same backup file.
|
69
|
+
q = SubmissionQueue.new(@filename)
|
70
|
+
|
71
|
+
tracks = q.peek(1)
|
72
|
+
assert_equal(tracks.length, 1)
|
73
|
+
assert_equal(tracks[0].artist, 'Kraftwerk')
|
74
|
+
|
75
|
+
q.delete(1)
|
76
|
+
tracks = q.peek(5)
|
77
|
+
assert_equal(tracks.length, 2)
|
78
|
+
assert_equal(tracks[0].artist, 'Iron Maiden')
|
79
|
+
assert_equal(tracks[1].artist, 'Herbie Hancock')
|
80
|
+
end
|
81
|
+
|
82
|
+
def teardown
|
83
|
+
File.delete(@filename) if File.exists?(@filename)
|
84
|
+
end
|
85
|
+
end # class TestSubmissionQueue
|
86
|
+
end # class AudioScrobbler
|
metadata
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
rubygems_version: 0.8.11
|
3
|
+
specification_version: 1
|
4
|
+
name: audioscrobbler
|
5
|
+
version: !ruby/object:Gem::Version
|
6
|
+
version: 0.0.1
|
7
|
+
date: 2005-10-02 00:00:00 -07:00
|
8
|
+
summary: Library to submit music playlists to Last.fm
|
9
|
+
require_paths:
|
10
|
+
- lib
|
11
|
+
email: dan-ruby@erat.org
|
12
|
+
homepage: http://www.erat.org/ruby/
|
13
|
+
rubyforge_project:
|
14
|
+
description:
|
15
|
+
autorequire: audioscrobbler
|
16
|
+
default_executable:
|
17
|
+
bindir: bin
|
18
|
+
has_rdoc: true
|
19
|
+
required_ruby_version: !ruby/object:Gem::Version::Requirement
|
20
|
+
requirements:
|
21
|
+
-
|
22
|
+
- ">"
|
23
|
+
- !ruby/object:Gem::Version
|
24
|
+
version: 0.0.0
|
25
|
+
version:
|
26
|
+
platform: ruby
|
27
|
+
signing_key:
|
28
|
+
cert_chain:
|
29
|
+
authors:
|
30
|
+
- Daniel Erat
|
31
|
+
files:
|
32
|
+
- MANIFEST
|
33
|
+
- COPYING
|
34
|
+
- INSTALL
|
35
|
+
- setup.rb
|
36
|
+
- README
|
37
|
+
- lib
|
38
|
+
- test
|
39
|
+
- audioscrobbler.gemspec
|
40
|
+
- lib/audioscrobbler.rb
|
41
|
+
- test/test_audioscrobbler.rb
|
42
|
+
- test/test_queue.rb
|
43
|
+
test_files:
|
44
|
+
- test/test_audioscrobbler.rb
|
45
|
+
- test/test_queue.rb
|
46
|
+
rdoc_options: []
|
47
|
+
extra_rdoc_files: []
|
48
|
+
executables: []
|
49
|
+
extensions: []
|
50
|
+
requirements: []
|
51
|
+
dependencies: []
|