xhochy-scrobbler 0.2.14

Sign up to get free protection for your applications and to get access to all the features.
Files changed (66) hide show
  1. data/History.txt +5 -0
  2. data/MIT-LICENSE +19 -0
  3. data/Manifest +65 -0
  4. data/README.rdoc +114 -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 +6 -0
  11. data/examples/user.rb +14 -0
  12. data/lib/scrobbler.rb +22 -0
  13. data/lib/scrobbler/album.rb +140 -0
  14. data/lib/scrobbler/artist.rb +134 -0
  15. data/lib/scrobbler/base.rb +82 -0
  16. data/lib/scrobbler/chart.rb +31 -0
  17. data/lib/scrobbler/playing.rb +49 -0
  18. data/lib/scrobbler/rest.rb +47 -0
  19. data/lib/scrobbler/scrobble.rb +66 -0
  20. data/lib/scrobbler/search.rb +60 -0
  21. data/lib/scrobbler/simpleauth.rb +59 -0
  22. data/lib/scrobbler/tag.rb +93 -0
  23. data/lib/scrobbler/track.rb +89 -0
  24. data/lib/scrobbler/user.rb +173 -0
  25. data/lib/scrobbler/version.rb +3 -0
  26. data/scrobbler.gemspec +41 -0
  27. data/setup.rb +1585 -0
  28. data/test/fixtures/xml/album/info.xml +43 -0
  29. data/test/fixtures/xml/artist/fans.xml +52 -0
  30. data/test/fixtures/xml/artist/similar.xml +1004 -0
  31. data/test/fixtures/xml/artist/topalbums.xml +61 -0
  32. data/test/fixtures/xml/artist/toptags.xml +19 -0
  33. data/test/fixtures/xml/artist/toptracks.xml +62 -0
  34. data/test/fixtures/xml/search/album.xml +241 -0
  35. data/test/fixtures/xml/search/artist.xml +215 -0
  36. data/test/fixtures/xml/search/track.xml +209 -0
  37. data/test/fixtures/xml/tag/topalbums.xml +805 -0
  38. data/test/fixtures/xml/tag/topartists.xml +605 -0
  39. data/test/fixtures/xml/tag/toptags.xml +1254 -0
  40. data/test/fixtures/xml/tag/toptracks.xml +852 -0
  41. data/test/fixtures/xml/track/fans.xml +34 -0
  42. data/test/fixtures/xml/track/toptags.xml +33 -0
  43. data/test/fixtures/xml/user/friends.xml +30 -0
  44. data/test/fixtures/xml/user/neighbours.xml +23 -0
  45. data/test/fixtures/xml/user/profile.xml +12 -0
  46. data/test/fixtures/xml/user/recentbannedtracks.xml +24 -0
  47. data/test/fixtures/xml/user/recentlovedtracks.xml +24 -0
  48. data/test/fixtures/xml/user/recenttracks.xml +47 -0
  49. data/test/fixtures/xml/user/systemrecs.xml +18 -0
  50. data/test/fixtures/xml/user/topalbums.xml +61 -0
  51. data/test/fixtures/xml/user/topartists.xml +41 -0
  52. data/test/fixtures/xml/user/toptags.xml +44 -0
  53. data/test/fixtures/xml/user/toptracks.xml +65 -0
  54. data/test/mocks/rest.rb +102 -0
  55. data/test/test_helper.rb +20 -0
  56. data/test/unit/album_test.rb +73 -0
  57. data/test/unit/artist_test.rb +106 -0
  58. data/test/unit/chart_test.rb +34 -0
  59. data/test/unit/playing_test.rb +53 -0
  60. data/test/unit/scrobble_test.rb +69 -0
  61. data/test/unit/search_test.rb +55 -0
  62. data/test/unit/simpleauth_test.rb +45 -0
  63. data/test/unit/tag_test.rb +58 -0
  64. data/test/unit/track_test.rb +37 -0
  65. data/test/unit/user_test.rb +201 -0
  66. metadata +175 -0
