peteforde-scrobbler 0.2.3

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.
Files changed (68) hide show
  1. data/History.txt +4 -0
  2. data/MIT-LICENSE +19 -0
  3. data/Manifest +67 -0
  4. data/README.rdoc +113 -0
  5. data/Rakefile +36 -0
  6. data/examples/album.rb +17 -0
  7. data/examples/artist.rb +13 -0
  8. data/examples/scrobble.rb +31 -0
  9. data/examples/tag.rb +11 -0
  10. data/examples/track.rb +10 -0
  11. data/examples/user.rb +14 -0
  12. data/lib/scrobbler.rb +20 -0
  13. data/lib/scrobbler/album.rb +143 -0
  14. data/lib/scrobbler/artist.rb +127 -0
  15. data/lib/scrobbler/base.rb +29 -0
  16. data/lib/scrobbler/chart.rb +31 -0
  17. data/lib/scrobbler/playing.rb +49 -0
  18. data/lib/scrobbler/rest.rb +51 -0
  19. data/lib/scrobbler/scrobble.rb +66 -0
  20. data/lib/scrobbler/simpleauth.rb +59 -0
  21. data/lib/scrobbler/tag.rb +104 -0
  22. data/lib/scrobbler/track.rb +98 -0
  23. data/lib/scrobbler/user.rb +192 -0
  24. data/lib/scrobbler/version.rb +3 -0
  25. data/scrobbler.gemspec +39 -0
  26. data/setup.rb +1585 -0
  27. data/test/fixtures/xml/album/info.xml +70 -0
  28. data/test/fixtures/xml/artist/fans.xml +18 -0
  29. data/test/fixtures/xml/artist/similar.xml +39 -0
  30. data/test/fixtures/xml/artist/topalbums.xml +36 -0
  31. data/test/fixtures/xml/artist/toptags.xml +18 -0
  32. data/test/fixtures/xml/artist/toptracks.xml +27 -0
  33. data/test/fixtures/xml/tag/topalbums.xml +39 -0
  34. data/test/fixtures/xml/tag/topartists.xml +39 -0
  35. data/test/fixtures/xml/tag/toptags.xml +252 -0
  36. data/test/fixtures/xml/tag/toptracks.xml +30 -0
  37. data/test/fixtures/xml/track/fans.xml +28 -0
  38. data/test/fixtures/xml/track/toptags.xml +33 -0
  39. data/test/fixtures/xml/user/friends.xml +21 -0
  40. data/test/fixtures/xml/user/neighbours.xml +18 -0
  41. data/test/fixtures/xml/user/profile.xml +12 -0
  42. data/test/fixtures/xml/user/recentbannedtracks.xml +24 -0
  43. data/test/fixtures/xml/user/recentlovedtracks.xml +24 -0
  44. data/test/fixtures/xml/user/recenttracks.xml +27 -0
  45. data/test/fixtures/xml/user/systemrecs.xml +18 -0
  46. data/test/fixtures/xml/user/topalbums.xml +42 -0
  47. data/test/fixtures/xml/user/topartists.xml +30 -0
  48. data/test/fixtures/xml/user/toptags.xml +18 -0
  49. data/test/fixtures/xml/user/toptracks.xml +27 -0
  50. data/test/fixtures/xml/user/weeklyalbumchart.xml +35 -0
  51. data/test/fixtures/xml/user/weeklyalbumchart_from_1138536002_to_1139140802.xml +35 -0
  52. data/test/fixtures/xml/user/weeklyartistchart.xml +38 -0
  53. data/test/fixtures/xml/user/weeklyartistchart_from_1138536002_to_1139140802.xml +59 -0
  54. data/test/fixtures/xml/user/weeklychartlist.xml +74 -0
  55. data/test/fixtures/xml/user/weeklytrackchart.xml +35 -0
  56. data/test/fixtures/xml/user/weeklytrackchart_from_1138536002_to_1139140802.xml +35 -0
  57. data/test/mocks/rest.rb +50 -0
  58. data/test/test_helper.rb +17 -0
  59. data/test/unit/album_test.rb +86 -0
  60. data/test/unit/artist_test.rb +82 -0
  61. data/test/unit/chart_test.rb +34 -0
  62. data/test/unit/playing_test.rb +53 -0
  63. data/test/unit/scrobble_test.rb +69 -0
  64. data/test/unit/simpleauth_test.rb +45 -0
  65. data/test/unit/tag_test.rb +65 -0
  66. data/test/unit/track_test.rb +42 -0
  67. data/test/unit/user_test.rb +291 -0
  68. metadata +177 -0
