scrobbler-ng 2.0.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/.gitignore +11 -0
- data/MIT-LICENSE +19 -0
- data/README.rdoc +104 -0
- data/Rakefile +11 -0
- data/VERSION.yml +4 -0
- data/examples/album.rb +19 -0
- data/examples/artist.rb +13 -0
- data/examples/scrobble.rb +31 -0
- data/examples/tag.rb +11 -0
- data/examples/track.rb +6 -0
- data/examples/user.rb +14 -0
- data/lib/scrobbler/album.rb +165 -0
- data/lib/scrobbler/artist.rb +169 -0
- data/lib/scrobbler/auth.rb +47 -0
- data/lib/scrobbler/base.rb +124 -0
- data/lib/scrobbler/event.rb +105 -0
- data/lib/scrobbler/geo.rb +27 -0
- data/lib/scrobbler/helper/image.rb +52 -0
- data/lib/scrobbler/helper/streamable.rb +40 -0
- data/lib/scrobbler/library.rb +123 -0
- data/lib/scrobbler/playing.rb +49 -0
- data/lib/scrobbler/playlist.rb +45 -0
- data/lib/scrobbler/radio.rb +12 -0
- data/lib/scrobbler/rest.rb +47 -0
- data/lib/scrobbler/scrobble.rb +116 -0
- data/lib/scrobbler/session.rb +9 -0
- data/lib/scrobbler/shout.rb +21 -0
- data/lib/scrobbler/simpleauth.rb +60 -0
- data/lib/scrobbler/tag.rb +113 -0
- data/lib/scrobbler/track.rb +138 -0
- data/lib/scrobbler/user.rb +222 -0
- data/lib/scrobbler/venue.rb +67 -0
- data/lib/scrobbler.rb +34 -0
- data/tasks/jeweler.rake +18 -0
- data/tasks/rdoc.rake +7 -0
- data/tasks/tests.rake +13 -0
- data/tasks/yardoc.rake +8 -0
- data/test/fixtures/xml/album/info.xml +43 -0
- data/test/fixtures/xml/artist/fans.xml +52 -0
- data/test/fixtures/xml/artist/info.xml +58 -0
- data/test/fixtures/xml/artist/similar.xml +1004 -0
- data/test/fixtures/xml/artist/topalbums.xml +61 -0
- data/test/fixtures/xml/artist/toptags.xml +19 -0
- data/test/fixtures/xml/artist/toptracks.xml +62 -0
- data/test/fixtures/xml/auth/session.xml +7 -0
- data/test/fixtures/xml/auth/token.xml +3 -0
- data/test/fixtures/xml/event/attend.xml +3 -0
- data/test/fixtures/xml/event/attendees.xml +53 -0
- data/test/fixtures/xml/event/event.xml +35 -0
- data/test/fixtures/xml/event/shouts.xml +35 -0
- data/test/fixtures/xml/geo/events-distance-p1.xml +151 -0
- data/test/fixtures/xml/geo/events-lat-long.xml +167 -0
- data/test/fixtures/xml/geo/events-p1.xml +152 -0
- data/test/fixtures/xml/geo/events-p2.xml +380 -0
- data/test/fixtures/xml/geo/events-p3.xml +427 -0
- data/test/fixtures/xml/geo/top_artists-p1.xml +76 -0
- data/test/fixtures/xml/geo/top_tracks-p1.xml +77 -0
- data/test/fixtures/xml/library/albums-f30.xml +454 -0
- data/test/fixtures/xml/library/albums-p1.xml +754 -0
- data/test/fixtures/xml/library/albums-p2.xml +754 -0
- data/test/fixtures/xml/library/albums-p3.xml +754 -0
- data/test/fixtures/xml/library/albums-p4.xml +739 -0
- data/test/fixtures/xml/library/albums-p5.xml +754 -0
- data/test/fixtures/xml/library/albums-p6.xml +754 -0
- data/test/fixtures/xml/library/albums-p7.xml +739 -0
- data/test/fixtures/xml/library/albums-p8.xml +724 -0
- data/test/fixtures/xml/library/artists-f30.xml +334 -0
- data/test/fixtures/xml/library/artists-p1.xml +554 -0
- data/test/fixtures/xml/library/artists-p2.xml +554 -0
- data/test/fixtures/xml/library/artists-p3.xml +554 -0
- data/test/fixtures/xml/library/artists-p4.xml +554 -0
- data/test/fixtures/xml/library/artists-p5.xml +554 -0
- data/test/fixtures/xml/library/artists-p6.xml +554 -0
- data/test/fixtures/xml/library/artists-p7.xml +444 -0
- data/test/fixtures/xml/library/tracks-f30.xml +472 -0
- data/test/fixtures/xml/library/tracks-p1.xml +789 -0
- data/test/fixtures/xml/library/tracks-p10.xml +771 -0
- data/test/fixtures/xml/library/tracks-p11.xml +792 -0
- data/test/fixtures/xml/library/tracks-p12.xml +777 -0
- data/test/fixtures/xml/library/tracks-p13.xml +762 -0
- data/test/fixtures/xml/library/tracks-p14.xml +759 -0
- data/test/fixtures/xml/library/tracks-p15.xml +762 -0
- data/test/fixtures/xml/library/tracks-p16.xml +756 -0
- data/test/fixtures/xml/library/tracks-p17.xml +780 -0
- data/test/fixtures/xml/library/tracks-p18.xml +756 -0
- data/test/fixtures/xml/library/tracks-p19.xml +774 -0
- data/test/fixtures/xml/library/tracks-p2.xml +765 -0
- data/test/fixtures/xml/library/tracks-p20.xml +777 -0
- data/test/fixtures/xml/library/tracks-p21.xml +777 -0
- data/test/fixtures/xml/library/tracks-p22.xml +771 -0
- data/test/fixtures/xml/library/tracks-p23.xml +765 -0
- data/test/fixtures/xml/library/tracks-p24.xml +783 -0
- data/test/fixtures/xml/library/tracks-p25.xml +777 -0
- data/test/fixtures/xml/library/tracks-p26.xml +777 -0
- data/test/fixtures/xml/library/tracks-p27.xml +756 -0
- data/test/fixtures/xml/library/tracks-p28.xml +771 -0
- data/test/fixtures/xml/library/tracks-p29.xml +753 -0
- data/test/fixtures/xml/library/tracks-p3.xml +771 -0
- data/test/fixtures/xml/library/tracks-p30.xml +780 -0
- data/test/fixtures/xml/library/tracks-p31.xml +753 -0
- data/test/fixtures/xml/library/tracks-p32.xml +771 -0
- data/test/fixtures/xml/library/tracks-p33.xml +762 -0
- data/test/fixtures/xml/library/tracks-p34.xml +538 -0
- data/test/fixtures/xml/library/tracks-p4.xml +792 -0
- data/test/fixtures/xml/library/tracks-p5.xml +780 -0
- data/test/fixtures/xml/library/tracks-p6.xml +789 -0
- data/test/fixtures/xml/library/tracks-p7.xml +789 -0
- data/test/fixtures/xml/library/tracks-p8.xml +780 -0
- data/test/fixtures/xml/library/tracks-p9.xml +774 -0
- data/test/fixtures/xml/tag/similar.xml +254 -0
- data/test/fixtures/xml/tag/topalbums.xml +805 -0
- data/test/fixtures/xml/tag/topartists.xml +605 -0
- data/test/fixtures/xml/tag/toptags.xml +1254 -0
- data/test/fixtures/xml/tag/toptracks.xml +852 -0
- data/test/fixtures/xml/track/fans.xml +34 -0
- data/test/fixtures/xml/track/info.xml +53 -0
- data/test/fixtures/xml/track/toptags.xml +504 -0
- data/test/fixtures/xml/user/events.xml +401 -0
- data/test/fixtures/xml/user/friends.xml +30 -0
- data/test/fixtures/xml/user/lovedtracks.xml +678 -0
- data/test/fixtures/xml/user/neighbours.xml +23 -0
- data/test/fixtures/xml/user/playlists.xml +61 -0
- data/test/fixtures/xml/user/profile.xml +12 -0
- data/test/fixtures/xml/user/recentbannedtracks.xml +24 -0
- data/test/fixtures/xml/user/recentlovedtracks.xml +24 -0
- data/test/fixtures/xml/user/recenttracks.xml +124 -0
- data/test/fixtures/xml/user/systemrecs.xml +18 -0
- data/test/fixtures/xml/user/topalbums.xml +61 -0
- data/test/fixtures/xml/user/topartists.xml +41 -0
- data/test/fixtures/xml/user/toptags.xml +44 -0
- data/test/fixtures/xml/user/toptracks.xml +65 -0
- data/test/fixtures/xml/user/weeklyalbumchart.xml +256 -0
- data/test/fixtures/xml/user/weeklyartistchart.xml +220 -0
- data/test/fixtures/xml/user/weeklytrackchart.xml +746 -0
- data/test/fixtures/xml/venue/events.xml +151 -0
- data/test/fixtures/xml/venue/venue.xml +16 -0
- data/test/mocks/rest.rb +212 -0
- data/test/spec_helper.rb +7 -0
- data/test/test_helper.rb +20 -0
- data/test/unit/album_spec.rb +51 -0
- data/test/unit/artist_spec.rb +130 -0
- data/test/unit/auth_spec.rb +36 -0
- data/test/unit/event_spec.rb +109 -0
- data/test/unit/geo_spec.rb +148 -0
- data/test/unit/library_spec.rb +133 -0
- data/test/unit/playing_test.rb +53 -0
- data/test/unit/playlist_spec.rb +25 -0
- data/test/unit/radio_spec.rb +22 -0
- data/test/unit/scrobble_spec.rb +55 -0
- data/test/unit/scrobble_test.rb +69 -0
- data/test/unit/simpleauth_test.rb +45 -0
- data/test/unit/tag_spec.rb +101 -0
- data/test/unit/track_spec.rb +95 -0
- data/test/unit/user_spec.rb +264 -0
- data/test/unit/venue_spec.rb +104 -0
- metadata +253 -0
|
@@ -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
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module Scrobbler
|
|
2
|
+
class Shout < Base
|
|
3
|
+
attr_reader :author, :date, :body
|
|
4
|
+
|
|
5
|
+
class << self
|
|
6
|
+
def new_from_libxml(xml)
|
|
7
|
+
data={}
|
|
8
|
+
xml.children.each do |child|
|
|
9
|
+
data[:body] = child.content if child.name == 'body'
|
|
10
|
+
data[:author] = Scrobbler::User.new(child.content) if child.name == 'author'
|
|
11
|
+
data[:date] = Time.parse(child.content) if child.name == 'date'
|
|
12
|
+
end
|
|
13
|
+
Shout.new(data)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def initialize(input={})
|
|
18
|
+
populate_data(input)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
require 'digest/md5'
|
|
2
|
+
|
|
3
|
+
# exception definitions
|
|
4
|
+
class BadAuthError < StandardError; end
|
|
5
|
+
class BannedError < StandardError; end
|
|
6
|
+
class BadTimeError < StandardError; end
|
|
7
|
+
module Scrobbler
|
|
8
|
+
AUTH_URL = 'http://post.audioscrobbler.com'
|
|
9
|
+
AUTH_VER = '1.2.1'
|
|
10
|
+
|
|
11
|
+
# @todo This is not 2.0!!
|
|
12
|
+
class SimpleAuth
|
|
13
|
+
# you should read last.fm/api/submissions#handshake
|
|
14
|
+
|
|
15
|
+
attr_accessor :user, :password, :client_id, :client_ver
|
|
16
|
+
attr_reader :status, :session_id, :now_playing_url, :submission_url
|
|
17
|
+
|
|
18
|
+
def initialize(args = {})
|
|
19
|
+
@user = args[:user] # last.fm username
|
|
20
|
+
@password = args[:password] # last.fm password
|
|
21
|
+
@client_id = 'rbs' # Client ID assigned by last.fm; Don't change this!
|
|
22
|
+
@client_ver = '0.2.13'
|
|
23
|
+
|
|
24
|
+
raise ArgumentError, 'Missing required argument' if @user.empty? || @password.empty?
|
|
25
|
+
|
|
26
|
+
@connection = REST::Connection.new(AUTH_URL)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def handshake!
|
|
30
|
+
password_hash = Digest::MD5.hexdigest(@password)
|
|
31
|
+
timestamp = Time.now.to_i.to_s
|
|
32
|
+
token = Digest::MD5.hexdigest(password_hash + timestamp)
|
|
33
|
+
|
|
34
|
+
query = { :hs => 'true',
|
|
35
|
+
:p => AUTH_VER,
|
|
36
|
+
:c => @client_id,
|
|
37
|
+
:v => @client_ver,
|
|
38
|
+
:u => @user,
|
|
39
|
+
:t => timestamp,
|
|
40
|
+
:a => token }
|
|
41
|
+
result = @connection.get('/', query)
|
|
42
|
+
|
|
43
|
+
@status = result.split(/\n/)[0]
|
|
44
|
+
case @status
|
|
45
|
+
when /OK/
|
|
46
|
+
@session_id, @now_playing_url, @submission_url = result.split(/\n/)[1,3]
|
|
47
|
+
when /BANNED/
|
|
48
|
+
raise BannedError # something is wrong with the gem, check for an update
|
|
49
|
+
when /BADAUTH/
|
|
50
|
+
raise BadAuthError # invalid user/password
|
|
51
|
+
when /FAILED/
|
|
52
|
+
raise RequestFailedError, @status
|
|
53
|
+
when /BADTIME/
|
|
54
|
+
raise BadTimeError # system time is way off
|
|
55
|
+
else
|
|
56
|
+
raise RequestFailedError
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# Below is code samples for how to find the top albums and tracks for a tag.
|
|
2
|
+
#
|
|
3
|
+
# tag = Scrobbler::Tag.new('country')
|
|
4
|
+
#
|
|
5
|
+
# puts 'Top Albums'
|
|
6
|
+
# tag.top_albums.each { |a| puts "(#{a.count}) #{a.name} by #{a.artist}" }
|
|
7
|
+
#
|
|
8
|
+
# puts
|
|
9
|
+
#
|
|
10
|
+
# puts 'Top Tracks'
|
|
11
|
+
# tag.top_tracks.each { |t| puts "(#{t.count}) #{t.name} by #{t.artist}" }
|
|
12
|
+
#
|
|
13
|
+
# Which would output something similar to:
|
|
14
|
+
#
|
|
15
|
+
# Top Albums
|
|
16
|
+
# (29) American IV: The Man Comes Around by Johnny Cash
|
|
17
|
+
# (14) Folks Pop In at the Waterhouse by Various Artists
|
|
18
|
+
# (13) Hapless by Flowers From The Man Who Shot Your Cousin
|
|
19
|
+
# (9) Taking The Long Way by Dixie Chicks
|
|
20
|
+
# (8) Unchained by Johnny Cash
|
|
21
|
+
# (8) American III: Solitary Man by Johnny Cash
|
|
22
|
+
# (8) Wide Open Spaces by Dixie Chicks
|
|
23
|
+
# (7) It's Now or Later by Tangled Star
|
|
24
|
+
# (7) Greatest Hits by Hank Williams
|
|
25
|
+
# (7) American Recordings by Johnny Cash
|
|
26
|
+
# (6) Forgotten Landscape by theNoLifeKing
|
|
27
|
+
# (6) At Folsom Prison by Johnny Cash
|
|
28
|
+
# (6) Fox Confessor Brings the Flood by Neko Case
|
|
29
|
+
# (6) Murder by Johnny Cash
|
|
30
|
+
# (5) Gloom by theNoLifeKing
|
|
31
|
+
# (5) Set This Circus Down by Tim McGraw
|
|
32
|
+
# (5) Blacklisted by Neko Case
|
|
33
|
+
# (5) Breathe by Faith Hill
|
|
34
|
+
# (5) Unearthed (disc 4: My Mother's Hymn Book) by Johnny Cash
|
|
35
|
+
# (4) Home by Dixie Chicks
|
|
36
|
+
#
|
|
37
|
+
# Top Tracks
|
|
38
|
+
# (221) Hurt by Johnny Cash
|
|
39
|
+
# (152) I Walk the Line by Johnny Cash
|
|
40
|
+
# (147) Ring of Fire by Johnny Cash
|
|
41
|
+
# (125) Folsom Prison Blues by Johnny Cash
|
|
42
|
+
# (77) The Man Comes Around by Johnny Cash
|
|
43
|
+
# (67) Personal Jesus by Johnny Cash
|
|
44
|
+
# (65) Not Ready To Make Nice by Dixie Chicks
|
|
45
|
+
# (63) Before He Cheats by Carrie Underwood
|
|
46
|
+
# (62) Give My Love to Rose by Johnny Cash
|
|
47
|
+
# (49) Jackson by Johnny Cash
|
|
48
|
+
# (49) What Hurts The Most by Rascal Flatts
|
|
49
|
+
# (48) Big River by Johnny Cash
|
|
50
|
+
# (46) Man in Black by Johnny Cash
|
|
51
|
+
# (46) Jolene by Dolly Parton
|
|
52
|
+
# (46) Friends in Low Places by Garth Brooks
|
|
53
|
+
# (46) One by Johnny Cash
|
|
54
|
+
# (44) Cocaine Blues by Johnny Cash
|
|
55
|
+
# (41) Get Rhythm by Johnny Cash
|
|
56
|
+
# (41) I Still Miss Someone by Johnny Cash
|
|
57
|
+
# (40) The Devil Went Down to Georgia by Charlie Daniels Band
|
|
58
|
+
module Scrobbler
|
|
59
|
+
class Tag < Base
|
|
60
|
+
attr_accessor :name, :count, :url, :streamable
|
|
61
|
+
|
|
62
|
+
class << self
|
|
63
|
+
def new_from_libxml(xml)
|
|
64
|
+
data = {}
|
|
65
|
+
xml.children.each do |child|
|
|
66
|
+
data[:name] = child.content if child.name == 'name'
|
|
67
|
+
data[:count] = child.content.to_i if child.name == 'count'
|
|
68
|
+
data[:url] = child.content if child.name == 'url'
|
|
69
|
+
if child.name == 'streamable'
|
|
70
|
+
if ['1', 'true'].include?(child.content)
|
|
71
|
+
data[:streamable] = true
|
|
72
|
+
else
|
|
73
|
+
data[:streamable] = false
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
Tag.new(data[:name], data)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def initialize(name, data={})
|
|
83
|
+
raise ArgumentError, "Name is required" if name.empty?
|
|
84
|
+
@name = name
|
|
85
|
+
@url = data[:url] unless data[:url].nil?
|
|
86
|
+
@count = data[:count] unless data[:count].nil?
|
|
87
|
+
@streamable = data[:streamable] unless data[:streamable].nil?
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def top_artists(force=false)
|
|
91
|
+
get_response('tag.gettopartists', :top_artists, 'topartists', 'artist', {'tag'=>@name}, force)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def top_albums(force=false)
|
|
95
|
+
get_response('tag.gettopalbums', :top_albums, 'topalbums', 'album', {'tag'=>@name}, force)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def top_tracks(force=false)
|
|
99
|
+
get_response('tag.gettoptracks', :top_tracks, 'toptracks', 'track', {'tag'=>@name}, force)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def Tag.top_tags
|
|
103
|
+
self.get('tag.gettoptags', :toptags, :tag)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Search for tags similar to this one. Returns tags ranked by similarity,
|
|
107
|
+
# based on listening data.
|
|
108
|
+
def similar(force=false)
|
|
109
|
+
params = {:tag => @name}
|
|
110
|
+
get_response('tag.getsimilar', :similar, 'similartags', 'tag', params, force)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# Below is an example of how to get the top fans for a track.
|
|
2
|
+
#
|
|
3
|
+
# track = Scrobbler::Track.new('Carrie Underwood', 'Before He Cheats')
|
|
4
|
+
# puts 'Fans'
|
|
5
|
+
# puts "=" * 4
|
|
6
|
+
# track.fans.each { |u| puts u.username }
|
|
7
|
+
#
|
|
8
|
+
# Which would output something like:
|
|
9
|
+
#
|
|
10
|
+
# track = Scrobbler::Track.new('Carrie Underwood', 'Before He Cheats')
|
|
11
|
+
# puts 'Fans'
|
|
12
|
+
# puts "=" * 4
|
|
13
|
+
# track.fans.each { |u| puts "(#{u.weight}) #{u.username}" }
|
|
14
|
+
#
|
|
15
|
+
# Fans
|
|
16
|
+
# ====
|
|
17
|
+
# (69163) PimpinRose
|
|
18
|
+
# (7225) selene204
|
|
19
|
+
# (7000) CelestiaLegends
|
|
20
|
+
# (6817) muehllr
|
|
21
|
+
# (5387) Mudley
|
|
22
|
+
# (5368) ilovejohnny1984
|
|
23
|
+
# (5232) MeganIAD
|
|
24
|
+
# (5132) Veric
|
|
25
|
+
# (5097) aeVnar
|
|
26
|
+
# (3390) kristaaan
|
|
27
|
+
# (3239) kelseaowns
|
|
28
|
+
# (2780) syndication
|
|
29
|
+
# (2735) mkumm
|
|
30
|
+
# (2706) Kimmybeebee
|
|
31
|
+
# (2648) skorpcroze
|
|
32
|
+
# (2549) mistergreg
|
|
33
|
+
# (2449) mlmjcace
|
|
34
|
+
# (2302) tiNEey
|
|
35
|
+
# (2169) ajsbabiegirl
|
|
36
|
+
module Scrobbler
|
|
37
|
+
class Track < Base
|
|
38
|
+
# Load Helper modules
|
|
39
|
+
include ImageObjectFuncs
|
|
40
|
+
extend ImageClassFuncs
|
|
41
|
+
|
|
42
|
+
attr_accessor :artist, :name, :mbid, :playcount, :rank, :url, :id, :count
|
|
43
|
+
attr_accessor :streamable, :album, :date, :now_playing, :tagcount
|
|
44
|
+
attr_accessor :duration, :listeners
|
|
45
|
+
|
|
46
|
+
class << self
|
|
47
|
+
def new_from_libxml(xml)
|
|
48
|
+
data = {}
|
|
49
|
+
xml.children.each do |child|
|
|
50
|
+
data[:name] = child.content if child.name == 'name'
|
|
51
|
+
data[:mbid] = child.content if child.name == 'mbid'
|
|
52
|
+
data[:url] = child.content if child.name == 'url'
|
|
53
|
+
data[:date] = Time.parse(child.content) if child.name == 'date'
|
|
54
|
+
data[:artist] = Artist.new_from_libxml(child) if child.name == 'artist'
|
|
55
|
+
data[:album] = Album.new_from_libxml(child) if child.name == 'album'
|
|
56
|
+
data[:playcount] = child.content.to_i if child.name == 'playcount'
|
|
57
|
+
data[:tagcount] = child.content.to_i if child.name == 'tagcount'
|
|
58
|
+
maybe_image_node(data, child)
|
|
59
|
+
if child.name == 'streamable'
|
|
60
|
+
if ['1', 'true'].include?(child.content)
|
|
61
|
+
data[:streamable] = true
|
|
62
|
+
else
|
|
63
|
+
data[:streamable] = false
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
data[:rank] = xml['rank'].to_i if xml['rank']
|
|
70
|
+
data[:now_playing] = true if xml['nowplaying'] && xml['nowplaying'] == 'true'
|
|
71
|
+
|
|
72
|
+
data[:now_playing] = false if data[:now_playing].nil?
|
|
73
|
+
|
|
74
|
+
Track.new(data[:artist], data[:name], data)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def initialize(artist, name, data={})
|
|
79
|
+
raise ArgumentError, "Artist is required" if artist.nil?
|
|
80
|
+
raise ArgumentError, "Name is required" if name.empty?
|
|
81
|
+
@artist = artist
|
|
82
|
+
@name = name
|
|
83
|
+
populate_data(data)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def add_tags(tags)
|
|
87
|
+
# This function requires authentication, but SimpleAuth is not yet 2.0
|
|
88
|
+
raise NotImplementedError
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def ban
|
|
92
|
+
# This function requires authentication, but SimpleAuth is not yet 2.0
|
|
93
|
+
raise NotImplementedError
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
@info_loaded = false
|
|
97
|
+
def load_info
|
|
98
|
+
return nil if @info_loaded
|
|
99
|
+
doc = Base.request('track.getinfo', :artist => @artist.name, :track => @name)
|
|
100
|
+
doc.root.children.each do |childL1|
|
|
101
|
+
next unless childL1.name == 'track'
|
|
102
|
+
childL1.children.each do |child|
|
|
103
|
+
@id = child.content.to_i if child.name == 'id'
|
|
104
|
+
@mbid = child.content if child.name == 'mbid'
|
|
105
|
+
@duration = child.content.to_i if child.name == 'duration'
|
|
106
|
+
@url = child.content if child.name == 'url'
|
|
107
|
+
if child.name == 'streamable'
|
|
108
|
+
if ['1', 'true'].include?(child.content)
|
|
109
|
+
@streamable = true
|
|
110
|
+
else
|
|
111
|
+
@streamable = false
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
@listeners = child.content.to_i if child.name == 'listeners'
|
|
115
|
+
@playcount = child.content.to_i if child.name == 'playcount'
|
|
116
|
+
@artist = Artist.new_from_libxml(child) if child.name == 'artist'
|
|
117
|
+
@album = Album.new_from_libxml(child) if child.name == 'album'
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
@info_loaded = true
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def top_fans(force=false)
|
|
124
|
+
get_response('track.gettopfans', :fans, 'topfans', 'user', {:artist=>@artist.name, :track=>@name}, force)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def top_tags(force=false)
|
|
128
|
+
get_response('track.gettoptags', :top_tags, 'toptags', 'tag', {:artist=>@artist.name.to_s, :track=>@name}, force)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def ==(otherTrack)
|
|
132
|
+
if otherTrack.is_a?(Scrobbler::Track)
|
|
133
|
+
return ((@name == otherTrack.name) && (@artist == otherTrack.artist))
|
|
134
|
+
end
|
|
135
|
+
false
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# Probably the most common use of this lib would be to get your most recent tracks or your top tracks. Below are some code samples.
|
|
2
|
+
# user = Scrobbler::User.new('jnunemaker')
|
|
3
|
+
#
|
|
4
|
+
# puts "#{user.username}'s Recent Tracks"
|
|
5
|
+
# puts "=" * (user.username.length + 16)
|
|
6
|
+
# user.recent_tracks.each { |t| puts t.name }
|
|
7
|
+
#
|
|
8
|
+
# puts
|
|
9
|
+
# puts
|
|
10
|
+
#
|
|
11
|
+
# puts "#{user.username}'s Top Tracks"
|
|
12
|
+
# puts "=" * (user.username.length + 13)
|
|
13
|
+
# user.top_tracks.each { |t| puts "(#{t.playcount}) #{t.name}" }
|
|
14
|
+
#
|
|
15
|
+
# Which would output something like:
|
|
16
|
+
#
|
|
17
|
+
# jnunemaker's Recent Tracks
|
|
18
|
+
# ==========================
|
|
19
|
+
# Everything You Want
|
|
20
|
+
# You're a God
|
|
21
|
+
# Bitter Sweet Symphony [Original Version]
|
|
22
|
+
# Lord I Guess I'll Never Know
|
|
23
|
+
# Country Song
|
|
24
|
+
# Bitter Sweet Symphony (Radio Edit)
|
|
25
|
+
#
|
|
26
|
+
#
|
|
27
|
+
# jnunemaker's Top Tracks
|
|
28
|
+
# =======================
|
|
29
|
+
# (62) Probably Wouldn't Be This Way
|
|
30
|
+
# (55) Not Ready To Make Nice
|
|
31
|
+
# (45) Easy Silence
|
|
32
|
+
# (43) Song 2
|
|
33
|
+
# (40) Everybody Knows
|
|
34
|
+
# (39) Before He Cheats
|
|
35
|
+
# (39) Something's Gotta Give
|
|
36
|
+
# (38) Hips Don't Lie (featuring Wyclef Jean)
|
|
37
|
+
# (37) Unwritten
|
|
38
|
+
# (37) Move Along
|
|
39
|
+
# (37) Dance, Dance
|
|
40
|
+
# (36) We Belong Together
|
|
41
|
+
# (36) Jesus, Take the Wheel
|
|
42
|
+
# (36) Black Horse and the Cherry Tree (radio version)
|
|
43
|
+
# (35) Photograph
|
|
44
|
+
# (35) You're Beautiful
|
|
45
|
+
# (35) Walk Away
|
|
46
|
+
# (34) Stickwitu
|
|
47
|
+
module Scrobbler
|
|
48
|
+
class User < Base
|
|
49
|
+
# Load Helper modules
|
|
50
|
+
include ImageObjectFuncs
|
|
51
|
+
extend ImageClassFuncs
|
|
52
|
+
|
|
53
|
+
attr_reader :username, :url, :weight, :match, :realname, :name
|
|
54
|
+
|
|
55
|
+
class << self
|
|
56
|
+
def new_from_libxml(xml)
|
|
57
|
+
data = {}
|
|
58
|
+
xml.children.each do |child|
|
|
59
|
+
data[:name] = child.content if child.name == 'name'
|
|
60
|
+
data[:url] = child.content if child.name == 'url'
|
|
61
|
+
data[:weight] = child.content.to_i if child.name == 'weight'
|
|
62
|
+
data[:match] = child.content if child.name == 'match'
|
|
63
|
+
data[:realname] = child.content if child.name == 'realname'
|
|
64
|
+
maybe_image_node(data, child)
|
|
65
|
+
end
|
|
66
|
+
User.new(data[:name], data)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def find(*args)
|
|
70
|
+
options = {:include_profile => false}
|
|
71
|
+
options.merge!(args.pop) if args.last.is_a?(Hash)
|
|
72
|
+
users = args.flatten.inject([]) { |users, u| users << User.new(u, options); users }
|
|
73
|
+
users.length == 1 ? users.pop : users
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def initialize(username, input={})
|
|
78
|
+
data = {:include_profile => false}.merge(input)
|
|
79
|
+
raise ArgumentError if username.empty?
|
|
80
|
+
@username = username
|
|
81
|
+
@name = @username
|
|
82
|
+
load_profile() if data[:include_profile]
|
|
83
|
+
populate_data(data)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Get a list of upcoming events that this user is attending.
|
|
87
|
+
#
|
|
88
|
+
# Supports ical, ics or rss as its format
|
|
89
|
+
def events(force=false)
|
|
90
|
+
get_response('user.getevents', :events, 'events', 'event', {'user'=>@username}, force)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Get a list of the user's friends on Last.fm.
|
|
94
|
+
def friends(force=false, page=1, limit=50)
|
|
95
|
+
get_response('user.getfriends', :friends, 'friends', 'user', {'user'=>@username, 'page'=>page.to_s, 'limit'=>limit.to_s}, force)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Get information about a user profile.
|
|
99
|
+
def load_info
|
|
100
|
+
# This function requires authentication, but SimpleAuth is not yet 2.0
|
|
101
|
+
raise NotImplementedError
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Get the last 50 tracks loved by a user.
|
|
105
|
+
def loved_tracks(force=false)
|
|
106
|
+
get_response('user.getlovedtracks', :loved_tracks, 'lovedtracks', 'track', {'user'=>@username}, force)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Get a list of a user's neighbours on Last.fm.
|
|
110
|
+
def neighbours(force=false)
|
|
111
|
+
get_response('user.getneighbours', :neighbours, 'neighbours', 'user', {'user'=>@username}, force)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Get a paginated list of all events a user has attended in the past.
|
|
115
|
+
def past_events(format=:ics)
|
|
116
|
+
# This needs a Event class, which is yet not available
|
|
117
|
+
raise NotImplementedError
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Get a list of a user's playlists on Last.fm.
|
|
121
|
+
def playlists(force=false)
|
|
122
|
+
#(api_method, instance_name, parent, element, parameters, force=false)
|
|
123
|
+
get_response('user.getplaylists', :playlist, 'playlists', 'playlist', {'user'=>@username}, force)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Get a list of the recent tracks listened to by this user. Indicates now
|
|
127
|
+
# playing track if the user is currently listening.
|
|
128
|
+
#
|
|
129
|
+
# Possible parameters:
|
|
130
|
+
# - limit: An integer used to limit the number of tracks returned.
|
|
131
|
+
def recent_tracks(force=false, parameters={})
|
|
132
|
+
parameters.merge!({'user' => @username})
|
|
133
|
+
get_response('user.getrecenttracks', :recent_tracks, 'recenttracks', 'track', parameters, force)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Get Last.fm artist recommendations for a user
|
|
137
|
+
def recommended_artists
|
|
138
|
+
# This function require authentication, but SimpleAuth is not yet 2.0
|
|
139
|
+
raise NotImplementedError
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Get a paginated list of all events recommended to a user by Last.fm,
|
|
143
|
+
# based on their listening profile.
|
|
144
|
+
def recommended_events
|
|
145
|
+
# This function require authentication, but SimpleAuth is not yet 2.0
|
|
146
|
+
raise NotImplementedError
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Get shouts for this user.
|
|
150
|
+
def shouts
|
|
151
|
+
# This needs a Shout class which is yet not available
|
|
152
|
+
raise NotImplementedError
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Get the top albums listened to by a user. You can stipulate a time period.
|
|
156
|
+
# Sends the overall chart by default.
|
|
157
|
+
def top_albums(force=false, period='overall')
|
|
158
|
+
get_response('user.gettopalbums', :top_albums, 'topalbums', 'album', {'user'=>@username, 'period'=>period}, force)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Get the top artists listened to by a user. You can stipulate a time
|
|
162
|
+
# period. Sends the overall chart by default.
|
|
163
|
+
def top_artists(force=false, period='overall')
|
|
164
|
+
get_response('user.gettopartists', :top_artists, 'topartists', 'artist', {'user' => @username, 'period'=>period}, force)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Get the top tags used by this user.
|
|
168
|
+
def top_tags(force=false)
|
|
169
|
+
get_response('user.gettoptags', :top_tags, 'toptags', 'tag', {'user'=>@username}, force)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Get the top tracks listened to by a user. You can stipulate a time period.
|
|
173
|
+
# Sends the overall chart by default.
|
|
174
|
+
def top_tracks(force=false, period='overall')
|
|
175
|
+
get_response('user.gettoptracks', :top_tracks, 'toptracks', 'track', {'user'=>@username, 'period'=>period}, force)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Get an album chart for a user profile, for a given date range. If no date
|
|
179
|
+
# range is supplied, it will return the most recent album chart for this
|
|
180
|
+
# user.
|
|
181
|
+
def weekly_album_chart(from=nil, to=nil)
|
|
182
|
+
parameters = {'user' => @username}
|
|
183
|
+
parameters['from'] = from unless from.nil?
|
|
184
|
+
parameters['to'] = to unless to.nil?
|
|
185
|
+
get_response('user.getweeklyalbumchart', nil, 'weeklyalbumchart', 'album', parameters, true)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Get an artist chart for a user profile, for a given date range. If no date
|
|
189
|
+
# range is supplied, it will return the most recent artist chart for this
|
|
190
|
+
# user.
|
|
191
|
+
def weekly_artist_chart(from=nil, to=nil)
|
|
192
|
+
parameters = {'user' => @username}
|
|
193
|
+
parameters['from'] = from unless from.nil?
|
|
194
|
+
parameters['to'] = to unless to.nil?
|
|
195
|
+
get_response('user.getweeklyartistchart', nil, 'weeklyartistchart', 'artist', parameters, true)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Get a list of available charts for this user, expressed as date ranges
|
|
199
|
+
# which can be sent to the chart services.
|
|
200
|
+
def weekly_chart_list(force=false)
|
|
201
|
+
# @todo
|
|
202
|
+
raise NotImplementedError
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# Get a track chart for a user profile, for a given date range. If no date
|
|
206
|
+
# range is supplied, it will return the most recent track chart for this
|
|
207
|
+
# user.
|
|
208
|
+
def weekly_track_chart(from=nil, to=nil)
|
|
209
|
+
parameters = {'user' => @username}
|
|
210
|
+
parameters['from'] = from unless from.nil?
|
|
211
|
+
parameters['to'] = to unless to.nil?
|
|
212
|
+
get_response('user.getweeklytrackchart', nil, 'weeklytrackchart', 'track', parameters, true)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Shout on this user's shoutbox
|
|
216
|
+
def shout(message)
|
|
217
|
+
# This function require authentication, but SimpleAuth is not yet 2.0
|
|
218
|
+
raise NotImplementedError
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
end
|
|
222
|
+
end
|