@@ -0,0 +1,134 @@
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
+ # @todo Add missing functions that require authentication
61
+ # @todo Integrate search functionality into this class which is already implemented in Scrobbler::Search
62
+ class Artist < Base
63
+ attr_accessor :name, :mbid, :playcount, :rank, :url, :count, :streamable
64
+ attr_accessor :chartposition, :image_small, :image_medium, :image_large
65
+
66
+ # used for similar artists
67
+ attr_accessor :match
68
+
69
+ class << self
70
+ def new_from_xml(xml, doc=nil)
71
+ # occasionally name can be found in root of artist element (<artist name="">) rather than as an element (<name>)
72
+ name = Base::sanitize(xml['name']) if name.nil? && xml['name']
73
+ name = Base::sanitize(xml.at('/name').inner_html) if name.nil? && (xml).at(:name)
74
+ a = Artist.new(name)
75
+ a.mbid = xml.at(:mbid).inner_html if xml.at(:mbid)
76
+ a.playcount = xml.at(:playcount).inner_html if xml.at(:playcount)
77
+ a.rank = Base::sanitize(xml['rank']) if xml['rank']
78
+ a.url = xml.at(:url).inner_html if xml.at(:url)
79
+ a.image_small = xml.at("image[@size='small']").inner_html if xml.at("image[@size='small']")
80
+ a.image_medium = xml.at("image[@size='medium']'").inner_html if xml.at("image[@size='medium']'")
81
+ a.image_large = xml.at("image[@size='large']'").inner_html if xml.at("image[@size='large']'")
82
+ a.match = xml.at(:match).inner_html if xml.at(:match)
83
+ a.chartposition = xml.at(:chartposition).inner_html if xml.at(:chartposition)
84
+
85
+ # in top artists for tag
86
+ a.count = xml.at('/tagcount').inner_html if xml.at('/tagcount')
87
+ a.streamable = xml['streamable'] if xml['streamable']
88
+ a.streamable = xml.at(:streamable).inner_html == '1' ? 'yes' : 'no' if a.streamable.nil? && xml.at(:streamable)
89
+ a
90
+ end
91
+ end
92
+
93
+ def initialize(name)
94
+ raise ArgumentError, "Name is required" if name.blank?
95
+ @name = name
96
+ end
97
+
98
+ # Get the URL to the ical or rss representation of the current events that
99
+ # a artist will play
100
+ #
101
+ # @todo Use the API function and parse that into a common ruby structure
102
+ def current_events(format=:ics)
103
+ format = :ics if format.to_s == 'ical'
104
+ raise ArgumentError unless ['ics', 'rss'].include?(format.to_s)
105
+ "#{API_URL.chop}/2.0/artist/#{CGI::escape(name)}/events.#{format}"
106
+ end
107
+
108
+ def image(which=:small)
109
+ which = which.to_s
110
+ raise ArgumentError unless ['small', 'medium', 'large'].include?(which)
111
+ instance_variable_get("@image_#{which}")
112
+ end
113
+
114
+ def similar(force=false)
115
+ get_instance2('artist.getsimilar', :similar, :artist, {'artist'=>@name}, force)
116
+ end
117
+
118
+ def top_fans(force=false)
119
+ get_instance2('artist.gettopfans', :top_fans, :user, {'artist'=>@name}, force)
120
+ end
121
+
122
+ def top_tracks(force=false)
123
+ get_instance2('artist.gettoptracks', :top_tracks, :track, {'artist'=>@name}, force)
124
+ end
125
+
126
+ def top_albums(force=false)
127
+ get_instance2('artist.gettopalbums', :top_albums, :album, {'artist'=>@name}, force)
128
+ end
129
+
130
+ def top_tags(force=false)
131
+ get_instance2('artist.gettoptags', :top_tags, :tag, {'artist'=>@name}, force)
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,82 @@
1
+ require 'rubygems'
2
+ require 'cgi'
3
+ require 'htmlentities'
4
+
5
+ $KCODE = 'u'
6
+
7
+ module Scrobbler
8
+
9
+ API_URL = 'http://ws.audioscrobbler.com/'
10
+ API_VERSION = '1.0'
11
+
12
+ class Base
13
+ def Base.api_key=(api_key)
14
+ @@api_key = api_key
15
+ end
16
+
17
+ @@coder = HTMLEntities.new
18
+
19
+ # Decode HTML Entities so that we have plain strings in out code
20
+ def Base.sanitize(string)
21
+ @@coder.decode(string)
22
+ end
23
+
24
+ class << self
25
+ def connection
26
+ @connection ||= REST::Connection.new(API_URL)
27
+ end
28
+
29
+ def fetch_and_parse(resource)
30
+ Hpricot::XML(connection.get(resource))
31
+ end
32
+
33
+ end
34
+
35
+ private
36
+ def request(api_method, parameters)
37
+ parameters['api_key'] = @@api_key
38
+ parameters['method'] = api_method.to_s
39
+ paramlist = []
40
+ parameters.each do |key, value|
41
+ paramlist << "#{CGI::escape(key)}=#{CGI::escape(value)}"
42
+ end
43
+
44
+ self.class.fetch_and_parse('/2.0/?' + paramlist.join('&'))
45
+ end
46
+
47
+ def get_instance2(api_method, instance_name, element, parameters = {}, force=false)
48
+ scrobbler_class = "scrobbler/#{element.to_s}".camelize.constantize
49
+ if instance_variable_get("@#{instance_name}").nil? || force
50
+ # Add the API key and the method to the parameters, they are always required
51
+ parameters['api_key'] = @@api_key
52
+ parameters['method'] = api_method.to_s
53
+
54
+ # url-escape all parameters
55
+ paramlist = []
56
+ parameters.each do |key, value|
57
+ paramlist << "#{CGI::escape(key)}=#{CGI::escape(value)}"
58
+ end
59
+
60
+ # Fetch data
61
+ doc = self.class.fetch_and_parse('/2.0/?' + paramlist.join('&'));
62
+ elements = (doc/element).inject([]) do |elements, el|
63
+ elements << scrobbler_class.new_from_xml(el, doc);
64
+ elements
65
+ end
66
+ instance_variable_set("@#{instance_name}", elements)
67
+ end
68
+ instance_variable_get("@#{instance_name}")
69
+ end
70
+
71
+ # in order for subclass to use, it must have api_path method
72
+ def get_instance(api_method, instance_name, element, force=false)
73
+ scrobbler_class = "scrobbler/#{element.to_s}".camelize.constantize
74
+ if instance_variable_get("@#{instance_name}").nil? || force
75
+ doc = self.class.fetch_and_parse("#{api_path}/#{api_method}.xml")
76
+ elements = (doc/element).inject([]) { |elements, el| elements << scrobbler_class.new_from_xml(el, doc); elements }
77
+ instance_variable_set("@#{instance_name}", elements)
78
+ end
79
+ instance_variable_get("@#{instance_name}")
80
+ end
81
+ end
82
+ 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,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,60 @@
1
+ #
2
+ # This is an interface to the Last.FM Search API.
3
+ # It currently allows you to search by album, artist, or track.
4
+ #
5
+
6
+ module Scrobbler
7
+ class Search < Base
8
+ attr_accessor :type, :query, :api_key
9
+
10
+ def initialize(api_key)
11
+ @api_key = api_key
12
+ end
13
+
14
+ def execute
15
+ doc = self.class.fetch_and_parse(api_path)
16
+ results = []
17
+ if type == 'album'
18
+ (doc/"//album").each do |album|
19
+ artist = album/"artist"
20
+ name = album/"name"
21
+ results << Album.new(artist.inner_html, name.inner_html, :include_info => true)
22
+ end
23
+ elsif type == 'artist'
24
+ (doc/"//artist/name").each do |name|
25
+ results << Artist.new(name.inner_html)
26
+ end
27
+ elsif type == 'track'
28
+ (doc/"//track").each do |track|
29
+ artist = track/"artist"
30
+ name = track/"name"
31
+ results << Track.new(artist.inner_html, name.inner_html)
32
+ end
33
+ end
34
+ results
35
+ end
36
+
37
+ def by_album(album_name)
38
+ @type = 'album'
39
+ @query = album_name
40
+ execute
41
+ end
42
+
43
+ def by_artist(artist_name)
44
+ @type = 'artist'
45
+ @query = artist_name
46
+ execute
47
+ end
48
+
49
+ def by_track(track_name)
50
+ @type = 'track'
51
+ @query = track_name
52
+ execute
53
+ end
54
+
55
+ def api_path
56
+ "/2.0/?method=#{type}.search&#{type}=#{CGI::escape(query)}&api_key=#{api_key}"
57
+ end
58
+
59
+ end
60
+ end