hornairs-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.
- data/.gitignore +13 -0
- data/.loadpath +5 -0
- data/History.txt +5 -0
- data/MIT-LICENSE +19 -0
- data/Manifest +75 -0
- data/README.rdoc +104 -0
- data/Rakefile +10 -0
- data/VERSION.yml +5 -0
- data/examples/album.rb +20 -0
- data/examples/artist.rb +13 -0
- data/examples/playlist.rb +7 -0
- data/examples/scrobble.rb +31 -0
- data/examples/tag.rb +11 -0
- data/examples/track.rb +12 -0
- data/examples/user.rb +15 -0
- data/lib/scrobbler.rb +33 -0
- data/lib/scrobbler/album.rb +181 -0
- data/lib/scrobbler/artist.rb +165 -0
- data/lib/scrobbler/auth.rb +47 -0
- data/lib/scrobbler/base.rb +105 -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 +112 -0
- data/lib/scrobbler/playing.rb +49 -0
- data/lib/scrobbler/playlist.rb +87 -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 +152 -0
- data/lib/scrobbler/user.rb +222 -0
- data/lib/scrobbler/venue.rb +67 -0
- data/setup.rb +1585 -0
- data/tasks/jeweler.rake +15 -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 +52 -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 +265 -0
|
@@ -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,112 @@
|
|
|
1
|
+
module Scrobbler
|
|
2
|
+
# @todo everything
|
|
3
|
+
class Library < Base
|
|
4
|
+
attr_reader :user
|
|
5
|
+
|
|
6
|
+
def initialize(user)
|
|
7
|
+
@user = user if user.class == Scrobbler::User
|
|
8
|
+
@user = Scrobbler::User.new(user.to_s) unless user.class == Scrobbler::User
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def add_album
|
|
12
|
+
# This function require authentication, but SimpleAuth is not yet 2.0
|
|
13
|
+
raise NotImplementedError
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def add_artist
|
|
17
|
+
# This function require authentication, but SimpleAuth is not yet 2.0
|
|
18
|
+
raise NotImplementedError
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def add_track
|
|
22
|
+
# This function require authentication, but SimpleAuth is not yet 2.0
|
|
23
|
+
raise NotImplementedError
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# A list of all the albums in a user's library, with play counts and tag
|
|
27
|
+
# counts.
|
|
28
|
+
def albums(options={})
|
|
29
|
+
options = {:force => false, :all => true}.merge options
|
|
30
|
+
options[:user] = @user.name
|
|
31
|
+
albums = []
|
|
32
|
+
if options[:all]
|
|
33
|
+
doc = Base.request('library.getalbums', options)
|
|
34
|
+
root = nil
|
|
35
|
+
doc.root.children.each do |child|
|
|
36
|
+
next unless child.name == 'albums'
|
|
37
|
+
root = child
|
|
38
|
+
end
|
|
39
|
+
total_pages = root['totalPages'].to_i
|
|
40
|
+
root.children.each do |child|
|
|
41
|
+
next unless child.name == 'album'
|
|
42
|
+
albums << Scrobbler::Album.new_from_libxml(child)
|
|
43
|
+
end
|
|
44
|
+
for i in 2..total_pages do
|
|
45
|
+
options[:page] = i
|
|
46
|
+
albums.concat get_response('library.getalbums', :none, 'albums', 'album', options, true)
|
|
47
|
+
end
|
|
48
|
+
else
|
|
49
|
+
albums = get_response('library.getalbums', :get_albums, 'albums', 'album', options, true)
|
|
50
|
+
end
|
|
51
|
+
albums
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# A list of all the artists in a user's library, with play counts and tag
|
|
55
|
+
# counts.
|
|
56
|
+
def artists(options={})
|
|
57
|
+
options = {:force => false, :all => true}.merge options
|
|
58
|
+
options[:user] = @user.name
|
|
59
|
+
artists = []
|
|
60
|
+
if options[:all]
|
|
61
|
+
doc = Base.request('library.getartists', options)
|
|
62
|
+
root = nil
|
|
63
|
+
doc.root.children.each do |child|
|
|
64
|
+
next unless child.name == 'artists'
|
|
65
|
+
root = child
|
|
66
|
+
end
|
|
67
|
+
total_pages = root['totalPages'].to_i
|
|
68
|
+
root.children.each do |child|
|
|
69
|
+
next unless child.name == 'artist'
|
|
70
|
+
artists << Scrobbler::Artist.new_from_libxml(child)
|
|
71
|
+
end
|
|
72
|
+
for i in 2..total_pages do
|
|
73
|
+
options[:page] = i
|
|
74
|
+
artists.concat get_response('library.getartists', :none, 'artists', 'artist', options, true)
|
|
75
|
+
end
|
|
76
|
+
else
|
|
77
|
+
artists = get_response('library.getartists', :get_albums, 'artists', 'artist', options, true)
|
|
78
|
+
end
|
|
79
|
+
artists
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# A list of all the tracks in a user's library, with play counts and tag
|
|
83
|
+
# counts.
|
|
84
|
+
def tracks(options={})
|
|
85
|
+
options = {:force => false, :all => true}.merge options
|
|
86
|
+
options[:user] = @user.name
|
|
87
|
+
tracks = []
|
|
88
|
+
if options[:all]
|
|
89
|
+
doc = Base.request('library.gettracks', options)
|
|
90
|
+
root = nil
|
|
91
|
+
doc.root.children.each do |child|
|
|
92
|
+
next unless child.name == 'tracks'
|
|
93
|
+
root = child
|
|
94
|
+
end
|
|
95
|
+
total_pages = root['totalPages'].to_i
|
|
96
|
+
root.children.each do |child|
|
|
97
|
+
next unless child.name == 'track'
|
|
98
|
+
tracks << Scrobbler::Track.new_from_libxml(child)
|
|
99
|
+
end
|
|
100
|
+
for i in 2..total_pages do
|
|
101
|
+
options[:page] = i
|
|
102
|
+
tracks.concat get_response('library.gettracks', :none, 'tracks', 'track', options, true)
|
|
103
|
+
end
|
|
104
|
+
else
|
|
105
|
+
tracks = get_response('library.gettracks', :get_albums, 'tracks', 'track', options, true)
|
|
106
|
+
end
|
|
107
|
+
tracks
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
@@ -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,87 @@
|
|
|
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
|
+
|
|
12
|
+
def url_for_album(album_id)
|
|
13
|
+
"lastfm://playlist/album/"+album_id.to_s
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def url_for_playlist(playlist_id)
|
|
17
|
+
"lastfm://playlist/album/"+playlist_id.to_s
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def new_from_album(album)
|
|
21
|
+
pp album.class
|
|
22
|
+
raise ArgumentError, "Must create a playlist from an album object" if album.class != Scrobbler::Album
|
|
23
|
+
Playlist.new(url_for_album(album.album_id))
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def new_from_xml(xml)
|
|
27
|
+
data = data_from_xml(xml)
|
|
28
|
+
puts "Creating Playlist: #{data[:title]}"
|
|
29
|
+
return nil if data[:title].nil?
|
|
30
|
+
Playlist.new(data[:url],data)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def data_from_xml(xml, o = {})
|
|
34
|
+
o = {:include_track_info => true}.merge(o)
|
|
35
|
+
data = {}
|
|
36
|
+
xml.children.each do |child|
|
|
37
|
+
data[:id] = child.content.to_i if child.name == 'id'
|
|
38
|
+
data[:title] = child.content if child.name == 'title'
|
|
39
|
+
|
|
40
|
+
maybe_image_node(data, child)
|
|
41
|
+
data[:date] = Time.parse(child.content) if child.name == 'date'
|
|
42
|
+
|
|
43
|
+
data[:size] = child.content.to_i if child.name == 'size'
|
|
44
|
+
data[:description] = child.content if child.name == 'description'
|
|
45
|
+
data[:duration] = child.content.to_i if child.name == 'duration'
|
|
46
|
+
|
|
47
|
+
if child.name == 'streamable'
|
|
48
|
+
if ['1', 'true'].include?(child.content)
|
|
49
|
+
data[:streamable] = true
|
|
50
|
+
else
|
|
51
|
+
data[:streamable] = false
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
if child.name == 'trackList'
|
|
56
|
+
data[:tracks] = []
|
|
57
|
+
child.children.each do |grandchild|
|
|
58
|
+
data[:tracks] << Track.new_from_xml(grandchild, o) if grandchild.name == 'track'
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
data[:creator] = child.content if child.name == 'creator'
|
|
62
|
+
data[:url] = child.content if child.name == 'url'
|
|
63
|
+
end
|
|
64
|
+
data
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def initialize(url,data={})
|
|
69
|
+
@url = url
|
|
70
|
+
fetch() unless data[:include_info] && data[:include_info] == false
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def fetch()
|
|
74
|
+
#Since playlists aren't static in the same sense the other objects are, dont cache results, always fetch the lastest version.
|
|
75
|
+
xml = Base.request('playlist.fetch', {:playlistURL => @url});
|
|
76
|
+
unless xml.root['status'] == 'failed'
|
|
77
|
+
xml.root.children.each do |child|
|
|
78
|
+
next unless child.name == 'playlist'
|
|
79
|
+
data = self.class.data_from_xml(child, {:include_track_info => false, :include_album_info => false})
|
|
80
|
+
populate_data(data)
|
|
81
|
+
break
|
|
82
|
+
end # xml.children.each do |child|
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
@@ -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,116 @@
|
|
|
1
|
+
# exception definitions
|
|
2
|
+
class BadAuthError < StandardError; end
|
|
3
|
+
class BadSessionError < StandardError; end
|
|
4
|
+
class BadTimeError < StandardError; end
|
|
5
|
+
class ClientBannedError < StandardError; end
|
|
6
|
+
class RequestFailedError < StandardError; end
|
|
7
|
+
|
|
8
|
+
module Scrobbler
|
|
9
|
+
class Scrobble
|
|
10
|
+
SUBMISSION_HOST = 'http://post.audioscrobbler.com:80/'
|
|
11
|
+
SUBMISSION_PORT = 80
|
|
12
|
+
SUBMISSION_VERSION = '1.2.1'
|
|
13
|
+
|
|
14
|
+
@@client_id = 'tst'
|
|
15
|
+
@@client_ver = '1.0'
|
|
16
|
+
@@default_options = {
|
|
17
|
+
:handshake_on_init => true
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
def Scrobble.client_id=(cid)
|
|
21
|
+
@@client_id = cid
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def Scrobble.client_ver=(cver)
|
|
25
|
+
@@client_ver = cver
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def initialize(options={})
|
|
29
|
+
params = @@default_options.merge(options)
|
|
30
|
+
warn 'Warning: Default client id used, this is for testing only' if @@client_id == 'tst'
|
|
31
|
+
raise ArgumentError, 'Session is required' if params[:session].nil?
|
|
32
|
+
raise ArgumentError, 'User is required' if params[:user].nil?
|
|
33
|
+
@session = params[:session]
|
|
34
|
+
@user = params[:user]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def handshake
|
|
38
|
+
ts = Time.now.to_i.to_s
|
|
39
|
+
auth_token = Digest::MD5.hexdigest(@session.key + ts)
|
|
40
|
+
params = {
|
|
41
|
+
:hs => 'true',
|
|
42
|
+
:ps => SUBMISSION_VERSION,
|
|
43
|
+
:c => @@client_id,
|
|
44
|
+
:v => @@client_ver,
|
|
45
|
+
:u => @user.name,
|
|
46
|
+
:t => ts,
|
|
47
|
+
:a => auth_token,
|
|
48
|
+
:sk => @session.key
|
|
49
|
+
}
|
|
50
|
+
url = URI.join(SUBMISSION_HOST)
|
|
51
|
+
query = []
|
|
52
|
+
params.each do |k,v|
|
|
53
|
+
query << [Base.sanitize(k), Base.sanitize(v)]
|
|
54
|
+
end
|
|
55
|
+
url.query = query.join('&')
|
|
56
|
+
req = Net::HTTP::Get.new(url.request_uri)
|
|
57
|
+
res = Net::HTTP.start(SUBMISSION_HOST, SUBMISSION_PORT) do |conn|
|
|
58
|
+
conn.request(req)
|
|
59
|
+
end
|
|
60
|
+
res = res.body.split("\n")
|
|
61
|
+
if res[0] == 'OK'
|
|
62
|
+
# Handshake done, parse and return information
|
|
63
|
+
@session_id = res[1]
|
|
64
|
+
@now_playing_url = res[2]
|
|
65
|
+
@submission_url = res[3]
|
|
66
|
+
elsif res[0] == 'BANNED'
|
|
67
|
+
# This indicates that this client version has been banned from the
|
|
68
|
+
# server. This usually happens if the client is violating the protocol
|
|
69
|
+
# in a destructive way. Users should be asked to upgrade their client
|
|
70
|
+
# application.
|
|
71
|
+
raise ClientBannedError, 'Please update your client to a newer version.'
|
|
72
|
+
elsif res[0] == 'BADAUTH'
|
|
73
|
+
# This indicates that the authentication details provided were incorrect.
|
|
74
|
+
# The client should not retry the handshake until the user has changed
|
|
75
|
+
# their details
|
|
76
|
+
raise BadAuthError
|
|
77
|
+
elsif res[0] == 'BADTIME'
|
|
78
|
+
# The timestamp provided was not close enough to the current time.
|
|
79
|
+
# The system clock must be corrected before re-handshaking.
|
|
80
|
+
raise BadTimeError, "Not close enough to current time: #{ts}"
|
|
81
|
+
else
|
|
82
|
+
# This indicates a temporary server failure. The reason indicates the
|
|
83
|
+
# cause of the failure. The client should proceed as directed in the
|
|
84
|
+
# failure handling section
|
|
85
|
+
raise RequestFailedError, res[0]
|
|
86
|
+
end
|
|
87
|
+
end #^ handshake
|
|
88
|
+
|
|
89
|
+
def now_playing(track)
|
|
90
|
+
params = {
|
|
91
|
+
's' => @session_id,
|
|
92
|
+
't' => track.name,
|
|
93
|
+
'a' => track.artist.name,
|
|
94
|
+
'b' => '',
|
|
95
|
+
'l' => '',
|
|
96
|
+
'n' => '',
|
|
97
|
+
'm' => ''
|
|
98
|
+
}
|
|
99
|
+
params['b'] = track.album.name unless track.album.nil?
|
|
100
|
+
params['l'] = track.duration.to_s unless track.duration.nil? && track.duration > 30
|
|
101
|
+
# @todo params['n']
|
|
102
|
+
params['m'] = track.mbid unless track.mbid.nil?
|
|
103
|
+
res = Net::HTTP.post_form(URI.parse(SUBMISSION_HOST), params).body.split("\n")
|
|
104
|
+
if res[0] == 'BADSESSION'
|
|
105
|
+
# This indicates that the Session ID sent was somehow invalid, possibly
|
|
106
|
+
# because another client has performed a handshake for this user. On
|
|
107
|
+
# receiving this, the client should re-handshake with the server before
|
|
108
|
+
# continuing.
|
|
109
|
+
raise BadSessionError
|
|
110
|
+
end
|
|
111
|
+
end #^ now_playing
|
|
112
|
+
|
|
113
|
+
def submit(tracks={})
|
|
114
|
+
end #^ submit
|
|
115
|
+
end
|
|
116
|
+
end
|