titanous-scrobbler 0.2.2
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/History.txt +3 -0
- data/MIT-LICENSE +19 -0
- data/Manifest +67 -0
- data/README.rdoc +107 -0
- data/Rakefile +36 -0
- data/examples/album.rb +17 -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 +143 -0
- data/lib/scrobbler/artist.rb +127 -0
- data/lib/scrobbler/base.rb +29 -0
- data/lib/scrobbler/chart.rb +31 -0
- data/lib/scrobbler/playing.rb +49 -0
- data/lib/scrobbler/rest.rb +47 -0
- data/lib/scrobbler/scrobble.rb +66 -0
- data/lib/scrobbler/simpleauth.rb +59 -0
- data/lib/scrobbler/tag.rb +104 -0
- data/lib/scrobbler/track.rb +96 -0
- data/lib/scrobbler/user.rb +192 -0
- data/lib/scrobbler/version.rb +3 -0
- data/lib/scrobbler.rb +20 -0
- data/scrobbler.gemspec +41 -0
- data/setup.rb +1585 -0
- data/test/fixtures/xml/album/info.xml +70 -0
- data/test/fixtures/xml/artist/fans.xml +18 -0
- data/test/fixtures/xml/artist/similar.xml +39 -0
- data/test/fixtures/xml/artist/topalbums.xml +36 -0
- data/test/fixtures/xml/artist/toptags.xml +18 -0
- data/test/fixtures/xml/artist/toptracks.xml +27 -0
- data/test/fixtures/xml/tag/topalbums.xml +39 -0
- data/test/fixtures/xml/tag/topartists.xml +39 -0
- data/test/fixtures/xml/tag/toptags.xml +252 -0
- data/test/fixtures/xml/tag/toptracks.xml +30 -0
- data/test/fixtures/xml/track/fans.xml +28 -0
- data/test/fixtures/xml/track/toptags.xml +33 -0
- data/test/fixtures/xml/user/friends.xml +21 -0
- data/test/fixtures/xml/user/neighbours.xml +18 -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 +27 -0
- data/test/fixtures/xml/user/systemrecs.xml +18 -0
- data/test/fixtures/xml/user/topalbums.xml +42 -0
- data/test/fixtures/xml/user/topartists.xml +30 -0
- data/test/fixtures/xml/user/toptags.xml +18 -0
- data/test/fixtures/xml/user/toptracks.xml +27 -0
- data/test/fixtures/xml/user/weeklyalbumchart.xml +35 -0
- data/test/fixtures/xml/user/weeklyalbumchart_from_1138536002_to_1139140802.xml +35 -0
- data/test/fixtures/xml/user/weeklyartistchart.xml +38 -0
- data/test/fixtures/xml/user/weeklyartistchart_from_1138536002_to_1139140802.xml +59 -0
- data/test/fixtures/xml/user/weeklychartlist.xml +74 -0
- data/test/fixtures/xml/user/weeklytrackchart.xml +35 -0
- data/test/fixtures/xml/user/weeklytrackchart_from_1138536002_to_1139140802.xml +35 -0
- data/test/mocks/rest.rb +50 -0
- data/test/test_helper.rb +17 -0
- data/test/unit/album_test.rb +86 -0
- data/test/unit/artist_test.rb +82 -0
- data/test/unit/chart_test.rb +34 -0
- data/test/unit/playing_test.rb +53 -0
- data/test/unit/scrobble_test.rb +69 -0
- data/test/unit/simpleauth_test.rb +45 -0
- data/test/unit/tag_test.rb +65 -0
- data/test/unit/track_test.rb +42 -0
- data/test/unit/user_test.rb +291 -0
- metadata +172 -0
@@ -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,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,66 @@
|
|
1
|
+
# exception definitions
|
2
|
+
class BadSessionError < StandardError; end
|
3
|
+
class RequestFailedError < StandardError; end
|
4
|
+
|
5
|
+
module Scrobbler
|
6
|
+
class Scrobble
|
7
|
+
# you need to read last.fm/api/submissions#subs first!
|
8
|
+
|
9
|
+
attr_accessor :session_id, :submission_url, :artist, :track, :time,
|
10
|
+
:source, :length, :album, :track_number, :mb_track_id
|
11
|
+
attr_reader :status
|
12
|
+
|
13
|
+
def initialize(args = {})
|
14
|
+
@session_id = args[:session_id] # from Scrobbler::SimpleAuth
|
15
|
+
@submission_url = args[:submission_url] # from Scrobbler::SimpleAuth (can change)
|
16
|
+
@artist = args[:artist] # track artist
|
17
|
+
@track = args[:track] # track name
|
18
|
+
@time = args[:time] # a Time object set to the time the track started playing
|
19
|
+
@source = args[:source] || 'P' # track source, see last.fm/api/submissions#subs
|
20
|
+
@length = args[:length].to_s || '' # track length in seconds
|
21
|
+
@album = args[:album] || '' # track album name (optional)
|
22
|
+
@track_number = args[:track_number] || '' # track number (optional)
|
23
|
+
@mb_track_id = args[:mb_track_id] || '' # MusicBrainz track ID (optional)
|
24
|
+
|
25
|
+
if [@session_id, @submission_url, @artist, @track].any?(&:blank?)
|
26
|
+
raise ArgumentError, 'Missing required argument'
|
27
|
+
elsif @time.class.to_s != 'Time'
|
28
|
+
raise ArgumentError, ":time must be a Time object"
|
29
|
+
elsif !['P','R','E','U'].include?(@source) # see last.fm/api/submissions#subs
|
30
|
+
raise ArgumentError, "Invalid source"
|
31
|
+
elsif @source == 'P' && @length.blank? # length is not optional if source is P
|
32
|
+
raise ArgumentError, 'Length must be set'
|
33
|
+
elsif !@length.blank? && @length.to_i <= 30 # see last.fm/api/submissions#subs
|
34
|
+
raise ArgumentError, 'Length must be greater than 30 seconds'
|
35
|
+
end
|
36
|
+
|
37
|
+
@connection = REST::Connection.new(@submission_url)
|
38
|
+
end
|
39
|
+
|
40
|
+
def submit!
|
41
|
+
query = { :s => @session_id,
|
42
|
+
'a[0]' => @artist,
|
43
|
+
't[0]' => @track,
|
44
|
+
'i[0]' => @time.utc.to_i,
|
45
|
+
'o[0]' => @source,
|
46
|
+
'r[0]' => '',
|
47
|
+
'l[0]' => @length,
|
48
|
+
'b[0]' => @album,
|
49
|
+
'n[0]' => @track_number,
|
50
|
+
'm[0]' => @mb_track_id }
|
51
|
+
|
52
|
+
@status = @connection.post('', query)
|
53
|
+
|
54
|
+
case @status
|
55
|
+
when /OK/
|
56
|
+
|
57
|
+
when /BADSESSION/
|
58
|
+
raise BadSessionError # rerun Scrobbler::SimpleAuth#handshake!
|
59
|
+
when /FAILED/
|
60
|
+
raise RequestFailedError, @status
|
61
|
+
else
|
62
|
+
raise RequestFailedError
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,59 @@
|
|
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
|
+
class SimpleAuth
|
12
|
+
# you should read last.fm/api/submissions#handshake
|
13
|
+
|
14
|
+
attr_accessor :user, :password, :client_id, :client_ver
|
15
|
+
attr_reader :status, :session_id, :now_playing_url, :submission_url
|
16
|
+
|
17
|
+
def initialize(args = {})
|
18
|
+
@user = args[:user] # last.fm username
|
19
|
+
@password = args[:password] # last.fm password
|
20
|
+
@client_id = 'rbs' # Client ID assigned by last.fm; Don't change this!
|
21
|
+
@client_ver = Scrobbler::Version
|
22
|
+
|
23
|
+
raise ArgumentError, 'Missing required argument' if @user.blank? || @password.blank?
|
24
|
+
|
25
|
+
@connection = REST::Connection.new(AUTH_URL)
|
26
|
+
end
|
27
|
+
|
28
|
+
def handshake!
|
29
|
+
password_hash = Digest::MD5.hexdigest(@password)
|
30
|
+
timestamp = Time.now.to_i.to_s
|
31
|
+
token = Digest::MD5.hexdigest(password_hash + timestamp)
|
32
|
+
|
33
|
+
query = { :hs => 'true',
|
34
|
+
:p => AUTH_VER,
|
35
|
+
:c => @client_id,
|
36
|
+
:v => @client_ver,
|
37
|
+
:u => @user,
|
38
|
+
:t => timestamp,
|
39
|
+
:a => token }
|
40
|
+
result = @connection.get('/', query)
|
41
|
+
|
42
|
+
@status = result.split(/\n/)[0]
|
43
|
+
case @status
|
44
|
+
when /OK/
|
45
|
+
@session_id, @now_playing_url, @submission_url = result.split(/\n/)[1,3]
|
46
|
+
when /BANNED/
|
47
|
+
raise BannedError # something is wrong with the gem, check for an update
|
48
|
+
when /BADAUTH/
|
49
|
+
raise BadAuthError # invalid user/password
|
50
|
+
when /FAILED/
|
51
|
+
raise RequestFailedError, @status
|
52
|
+
when /BADTIME/
|
53
|
+
raise BadTimeError # system time is way off
|
54
|
+
else
|
55
|
+
raise RequestFailedError
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,104 @@
|
|
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
|
61
|
+
|
62
|
+
class << self
|
63
|
+
def new_from_xml(xml, doc=nil)
|
64
|
+
name = (xml).at(:name).inner_html
|
65
|
+
t = Tag.new(name)
|
66
|
+
t.count = (xml).at(:count).inner_html
|
67
|
+
t.url = (xml).at(:url).inner_html
|
68
|
+
t
|
69
|
+
end
|
70
|
+
|
71
|
+
def top_tags
|
72
|
+
doc = fetch_and_parse("/#{API_VERSION}/tag/toptags.xml")
|
73
|
+
@top_tags = (doc/:tag).inject([]) do |tags, tag|
|
74
|
+
t = Tag.new(tag['name'])
|
75
|
+
t.count = tag['count']
|
76
|
+
t.url = tag['url']
|
77
|
+
tags << t
|
78
|
+
tags
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def initialize(name)
|
84
|
+
raise ArgumentError, "Name is required" if name.blank?
|
85
|
+
@name = name
|
86
|
+
end
|
87
|
+
|
88
|
+
def api_path
|
89
|
+
"/#{API_VERSION}/tag/#{CGI::escape(name)}"
|
90
|
+
end
|
91
|
+
|
92
|
+
def top_artists(force=false)
|
93
|
+
get_instance(:topartists, :top_artists, :artist, force)
|
94
|
+
end
|
95
|
+
|
96
|
+
def top_albums(force=false)
|
97
|
+
get_instance(:topalbums, :top_albums, :album, force)
|
98
|
+
end
|
99
|
+
|
100
|
+
def top_tracks(force=false)
|
101
|
+
get_instance(:toptracks, :top_tracks, :track, force)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,96 @@
|
|
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
|
+
attr_accessor :artist, :artist_mbid, :name, :mbid, :playcount, :rank, :url, :reach
|
39
|
+
attr_accessor :streamable, :album, :album_mbid, :date, :date_uts
|
40
|
+
|
41
|
+
# only seems to be used on top tracks for tag
|
42
|
+
attr_accessor :count, :thumbnail, :image
|
43
|
+
|
44
|
+
# for weekly top tracks
|
45
|
+
attr_accessor :chartposition
|
46
|
+
|
47
|
+
class << self
|
48
|
+
def new_from_xml(xml, doc=nil)
|
49
|
+
artist = (xml).at(:artist)['name'] if (xml).at(:artist) && (xml).at(:artist)['name']
|
50
|
+
artist = (xml).at(:artist).inner_html if artist.nil? && (xml).at(:artist)
|
51
|
+
artist = doc.root['artist'] if artist.nil? && doc.root['artist']
|
52
|
+
name = (xml).at(:name).inner_html if (xml).at(:name)
|
53
|
+
name = xml['name'] if name.nil? && xml['name']
|
54
|
+
t = Track.new(artist, name)
|
55
|
+
t.artist_mbid = (xml).at(:artist)['mbid'] if (xml).at(:artist) && (xml).at(:artist)['mbid']
|
56
|
+
t.artist_mbid = (xml).at(:artist).at(:mbid).inner_html if t.artist_mbid.nil? && (xml).at(:artist) && (xml).at(:artist).at(:mbid)
|
57
|
+
t.mbid = (xml).at(:mbid).inner_html if (xml).at(:mbid)
|
58
|
+
t.playcount = (xml).at(:playcount).inner_html if (xml).at(:playcount)
|
59
|
+
t.chartposition = (xml).at(:chartposition).inner_html if (xml).at(:chartposition)
|
60
|
+
t.rank = (xml).at(:rank).inner_html if (xml).at(:rank)
|
61
|
+
t.url = (xml/:url).last.inner_html if (xml/:url).size > 1
|
62
|
+
t.url = (xml).at(:url).inner_html if t.url.nil? && (xml).at(:url)
|
63
|
+
t.streamable = (xml).at(:track)['streamable'] if (xml).at(:track) && (xml).at(:track)['streamable']
|
64
|
+
t.streamable = xml['streamable'] if t.streamable.nil? && xml['streamable']
|
65
|
+
t.count = xml['count'] if xml['count']
|
66
|
+
t.album = (xml).at(:album).inner_html if (xml).at(:album)
|
67
|
+
t.album_mbid = (xml).at(:album)['mbid'] if (xml).at(:album) && (xml).at(:album)['mbid']
|
68
|
+
t.date = Time.parse((xml).at(:date).inner_html) if (xml).at(:date)
|
69
|
+
t.date_uts = (xml).at(:date)['uts'] if (xml).at(:date) && (xml).at(:date)['uts']
|
70
|
+
t.thumbnail = (xml).at(:thumbnail).inner_html if (xml).at(:thumbnail)
|
71
|
+
t.image = (xml).at(:image).inner_html if (xml).at(:image)
|
72
|
+
t.reach = (xml).at(:reach).inner_html if (xml).at(:reach)
|
73
|
+
t
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def initialize(artist, name)
|
78
|
+
raise ArgumentError, "Artist is required" if artist.blank?
|
79
|
+
raise ArgumentError, "Name is required" if name.blank?
|
80
|
+
@artist = artist
|
81
|
+
@name = name
|
82
|
+
end
|
83
|
+
|
84
|
+
def api_path
|
85
|
+
"/#{API_VERSION}/track/#{CGI::escape(artist)}/#{CGI::escape(name)}"
|
86
|
+
end
|
87
|
+
|
88
|
+
def fans(force=false)
|
89
|
+
get_instance(:fans, :fans, :user, force)
|
90
|
+
end
|
91
|
+
|
92
|
+
def tags(force=false)
|
93
|
+
get_instance(:toptags, :tags, :tag, force)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,192 @@
|
|
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
|
+
# attributes needed to initialize
|
50
|
+
attr_reader :username
|
51
|
+
|
52
|
+
# profile attributes
|
53
|
+
attr_accessor :id, :cluster, :url, :realname, :mbox_sha1sum, :registered
|
54
|
+
attr_accessor :registered_unixtime, :age, :gender, :country, :playcount, :avatar
|
55
|
+
|
56
|
+
# neighbor attributes
|
57
|
+
attr_accessor :match
|
58
|
+
|
59
|
+
# track fans attributes
|
60
|
+
attr_accessor :weight
|
61
|
+
|
62
|
+
class << self
|
63
|
+
def new_from_xml(xml, doc=nil)
|
64
|
+
u = User.new((xml)['username'])
|
65
|
+
u.url = (xml).at(:url).inner_html if (xml).at(:url)
|
66
|
+
u.avatar = (xml).at(:image).inner_html if (xml).at(:image)
|
67
|
+
u.weight = (xml).at(:weight).inner_html if (xml).at(:weight)
|
68
|
+
u.match = (xml).at(:match).inner_html if (xml).at(:match)
|
69
|
+
u
|
70
|
+
end
|
71
|
+
|
72
|
+
def find(*args)
|
73
|
+
options = {:include_profile => false}
|
74
|
+
options.merge!(args.pop) if args.last.is_a?(Hash)
|
75
|
+
users = args.flatten.inject([]) { |users, u| users << User.new(u, options); users }
|
76
|
+
users.length == 1 ? users.pop : users
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def initialize(username, o={})
|
81
|
+
options = {:include_profile => false}.merge(o)
|
82
|
+
raise ArgumentError if username.blank?
|
83
|
+
@username = username
|
84
|
+
load_profile() if options[:include_profile]
|
85
|
+
end
|
86
|
+
|
87
|
+
def api_path
|
88
|
+
"/#{API_VERSION}/user/#{CGI::escape(username)}"
|
89
|
+
end
|
90
|
+
|
91
|
+
def current_events(format=:ics)
|
92
|
+
format = :ics if format.to_s == 'ical'
|
93
|
+
raise ArgumentError unless ['ics', 'rss'].include?(format.to_s)
|
94
|
+
"#{API_URL.chop}#{api_path}/events.#{format}"
|
95
|
+
end
|
96
|
+
|
97
|
+
def friends_events(format=:ics)
|
98
|
+
format = :ics if format.to_s == 'ical'
|
99
|
+
raise ArgumentError unless ['ics', 'rss'].include?(format.to_s)
|
100
|
+
"#{API_URL.chop}#{api_path}/friendevents.#{format}"
|
101
|
+
end
|
102
|
+
|
103
|
+
def recommended_events(format=:ics)
|
104
|
+
format = :ics if format.to_s == 'ical'
|
105
|
+
raise ArgumentError unless ['ics', 'rss'].include?(format.to_s)
|
106
|
+
"#{API_URL.chop}#{api_path}/eventsysrecs.#{format}"
|
107
|
+
end
|
108
|
+
|
109
|
+
def load_profile
|
110
|
+
doc = self.class.fetch_and_parse("#{api_path}/profile.xml")
|
111
|
+
@id = (doc).at(:profile)['id']
|
112
|
+
@cluster = (doc).at(:profile)['cluster']
|
113
|
+
@url = (doc).at(:url).inner_html
|
114
|
+
@realname = (doc).at(:realname).inner_html
|
115
|
+
@mbox_sha1sum = (doc).at(:mbox_sha1sum).inner_html
|
116
|
+
@registered = (doc).at(:registered).inner_html
|
117
|
+
@registered_unixtime = (doc).at(:registered)['unixtime']
|
118
|
+
@age = (doc).at(:age).inner_html
|
119
|
+
@gender = (doc).at(:gender).inner_html
|
120
|
+
@country = (doc).at(:country).inner_html
|
121
|
+
@playcount = (doc).at(:playcount).inner_html
|
122
|
+
@avatar = (doc).at(:avatar).inner_html
|
123
|
+
end
|
124
|
+
|
125
|
+
def top_artists(force=false)
|
126
|
+
get_instance(:topartists, :top_artists, :artist, force)
|
127
|
+
end
|
128
|
+
|
129
|
+
def top_albums(force=false)
|
130
|
+
get_instance(:topalbums, :top_albums, :album, force)
|
131
|
+
end
|
132
|
+
|
133
|
+
def top_tracks(force=false)
|
134
|
+
get_instance(:toptracks, :top_tracks, :track, force)
|
135
|
+
end
|
136
|
+
|
137
|
+
def top_tags(force=false)
|
138
|
+
get_instance(:toptags, :top_tags, :tag, force)
|
139
|
+
end
|
140
|
+
|
141
|
+
def friends(force=false)
|
142
|
+
get_instance(:friends, :friends, :user, force)
|
143
|
+
end
|
144
|
+
|
145
|
+
def neighbours(force=false)
|
146
|
+
get_instance(:neighbours, :neighbours, :user, force)
|
147
|
+
end
|
148
|
+
|
149
|
+
def recent_tracks(force=false)
|
150
|
+
get_instance(:recenttracks, :recent_tracks, :track, force)
|
151
|
+
end
|
152
|
+
|
153
|
+
def recent_banned_tracks(force=false)
|
154
|
+
get_instance(:recentbannedtracks, :recent_banned_tracks, :track, force)
|
155
|
+
end
|
156
|
+
|
157
|
+
def recent_loved_tracks(force=false)
|
158
|
+
get_instance(:recentlovedtracks, :recent_loved_tracks, :track, force)
|
159
|
+
end
|
160
|
+
|
161
|
+
def recommendations(force=false)
|
162
|
+
get_instance(:systemrecs, :recommendations, :artist, force)
|
163
|
+
end
|
164
|
+
|
165
|
+
def charts(force=false)
|
166
|
+
get_instance(:weeklychartlist, :charts, :chart, force)
|
167
|
+
end
|
168
|
+
|
169
|
+
def weekly_artist_chart(from=nil, to=nil)
|
170
|
+
qs = create_query_string_from_timestamps(from, to)
|
171
|
+
doc = self.class.fetch_and_parse("#{api_path}/weeklyartistchart.xml#{qs}")
|
172
|
+
(doc/:artist).inject([]) { |elements, el| elements << Artist.new_from_xml(el); elements }
|
173
|
+
end
|
174
|
+
|
175
|
+
def weekly_album_chart(from=nil, to=nil)
|
176
|
+
qs = create_query_string_from_timestamps(from, to)
|
177
|
+
doc = self.class.fetch_and_parse("#{api_path}/weeklyalbumchart.xml#{qs}")
|
178
|
+
(doc/:album).inject([]) { |elements, el| elements << Album.new_from_xml(el); elements }
|
179
|
+
end
|
180
|
+
|
181
|
+
def weekly_track_chart(from=nil, to=nil)
|
182
|
+
qs = create_query_string_from_timestamps(from, to)
|
183
|
+
doc = self.class.fetch_and_parse("#{api_path}/weeklytrackchart.xml#{qs}")
|
184
|
+
(doc/:track).inject([]) { |elements, el| elements << Track.new_from_xml(el); elements }
|
185
|
+
end
|
186
|
+
|
187
|
+
private
|
188
|
+
def create_query_string_from_timestamps(from, to)
|
189
|
+
(from && to) ? "?from=#{from.to_i}&to=#{to.to_i}" : ''
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
data/lib/scrobbler.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
%w{cgi rubygems hpricot active_support}.each { |x| require x }
|
2
|
+
|
3
|
+
$: << File.expand_path(File.dirname(__FILE__))
|
4
|
+
|
5
|
+
require 'scrobbler/base'
|
6
|
+
require 'scrobbler/version'
|
7
|
+
|
8
|
+
|
9
|
+
require 'scrobbler/album'
|
10
|
+
require 'scrobbler/artist'
|
11
|
+
require 'scrobbler/chart'
|
12
|
+
require 'scrobbler/user'
|
13
|
+
require 'scrobbler/tag'
|
14
|
+
require 'scrobbler/track'
|
15
|
+
|
16
|
+
require 'scrobbler/simpleauth'
|
17
|
+
require 'scrobbler/scrobble'
|
18
|
+
require 'scrobbler/playing'
|
19
|
+
|
20
|
+
require 'scrobbler/rest'
|