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,124 @@
|
|
|
1
|
+
require 'digest/md5'
|
|
2
|
+
|
|
3
|
+
$KCODE = 'u'
|
|
4
|
+
|
|
5
|
+
include LibXML
|
|
6
|
+
|
|
7
|
+
module Scrobbler
|
|
8
|
+
|
|
9
|
+
API_URL = 'http://ws.audioscrobbler.com/'
|
|
10
|
+
|
|
11
|
+
class Base
|
|
12
|
+
def Base.api_key=(api_key)
|
|
13
|
+
@@api_key = api_key
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def Base.secret=(secret)
|
|
17
|
+
@@secret = secret
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def Base.connection
|
|
21
|
+
@connection ||= REST::Connection.new(API_URL)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def Base.sanitize(param)
|
|
25
|
+
URI.escape(param.to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def Base.camelize(lower_case_and_underscored_word, first_letter_in_uppercase = true)
|
|
29
|
+
if first_letter_in_uppercase
|
|
30
|
+
lower_case_and_underscored_word.to_s.gsub(/\/(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase }
|
|
31
|
+
else
|
|
32
|
+
lower_case_and_underscored_word.first.downcase + camelize(lower_case_and_underscored_word)[1..-1]
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def Base.constanize(camel_cased_word)
|
|
37
|
+
names = camel_cased_word.split('::')
|
|
38
|
+
names.shift if names.empty? || names.first.empty?
|
|
39
|
+
|
|
40
|
+
constant = Object
|
|
41
|
+
names.each do |name|
|
|
42
|
+
constant = constant.const_defined?(name) ? constant.const_get(name) : constant.const_missing(name)
|
|
43
|
+
end
|
|
44
|
+
constant
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def Base.get(api_method, parent, element, parameters = {})
|
|
48
|
+
scrobbler_class = constanize(camelize("scrobbler/#{element.to_s}"))
|
|
49
|
+
doc = request(api_method, parameters)
|
|
50
|
+
elements = []
|
|
51
|
+
doc.root.children.each do |child|
|
|
52
|
+
next unless child.name == parent.to_s
|
|
53
|
+
child.children.each do |child2|
|
|
54
|
+
next unless child2.name == element.to_s
|
|
55
|
+
elements << scrobbler_class.new_from_libxml(child2)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
elements
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def Base.post_request(api_method, parameters = {}, request_method = 'get')
|
|
62
|
+
Base.request(api_method, parameters, 'post')
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def Base.request(api_method, parameters = {}, request_method = 'get')
|
|
66
|
+
parameters = {:signed => false}.merge(parameters)
|
|
67
|
+
parameters['api_key'] = @@api_key
|
|
68
|
+
parameters['method'] = api_method.to_s
|
|
69
|
+
paramlist = []
|
|
70
|
+
# Check if we want a signed call and pop :signed
|
|
71
|
+
if parameters.delete :signed
|
|
72
|
+
#1: Sort alphabetically
|
|
73
|
+
params = parameters.sort{|a,b| a[0].to_s<=>b[0].to_s}
|
|
74
|
+
#2: concat them into one string
|
|
75
|
+
str = params.join('')
|
|
76
|
+
#3: Append secret
|
|
77
|
+
str = str + @@secret
|
|
78
|
+
#4: Make a md5 hash
|
|
79
|
+
md5 = Digest::MD5.hexdigest(str)
|
|
80
|
+
params << [:api_sig, md5]
|
|
81
|
+
params.each do |a|
|
|
82
|
+
paramlist << "#{sanitize(a[0])}=#{sanitize(a[1])}"
|
|
83
|
+
end
|
|
84
|
+
else
|
|
85
|
+
parameters.each do |key, value|
|
|
86
|
+
paramlist << "#{sanitize(key)}=#{sanitize(value)}"
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
url = '/2.0/?' + paramlist.join('&')
|
|
90
|
+
XML::Document.string(self.connection.send(request_method,url))
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
def Base.mixins(*args)
|
|
97
|
+
args.each do |arg|
|
|
98
|
+
if arg == :image
|
|
99
|
+
extend Scrobbler::ImageClassFuncs
|
|
100
|
+
include Scrobbler::ImageObjectFuncs
|
|
101
|
+
elsif arg == :streamable
|
|
102
|
+
attr_reader :streamable
|
|
103
|
+
extend StreamableClassFuncs
|
|
104
|
+
include StreamableObjectFuncs
|
|
105
|
+
else
|
|
106
|
+
raise ArgumentError, "#{arg} is not a known mixin"
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def populate_data(data = {})
|
|
112
|
+
data.each do |key, value|
|
|
113
|
+
instance_variable_set("@#{key.to_s}", value)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def get_response(api_method, instance_name, parent, element, params, force=false)
|
|
118
|
+
if instance_variable_get("@#{instance_name}").nil? || force
|
|
119
|
+
instance_variable_set("@#{instance_name}", Base.get(api_method, parent, element, params))
|
|
120
|
+
end
|
|
121
|
+
instance_variable_get("@#{instance_name}")
|
|
122
|
+
end
|
|
123
|
+
end # class Base
|
|
124
|
+
end # module Scrobbler
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
module Scrobbler
|
|
2
|
+
class Event < Base
|
|
3
|
+
# Load Helper modules
|
|
4
|
+
include ImageObjectFuncs
|
|
5
|
+
extend ImageClassFuncs
|
|
6
|
+
|
|
7
|
+
attr_accessor :id, :title, :start_date, :start_time, :description
|
|
8
|
+
attr_accessor :reviews, :tag, :url, :artists, :headliner, :image_small
|
|
9
|
+
attr_accessor :image_medium, :image_large, :attendance, :venue
|
|
10
|
+
|
|
11
|
+
class << self
|
|
12
|
+
|
|
13
|
+
def update_or_create_from_xml(xml, event=nil)
|
|
14
|
+
data = {}
|
|
15
|
+
artists = []
|
|
16
|
+
headliner = nil
|
|
17
|
+
venue = nil
|
|
18
|
+
|
|
19
|
+
xml.children.each do |child|
|
|
20
|
+
data[:id] = child.content.to_i if child.name == 'id'
|
|
21
|
+
data[:title] = child.content if child.name == 'title'
|
|
22
|
+
|
|
23
|
+
if child.name == 'artists'
|
|
24
|
+
child.children.each do |artist_element|
|
|
25
|
+
artists << Artist.new(artist_element.content) if artist_element.name == 'artist'
|
|
26
|
+
headliner = Artist.new(artist_element.content) if artist_element.name == 'headliner'
|
|
27
|
+
end
|
|
28
|
+
artists << headliner unless headliner.nil? || headliner_alrady_listed_in_artist_list?(artists,headliner)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
maybe_image_node(data, child)
|
|
32
|
+
data[:url] = child.content if child.name == 'url'
|
|
33
|
+
data[:description] = child.content if child.name == 'description'
|
|
34
|
+
data[:attendance] = child.content.to_i if child.name == 'attendance'
|
|
35
|
+
data[:reviews] = child.content.to_i if child.name == 'reviews'
|
|
36
|
+
data[:tag] = child.content if child.name == 'tag'
|
|
37
|
+
data[:start_date] = Time.parse(child.content) if child.name == 'startDate'
|
|
38
|
+
data[:start_time] = child.content if child.name == 'startTime'
|
|
39
|
+
venue = Venue.new_from_xml(child) if child.name == 'venue'
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
if event.nil?
|
|
43
|
+
event = Event.new(data[:id],data)
|
|
44
|
+
else
|
|
45
|
+
event.send :populate_data, data
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
event.artists = artists.uniq
|
|
49
|
+
event.headliner = headliner
|
|
50
|
+
event.venue = venue
|
|
51
|
+
event
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def headliner_alrady_listed_in_artist_list?(artists,headliner)
|
|
55
|
+
artists.each do |artist|
|
|
56
|
+
return true if artist.name == headliner.name
|
|
57
|
+
end
|
|
58
|
+
false
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def new_from_libxml(xml)
|
|
62
|
+
update_or_create_from_xml(xml)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def initialize(id,input={})
|
|
67
|
+
raise ArgumentError if id.nil?
|
|
68
|
+
@id = id
|
|
69
|
+
populate_data(input)
|
|
70
|
+
load_info() if input[:include_info]
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def shouts(force = false)
|
|
74
|
+
get_response('event.getshouts', :shouts, 'shouts', 'shout', {'event'=>@id}, force)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def attendees(force = false)
|
|
78
|
+
get_response('event.getattendees', :attendees, 'attendees', 'user', {'event'=>@id}, force)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def shout
|
|
82
|
+
# This function require authentication, but SimpleAuth is not yet 2.0
|
|
83
|
+
raise NotImplementedError
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def attend(session, attendance_status)
|
|
87
|
+
doc = Base.post_request('event.attend',{:event => @id, :signed => true, :status => attendance_status, :sk => session.key})
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def share
|
|
91
|
+
# This function require authentication, but SimpleAuth is not yet 2.0
|
|
92
|
+
raise NotImplementedError
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Load additional informatalbumion about this event
|
|
96
|
+
#
|
|
97
|
+
# Calls "event.getinfo" REST method
|
|
98
|
+
def load_info
|
|
99
|
+
doc = Base.request('event.getinfo', {'event' => @id})
|
|
100
|
+
doc.root.children.each do |child|
|
|
101
|
+
Event.update_or_create_from_xml(child, self) if child.name == 'event'
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
module Scrobbler
|
|
2
|
+
class Geo < Base
|
|
3
|
+
|
|
4
|
+
# Gets a list of events based on the location that
|
|
5
|
+
# the Geo object is set to
|
|
6
|
+
def events(options={})
|
|
7
|
+
options = set_default_options(options)
|
|
8
|
+
get_response('geo.getevents', :events, 'events', 'event', options, options[:force])
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def top_artists(options={})
|
|
12
|
+
options = set_default_options(options)
|
|
13
|
+
get_response('geo.gettopartists', :artists, 'topartists', 'artist', options, options[:force])
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def top_tracks(options={})
|
|
17
|
+
options = set_default_options(options)
|
|
18
|
+
get_response('geo.gettoptracks', :tracks, 'toptracks', 'track', options, options[:force])
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def set_default_options(options={})
|
|
24
|
+
{:force => false, :page => 1}.merge options
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
module Scrobbler
|
|
2
|
+
# Defines some functions that are used nearly all Scrobbler classes which
|
|
3
|
+
# have to deal with image references.
|
|
4
|
+
#
|
|
5
|
+
# This module defines the class functions, use "extend ImageClassFuncs" in
|
|
6
|
+
# class.
|
|
7
|
+
module ImageClassFuncs
|
|
8
|
+
# Check if the given libxml node is an image referencing node and in case
|
|
9
|
+
# of, read it into that given hash of data
|
|
10
|
+
def maybe_image_node(data, node)
|
|
11
|
+
if node.name == 'image'
|
|
12
|
+
data[:image_small] = node.content if node['size'] == 'small'
|
|
13
|
+
data[:image_medium] = node.content if node['size'] == 'medium'
|
|
14
|
+
data[:image_large] = node.content if node['size'] == 'large'
|
|
15
|
+
data[:image_extralarge] = node.content if node['size'] == 'extralarge'
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Defines some functions that are used nearly all Scrobbler classes which
|
|
21
|
+
# have to deal with image references.
|
|
22
|
+
#
|
|
23
|
+
# This module defines the object functions, use "include ImageObjectFuncs" in
|
|
24
|
+
# class.
|
|
25
|
+
module ImageObjectFuncs
|
|
26
|
+
# Check if the given libxml node is an image referencing node and in case
|
|
27
|
+
# of, read it into the object
|
|
28
|
+
def check_image_node(node)
|
|
29
|
+
if node.name == 'image'
|
|
30
|
+
@image_small = node.content if node['size'] == 'small'
|
|
31
|
+
@image_medium = node.content if node['size'] == 'medium'
|
|
32
|
+
@image_large = node.content if node['size'] == 'large'
|
|
33
|
+
@image_extralarge = node.content if node['size'] == 'extralarge'
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Return the URL to the specified image size.
|
|
38
|
+
#
|
|
39
|
+
# If the URL to the given image size is not known and a
|
|
40
|
+
# 'load_info'-function is defined, it will be called.
|
|
41
|
+
def image(which=:small)
|
|
42
|
+
which = which.to_s
|
|
43
|
+
raise ArgumentError unless ['small', 'medium', 'large', 'extralarge'].include?(which)
|
|
44
|
+
img_url = instance_variable_get("@image_#{which}")
|
|
45
|
+
if img_url.nil? && responds_to?(:load_info)
|
|
46
|
+
load_info
|
|
47
|
+
img_url = instance_variable_get("@image_#{which}")
|
|
48
|
+
end
|
|
49
|
+
img_url
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
module Scrobbler
|
|
2
|
+
# Defines some functions that are used nearly all Scrobbler classes which
|
|
3
|
+
# have to deal with the streamable flag.
|
|
4
|
+
#
|
|
5
|
+
# This module defines the class functions, use "extend StreamableClassFuncs" in
|
|
6
|
+
# class.
|
|
7
|
+
module StreamableClassFuncs
|
|
8
|
+
# Check if the given libxml node is defining if the given content is
|
|
9
|
+
# streamable. If so, set the flag in the given hash
|
|
10
|
+
def Base.maybe_streamable_node(data, node)
|
|
11
|
+
if node.name == 'streamable'
|
|
12
|
+
data[:streamable] = ['1', 'true'].include?(node.content)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Check if the given libxml node has a streamable attribute defining if the
|
|
17
|
+
# given content is streamable. If so, set the flag in the given hash
|
|
18
|
+
def Base.maybe_streamable_attribute(data, node)
|
|
19
|
+
if node['streamable']
|
|
20
|
+
data[:streamable] = ['1', 'true'].include?(node['streamable'])
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Defines some functions that are used nearly all Scrobbler classes which
|
|
27
|
+
# have to deal with the streamable flag.
|
|
28
|
+
#
|
|
29
|
+
# This module defines the object functions, use "include StreamableObjectFuncs" in
|
|
30
|
+
# class.
|
|
31
|
+
module StreamableObjectFuncs
|
|
32
|
+
# Check if the given libxml node is defining if the given content is
|
|
33
|
+
# streamable. If so, set the flag in the object
|
|
34
|
+
def check_streamable_node(node)
|
|
35
|
+
if node.name == 'streamable'
|
|
36
|
+
@streamable = ['1', 'true'].include?(node.content)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
module Scrobbler
|
|
2
|
+
# Class for invocation of library.* API functions.
|
|
3
|
+
class Library < Base
|
|
4
|
+
attr_reader :user
|
|
5
|
+
|
|
6
|
+
# Create a new Scrobber::Library instance.
|
|
7
|
+
#
|
|
8
|
+
# @param [String, Symbol, Scrobbler::User] user The user who's library data
|
|
9
|
+
# is requested.
|
|
10
|
+
# @raise ArgumentError If a not supported type is given as user.
|
|
11
|
+
def initialize(user)
|
|
12
|
+
case user.class
|
|
13
|
+
when user.class == Scrobbler::User
|
|
14
|
+
@user = user
|
|
15
|
+
when [String, Symbol].contains?(user.class)
|
|
16
|
+
@user = Scrobbler::User.new(user.to_s)
|
|
17
|
+
else
|
|
18
|
+
raise ArgumentError("Invalid argument for user.")
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def add_album
|
|
23
|
+
# This function require authentication, but SimpleAuth is not yet 2.0
|
|
24
|
+
raise NotImplementedError
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def add_artist
|
|
28
|
+
# This function require authentication, but SimpleAuth is not yet 2.0
|
|
29
|
+
raise NotImplementedError
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def add_track
|
|
33
|
+
# This function require authentication, but SimpleAuth is not yet 2.0
|
|
34
|
+
raise NotImplementedError
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# A list of all the albums in a user's library, with play counts and tag
|
|
38
|
+
# counts.
|
|
39
|
+
def albums(options={})
|
|
40
|
+
options = {:force => false, :all => true}.merge options
|
|
41
|
+
options[:user] = @user.name
|
|
42
|
+
albums = []
|
|
43
|
+
if options[:all]
|
|
44
|
+
doc = Base.request('library.getalbums', options)
|
|
45
|
+
root = nil
|
|
46
|
+
doc.root.children.each do |child|
|
|
47
|
+
next unless child.name == 'albums'
|
|
48
|
+
root = child
|
|
49
|
+
end
|
|
50
|
+
total_pages = root['totalPages'].to_i
|
|
51
|
+
root.children.each do |child|
|
|
52
|
+
next unless child.name == 'album'
|
|
53
|
+
albums << Scrobbler::Album.new_from_libxml(child)
|
|
54
|
+
end
|
|
55
|
+
for i in 2..total_pages do
|
|
56
|
+
options[:page] = i
|
|
57
|
+
albums.concat get_response('library.getalbums', :none, 'albums', 'album', options, true)
|
|
58
|
+
end
|
|
59
|
+
else
|
|
60
|
+
albums = get_response('library.getalbums', :get_albums, 'albums', 'album', options, true)
|
|
61
|
+
end
|
|
62
|
+
albums
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# A list of all the artists in a user's library, with play counts and tag
|
|
66
|
+
# counts.
|
|
67
|
+
def artists(options={})
|
|
68
|
+
options = {:force => false, :all => true}.merge options
|
|
69
|
+
options[:user] = @user.name
|
|
70
|
+
artists = []
|
|
71
|
+
if options[:all]
|
|
72
|
+
doc = Base.request('library.getartists', options)
|
|
73
|
+
root = nil
|
|
74
|
+
doc.root.children.each do |child|
|
|
75
|
+
next unless child.name == 'artists'
|
|
76
|
+
root = child
|
|
77
|
+
end
|
|
78
|
+
total_pages = root['totalPages'].to_i
|
|
79
|
+
root.children.each do |child|
|
|
80
|
+
next unless child.name == 'artist'
|
|
81
|
+
artists << Scrobbler::Artist.new_from_libxml(child)
|
|
82
|
+
end
|
|
83
|
+
for i in 2..total_pages do
|
|
84
|
+
options[:page] = i
|
|
85
|
+
artists.concat get_response('library.getartists', :none, 'artists', 'artist', options, true)
|
|
86
|
+
end
|
|
87
|
+
else
|
|
88
|
+
artists = get_response('library.getartists', :get_albums, 'artists', 'artist', options, true)
|
|
89
|
+
end
|
|
90
|
+
artists
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# A list of all the tracks in a user's library, with play counts and tag
|
|
94
|
+
# counts.
|
|
95
|
+
def tracks(options={})
|
|
96
|
+
options = {:force => false, :all => true}.merge options
|
|
97
|
+
options[:user] = @user.name
|
|
98
|
+
tracks = []
|
|
99
|
+
if options[:all]
|
|
100
|
+
doc = Base.request('library.gettracks', options)
|
|
101
|
+
root = nil
|
|
102
|
+
doc.root.children.each do |child|
|
|
103
|
+
next unless child.name == 'tracks'
|
|
104
|
+
root = child
|
|
105
|
+
end
|
|
106
|
+
total_pages = root['totalPages'].to_i
|
|
107
|
+
root.children.each do |child|
|
|
108
|
+
next unless child.name == 'track'
|
|
109
|
+
tracks << Scrobbler::Track.new_from_libxml(child)
|
|
110
|
+
end
|
|
111
|
+
for i in 2..total_pages do
|
|
112
|
+
options[:page] = i
|
|
113
|
+
tracks.concat get_response('library.gettracks', :none, 'tracks', 'track', options, true)
|
|
114
|
+
end
|
|
115
|
+
else
|
|
116
|
+
tracks = get_response('library.gettracks', :get_albums, 'tracks', 'track', options, true)
|
|
117
|
+
end
|
|
118
|
+
tracks
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
@@ -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?(&:empty?)
|
|
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,45 @@
|
|
|
1
|
+
module Scrobbler
|
|
2
|
+
# @todo everything
|
|
3
|
+
class Playlist < Base
|
|
4
|
+
# Load Helper modules
|
|
5
|
+
include ImageObjectFuncs
|
|
6
|
+
extend ImageClassFuncs
|
|
7
|
+
|
|
8
|
+
attr_reader :url, :id, :title, :date, :creator
|
|
9
|
+
attr_reader :description, :size, :duration, :streamable
|
|
10
|
+
class << self
|
|
11
|
+
def new_from_libxml(xml)
|
|
12
|
+
data = {}
|
|
13
|
+
|
|
14
|
+
xml.children.each do |child|
|
|
15
|
+
data[:id] = child.content.to_i if child.name == 'id'
|
|
16
|
+
data[:title] = child.content if child.name == 'title'
|
|
17
|
+
|
|
18
|
+
maybe_image_node(data, child)
|
|
19
|
+
data[:date] = Time.parse(child.content) if child.name == 'date'
|
|
20
|
+
|
|
21
|
+
data[:size] = child.content.to_i if child.name == 'size'
|
|
22
|
+
data[:description] = child.content if child.name == 'description'
|
|
23
|
+
data[:duration] = child.content.to_i if child.name == 'duration'
|
|
24
|
+
|
|
25
|
+
if child.name == 'streamable'
|
|
26
|
+
if ['1', 'true'].include?(child.content)
|
|
27
|
+
data[:streamable] = true
|
|
28
|
+
else
|
|
29
|
+
data[:streamable] = false
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
data[:creator] = child.content if child.name == 'creator'
|
|
33
|
+
data[:url] = child.content if child.name == 'url'
|
|
34
|
+
end
|
|
35
|
+
Playlist.new(data[:url],data)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def initialize(url,data={})
|
|
40
|
+
@url = url
|
|
41
|
+
populate_data(data)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
@@ -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
|