@@ -0,0 +1,127 @@
1
+ # Below are examples of how to find an artists top tracks and similar artists.
2
+ #
3
+ # artist = Scrobbler::Artist.new('Carrie Underwood')
4
+ #
5
+ # puts 'Top Tracks'
6
+ # puts "=" * 10
7
+ # artist.top_tracks.each { |t| puts "(#{t.reach}) #{t.name}" }
8
+ #
9
+ # puts
10
+ #
11
+ # puts 'Similar Artists'
12
+ # puts "=" * 15
13
+ # artist.similar.each { |a| puts "(#{a.match}%) #{a.name}" }
14
+ #
15
+ # Would output something similar to:
16
+ #
17
+ # Top Tracks
18
+ # ==========
19
+ # (8797) Before He Cheats
20
+ # (3574) Don't Forget to Remember Me
21
+ # (3569) Wasted
22
+ # (3246) Some Hearts
23
+ # (3142) Jesus, Take the Wheel
24
+ # (2600) Starts With Goodbye
25
+ # (2511) Jesus Take The Wheel
26
+ # (2423) Inside Your Heaven
27
+ # (2328) Lessons Learned
28
+ # (2040) I Just Can't Live a Lie
29
+ # (1899) Whenever You Remember
30
+ # (1882) We're Young and Beautiful
31
+ # (1854) That's Where It Is
32
+ # (1786) I Ain't in Checotah Anymore
33
+ # (1596) The Night Before (Life Goes On)
34
+ #
35
+ # Similar Artists
36
+ # ===============
37
+ # (100%) Rascal Flatts
38
+ # (84.985%) Keith Urban
39
+ # (84.007%) Kellie Pickler
40
+ # (82.694%) Katharine McPhee
41
+ # (81.213%) Martina McBride
42
+ # (79.397%) Faith Hill
43
+ # (77.121%) Tim McGraw
44
+ # (75.191%) Jessica Simpson
45
+ # (75.182%) Sara Evans
46
+ # (75.144%) The Wreckers
47
+ # (73.034%) Kenny Chesney
48
+ # (71.765%) Dixie Chicks
49
+ # (71.084%) Kelly Clarkson
50
+ # (69.535%) Miranda Lambert
51
+ # (66.952%) LeAnn Rimes
52
+ # (66.398%) Mandy Moore
53
+ # (65.817%) Bo Bice
54
+ # (65.279%) Diana DeGarmo
55
+ # (65.115%) Gretchen Wilson
56
+ # (62.982%) Clay Aiken
57
+ # (62.436%) Ashlee Simpson
58
+ # (62.160%) Christina Aguilera
59
+ module Scrobbler
60
+ class Artist < Base
61
+ attr_accessor :name, :mbid, :playcount, :rank, :url, :thumbnail, :image, :reach, :count, :streamable
62
+ attr_accessor :chartposition
63
+
64
+ # used for similar artists
65
+ attr_accessor :match
66
+
67
+ class << self
68
+ def new_from_xml(xml, doc=nil)
69
+ name = (xml).at(:name).inner_html if (xml).at(:name)
70
+ # occasionally name can be found in root of artist element (<artist name="">) rather than as an element (<name>)
71
+ name = xml['name'] if name.nil? && xml['name']
72
+ a = Artist.new(name)
73
+ a.mbid = (xml).at(:mbid).inner_html if (xml).at(:mbid)
74
+ a.playcount = (xml).at(:playcount).inner_html if (xml).at(:playcount)
75
+ a.rank = (xml).at(:rank).inner_html if (xml).at(:rank)
76
+ a.url = (xml).at(:url).inner_html if (xml).at(:url)
77
+ a.thumbnail = (xml).at(:thumbnail).inner_html if (xml).at(:thumbnail)
78
+ a.thumbnail = (xml).at(:image_small).inner_html if a.thumbnail.nil? && (xml).at(:image_small)
79
+ a.image = (xml).at(:image).inner_html if (xml).at(:image)
80
+ a.reach = (xml).at(:reach).inner_html if (xml).at(:reach)
81
+ a.match = (xml).at(:match).inner_html if (xml).at(:match)
82
+ a.chartposition = (xml).at(:chartposition).inner_html if (xml).at(:chartposition)
83
+
84
+ # in top artists for tag
85
+ a.count = xml['count'] if xml['count']
86
+ a.streamable = xml['streamable'] if xml['streamable']
87
+ a.streamable = (xml).at(:streamable).inner_html == '1' ? 'yes' : 'no' if a.streamable.nil? && (xml).at(:streamable)
88
+ a
89
+ end
90
+ end
91
+
92
+ def initialize(name)
93
+ raise ArgumentError, "Name is required" if name.blank?
94
+ @name = name
95
+ end
96
+
97
+ def api_path(version=nil)
98
+ "/#{version || API_VERSION}/artist/#{CGI::escape(name)}"
99
+ end
100
+
101
+ def current_events(format=:ics)
102
+ format = :ics if format.to_s == 'ical'
103
+ raise ArgumentError unless ['ics', 'rss'].include?(format.to_s)
104
+ "#{API_URL.chop}#{api_path}/events.#{format}"
105
+ end
106
+
107
+ def similar(force=false)
108
+ get_instance(:similar, :similar, :artist, force)
109
+ end
110
+
111
+ def top_fans(force=false)
112
+ get_instance(:fans, :top_fans, :user, force)
113
+ end
114
+
115
+ def top_tracks(force=false)
116
+ get_instance(:toptracks, :top_tracks, :track, force)
117
+ end
118
+
119
+ def top_albums(force=false)
120
+ get_instance(:topalbums, :top_albums, :album, force)
121
+ end
122
+
123
+ def top_tags(force=false)
124
+ get_instance(:toptags, :top_tags, :tag, force)
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,29 @@
1
+ module Scrobbler
2
+
3
+ API_URL = 'http://ws.audioscrobbler.com/'
4
+ API_VERSION = '1.0'
5
+
6
+ class Base
7
+ class << self
8
+ def connection
9
+ @connection ||= REST::Connection.new(API_URL)
10
+ end
11
+
12
+ def fetch_and_parse(resource)
13
+ Hpricot::XML(connection.get(resource))
14
+ end
15
+ end
16
+
17
+ private
18
+ # in order for subclass to use, it must have api_path method
19
+ def get_instance(api_method, instance_name, element, force=false, version=nil)
20
+ scrobbler_class = "scrobbler/#{element.to_s}".camelize.constantize
21
+ if instance_variable_get("@#{instance_name}").nil? || force
22
+ doc = self.class.fetch_and_parse("#{api_path(version)}/#{api_method}.xml")
23
+ elements = (doc/element).inject([]) { |elements, el| elements << scrobbler_class.new_from_xml(el, doc); elements }
24
+ instance_variable_set("@#{instance_name}", elements)
25
+ end
26
+ instance_variable_get("@#{instance_name}")
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,31 @@
1
+ module Scrobbler
2
+ class Chart < Base
3
+ class << self
4
+ def new_from_xml(xml, doc)
5
+ Chart.new(xml['from'], xml['to'])
6
+ end
7
+ end
8
+ def initialize(from, to)
9
+ raise ArgumentError, "From is required" if from.blank?
10
+ raise ArgumentError, "To is required" if to.blank?
11
+ @from = from
12
+ @to = to
13
+ end
14
+
15
+ def from=(value)
16
+ @from = value.to_i
17
+ end
18
+
19
+ def to=(value)
20
+ @to = value.to_i
21
+ end
22
+
23
+ def from
24
+ @from.to_i
25
+ end
26
+
27
+ def to
28
+ @to.to_i
29
+ end
30
+ end
31
+ end
@@ -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,51 @@
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
+ url.query = args.map { |k,v| "%s=%s" % [escape(k.to_s), escape(v.to_s)] }.join("&")
25
+ end
26
+
27
+ case method
28
+ when "get"
29
+ req = Net::HTTP::Get.new(url.request_uri)
30
+ when "post"
31
+ req = Net::HTTP::Post.new(url.request_uri)
32
+ end
33
+
34
+ if @username and @password
35
+ req.basic_auth(@username, @password)
36
+ end
37
+
38
+ http = Net::HTTP.new(url.host, url.port)
39
+ http.use_ssl = (url.port == 443)
40
+
41
+ res = http.start() { |conn| conn.request(req) }
42
+ res.body
43
+ end
44
+
45
+ private
46
+ def escape(str)
47
+ URI.escape(str, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
48
+ end
49
+ end
50
+ end
51
+ 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(version=nil)
89
+ "/#{version || 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