bts-mbws 0.0.1
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/COPYING +339 -0
- data/INSTALL +3 -0
- data/README +44 -0
- data/Rakefile +33 -0
- data/bin/console +3 -0
- data/bin/setup.rb +3 -0
- data/lib/mbws.rb +19 -0
- data/lib/mbws/artist.rb +84 -0
- data/lib/mbws/artist_relations.rb +6 -0
- data/lib/mbws/base.rb +7 -0
- data/lib/mbws/exceptions.rb +25 -0
- data/lib/mbws/label.rb +35 -0
- data/lib/mbws/label_relations.rb +6 -0
- data/lib/mbws/parsing.rb +43 -0
- data/lib/mbws/relations.rb +43 -0
- data/lib/mbws/release.rb +78 -0
- data/lib/mbws/release_relations.rb +6 -0
- data/lib/mbws/request.rb +45 -0
- data/lib/mbws/resource.rb +117 -0
- data/lib/mbws/track.rb +57 -0
- data/lib/mbws/track_relations.rb +6 -0
- data/lib/mbws/url_relations.rb +14 -0
- data/lib/mbws/version.rb +10 -0
- data/test/artist_test.rb +114 -0
- data/test/label_test.rb +32 -0
- data/test/release_test.rb +55 -0
- data/test/request_test.rb +12 -0
- data/test/test_helper.rb +7 -0
- data/test/track_test.rb +26 -0
- data/test/url_relations_test.rb +19 -0
- metadata +105 -0
data/lib/mbws/artist.rb
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
module MBWS
|
2
|
+
# Top of the artist documentation page
|
3
|
+
class Artist < Resource
|
4
|
+
|
5
|
+
attr_accessor :name,:sort_name,:type,:begin,:end,:disambiguation
|
6
|
+
|
7
|
+
def initialize(hash,options = {})
|
8
|
+
@aliases = nil
|
9
|
+
|
10
|
+
super
|
11
|
+
|
12
|
+
attrs = ["name","type","disambiguation","sort-name"]
|
13
|
+
attrs.each do |a|
|
14
|
+
self.instance_variable_set("@#{a.gsub('-','_')}",hash[a])
|
15
|
+
end
|
16
|
+
|
17
|
+
@begin = hash["life-span"]["begin"] if hash["life-span"]
|
18
|
+
@end = hash["life-span"]["end"] if hash["life-span"]
|
19
|
+
|
20
|
+
end
|
21
|
+
|
22
|
+
def aliases
|
23
|
+
return @aliases if @aliases
|
24
|
+
artist_xml = Request.get("artist/#{self.mid}",:inc => "aliases")
|
25
|
+
parse_aliases(artist_xml["artist"][0])
|
26
|
+
@aliases
|
27
|
+
end
|
28
|
+
|
29
|
+
# Return releases where artist is the primary artist (Single Artist Albums).
|
30
|
+
#
|
31
|
+
# Cached
|
32
|
+
def releases()
|
33
|
+
@releases if @releases
|
34
|
+
@releases = Release.find(:artistid => self.mid, :inc => "counts",:limit => 100)
|
35
|
+
end
|
36
|
+
|
37
|
+
#
|
38
|
+
def releases_find(options = {})
|
39
|
+
options.merge!(:artistid => self.mid, :inc => "counts")
|
40
|
+
Release.find(options)
|
41
|
+
end
|
42
|
+
|
43
|
+
#
|
44
|
+
def va_releases(options)
|
45
|
+
_inc_releases("va-",options)
|
46
|
+
end
|
47
|
+
|
48
|
+
def sa_releases(options)
|
49
|
+
_inc_releases("sa-",options)
|
50
|
+
end
|
51
|
+
|
52
|
+
def all_va_releases
|
53
|
+
_all_inc_releases("va-")
|
54
|
+
end
|
55
|
+
|
56
|
+
def all_sa_releases
|
57
|
+
_all_inc_releases("sa-")
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
def _inc_releases(prefix,options)
|
62
|
+
inc = []
|
63
|
+
[:type, :status].each do |k|
|
64
|
+
inc.push(prefix + options[k].to_s) if options.include?(k)
|
65
|
+
end
|
66
|
+
|
67
|
+
releases = []
|
68
|
+
xml = Request.get("artist/#{self.mid}", :inc => inc)
|
69
|
+
xml["artist"][0]["release-list"]["release"].each do |rxml|
|
70
|
+
releases.push(Release.new(rxml,{}))
|
71
|
+
end if ["artist"][0]["release-list"]
|
72
|
+
releases
|
73
|
+
end
|
74
|
+
|
75
|
+
def _all_inc_releases(prefix)
|
76
|
+
all = []
|
77
|
+
[:Official, :Promotion, :Bootleg, :PseudoRelease].each do |status|
|
78
|
+
sleep 1
|
79
|
+
all.push(_inc_releases(prefix,:status => status))
|
80
|
+
end
|
81
|
+
all.compact
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
data/lib/mbws/base.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
module MBWS
|
2
|
+
class MBWSException < StandardError
|
3
|
+
end
|
4
|
+
|
5
|
+
class ResponseError < MBWSException
|
6
|
+
end
|
7
|
+
|
8
|
+
class MBIDNotFound < ResponseError
|
9
|
+
end
|
10
|
+
|
11
|
+
class BadRequest < ResponseError
|
12
|
+
end
|
13
|
+
|
14
|
+
class InvalidUUID < MBWSException
|
15
|
+
def initialize(invalid_id)
|
16
|
+
message = "'#{invalid_id} is not a valid MBID (UUID). " +
|
17
|
+
"MBIDs must be 36 characters long " +
|
18
|
+
"(ex. c0b2500e-0cef-4130-869d-732b23ed9df5). " +
|
19
|
+
"Please consult the Web Service Documentation " +
|
20
|
+
"(http://musicbrainz.org/doc/XMLWebService) for"+
|
21
|
+
" more information. "
|
22
|
+
super(message)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
data/lib/mbws/label.rb
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
module MBWS
|
2
|
+
class Label < Resource
|
3
|
+
attr_accessor :name,:type,:disambiguation,:begin,:end,:label_code,:country
|
4
|
+
|
5
|
+
def initialize(hash,options)
|
6
|
+
@aliases = nil
|
7
|
+
|
8
|
+
super
|
9
|
+
|
10
|
+
attrs = ["name","type","disambiguation","label-code","country"]
|
11
|
+
attrs.each do |a|
|
12
|
+
self.instance_variable_set("@#{a.gsub('-','_')}",hash[a])
|
13
|
+
end
|
14
|
+
|
15
|
+
if options.has_key?(:inc)
|
16
|
+
options[:inc].map {|i| i.gsub(/-/,"_")}.each do |s|
|
17
|
+
self.send "parse_" + s,hash unless s.match(/rels/)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
@begin = hash["life-span"]["begin"] if hash["life-span"]
|
22
|
+
@end = hash["life-span"]["end"] if hash["life-span"]
|
23
|
+
end
|
24
|
+
|
25
|
+
def aliases
|
26
|
+
return @aliases if @aliases
|
27
|
+
label_xml = Request.get("label/#{self.mid}",:inc => "aliases")
|
28
|
+
parse_aliases(label_xml["label"][0])
|
29
|
+
@aliases
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
data/lib/mbws/parsing.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
#:stopdoc:
|
2
|
+
# A lot of this is based on...errr taken...from the AWS::S3 module (http://amazon.rubyforge.org/)
|
3
|
+
# by Marcel Molina <marcel@vernix.org>
|
4
|
+
module MBWS
|
5
|
+
module Parsing
|
6
|
+
class XmlParser < Hash
|
7
|
+
|
8
|
+
attr_reader :body, :xml_in, :root
|
9
|
+
|
10
|
+
def initialize(body)
|
11
|
+
@body = body
|
12
|
+
update(parse)
|
13
|
+
# set_root
|
14
|
+
# typecast_xml_in
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def parse
|
20
|
+
@xml_in = XmlSimple.xml_in(@body, parsing_options)
|
21
|
+
end
|
22
|
+
|
23
|
+
def parsing_options
|
24
|
+
{
|
25
|
+
# Force nested elements to be put into an array, even if there is only on of them.
|
26
|
+
'forcearray' => ['relation','artist','release','relation-list','disc','track','label','url'],
|
27
|
+
|
28
|
+
'KeyAttr' => ['target-type']
|
29
|
+
}
|
30
|
+
end
|
31
|
+
def set_root
|
32
|
+
@root = @xml_in.keys.first.underscore
|
33
|
+
end
|
34
|
+
def typecast_xml_in
|
35
|
+
typecast_xml = {}
|
36
|
+
@xml_in.dup.each do |k,v|
|
37
|
+
#typecast_xml[k.underscore] = typecast(v)
|
38
|
+
end
|
39
|
+
update(typecast_xml[root]) if typecase_xml[root].is_a?(Hash)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module MBWS
|
2
|
+
module Relations
|
3
|
+
class RelationsBase < Array
|
4
|
+
|
5
|
+
def initialize(xml = {})
|
6
|
+
parse(xml)
|
7
|
+
end
|
8
|
+
|
9
|
+
def relations_by_type(type)
|
10
|
+
rels = []
|
11
|
+
self.each do |r|
|
12
|
+
rels.push(r) if r[:type] == type
|
13
|
+
end
|
14
|
+
rels
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def parse(xml)
|
20
|
+
klass_str = self.class.to_s.split("::")[2].gsub(/Relations$/,'')
|
21
|
+
klass = MBWS.const_get(klass_str)
|
22
|
+
obj_sym = klass_str.downcase.to_sym
|
23
|
+
|
24
|
+
xml["relation"].each do |r|
|
25
|
+
h = Hash.new
|
26
|
+
|
27
|
+
h[:type] = r["type"]
|
28
|
+
h[:direction] = r["direction"] if r["direction"]
|
29
|
+
|
30
|
+
# Url's don't have their own tag
|
31
|
+
if obj_sym == :url
|
32
|
+
h[obj_sym] = klass.new(r)
|
33
|
+
else
|
34
|
+
h[obj_sym] = klass.new(r[klass_str.downcase][0])
|
35
|
+
end
|
36
|
+
|
37
|
+
self.push(h)
|
38
|
+
end if xml["relation"]
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
data/lib/mbws/release.rb
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
module MBWS
|
2
|
+
class Release < Resource
|
3
|
+
|
4
|
+
attr_accessor :title,:mid,:type,:status,:release_events,:asin
|
5
|
+
|
6
|
+
def initialize(hash,options = {})
|
7
|
+
@discs = nil
|
8
|
+
@discs_count = nil
|
9
|
+
@tracks_count = nil
|
10
|
+
@artist = nil
|
11
|
+
@tracks = nil
|
12
|
+
@release_events = nil
|
13
|
+
|
14
|
+
super
|
15
|
+
|
16
|
+
@title = hash["title"]
|
17
|
+
@asin = hash["asin"]
|
18
|
+
@mid = hash["id"]
|
19
|
+
@type = hash["type"].split(" ")[0] if hash["type"]
|
20
|
+
@status = hash["type"].split(" ")[1] if hash["type"]
|
21
|
+
end
|
22
|
+
|
23
|
+
def artist
|
24
|
+
return @artist if @artist
|
25
|
+
release_xml = Request.get("release/#{self.mid}",:inc => "artist")
|
26
|
+
parse_artist(release_xml["release"][0])
|
27
|
+
end
|
28
|
+
|
29
|
+
def discs
|
30
|
+
return @discs if @discs
|
31
|
+
release_xml = Request.get("release/#{self.mid}",:inc => "discs")
|
32
|
+
parse_discs(release_xml["release"][0])
|
33
|
+
end
|
34
|
+
|
35
|
+
def tracks
|
36
|
+
return @tracks if @tracks
|
37
|
+
release_xml = Request.get("release/#{self.mid}",:inc => "tracks")
|
38
|
+
parse_tracks(release_xml["release"][0])
|
39
|
+
@tracks
|
40
|
+
end
|
41
|
+
|
42
|
+
def tracks_count
|
43
|
+
return @tracks_count if @tracks_count
|
44
|
+
nil
|
45
|
+
#release_xml = Request.get("release/#{self.mid}",:inc => "counts")
|
46
|
+
#parse_counts(release_xml["release"][0])
|
47
|
+
#@tracks_count
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def parse_counts(xml)
|
53
|
+
@tracks_count = xml["track-list"]["count"]
|
54
|
+
end
|
55
|
+
|
56
|
+
def parse_discs(xml)
|
57
|
+
@discs = []
|
58
|
+
xml["disc-list"]["disc"].each do |d|
|
59
|
+
@discs.push({:sectors => d["sectors"], :id => d["id"]})
|
60
|
+
end
|
61
|
+
@discs_count = @discs.size
|
62
|
+
end
|
63
|
+
|
64
|
+
def parse_tracks(xml)
|
65
|
+
@tracks = []
|
66
|
+
xml["track-list"]["track"].each do |t|
|
67
|
+
@tracks.push(Track.new(t))
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def parse_release_events(xml)
|
72
|
+
@release_events = []
|
73
|
+
xml["release-event-list"]["event"].each do |re|
|
74
|
+
@release_events.push(re)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
data/lib/mbws/request.rb
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
module MBWS
|
2
|
+
class Request
|
3
|
+
@@requests = []
|
4
|
+
def self.get(path,options = {})
|
5
|
+
# MB limits us to 13 requests every 10 seconds.
|
6
|
+
# We police ourselves a bit stricter to make certain
|
7
|
+
# that we don't exceed this limit.
|
8
|
+
#
|
9
|
+
# Another way to do this would be to pace ourselves
|
10
|
+
# so that there is always 10/13 of a second between
|
11
|
+
# requests but this method allows short bursts
|
12
|
+
|
13
|
+
# Delete requests if they happened more than 11 seconds ago
|
14
|
+
now = Time.now
|
15
|
+
@@requests.delete_if{ |t| (now - t) > 11}
|
16
|
+
|
17
|
+
# If there are 11 or more requests sleep until
|
18
|
+
# only 10 have occured in the past 10 seconds
|
19
|
+
if @@requests.size > 10
|
20
|
+
interval = 10 - (now - @@requests[-11])
|
21
|
+
sleep interval if interval > 0
|
22
|
+
end
|
23
|
+
|
24
|
+
opts = options.collect {|k,v|
|
25
|
+
if k == :inc and v.class == Array
|
26
|
+
k.to_s + "=" + v.join("+")
|
27
|
+
else
|
28
|
+
k.to_s + "=" + v.to_s
|
29
|
+
end
|
30
|
+
}.join("&")
|
31
|
+
path += "?type=xml&" + opts
|
32
|
+
http = Net::HTTP.new('musicbrainz.org',80)
|
33
|
+
http.start do
|
34
|
+
request = Net::HTTP::Get.new('/ws/1/' + URI.escape(path))
|
35
|
+
@@requests.push(Time.now)
|
36
|
+
res = http.request(request)
|
37
|
+
if res.code == 404
|
38
|
+
raise MBIDNotFound
|
39
|
+
else
|
40
|
+
return Parsing::XmlParser.new(res.body)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
module MBWS #:nodoc:
|
2
|
+
class Resource
|
3
|
+
include Relations
|
4
|
+
|
5
|
+
attr_accessor :mid, :score
|
6
|
+
|
7
|
+
def initialize(hash,options)
|
8
|
+
@mid = hash["id"]
|
9
|
+
@score = hash["ext:score"]
|
10
|
+
@artist_rels = nil
|
11
|
+
@url_rels = nil
|
12
|
+
@track_rels = nil
|
13
|
+
@release_rels = nil
|
14
|
+
if options.has_key?(:inc)
|
15
|
+
options[:inc].map {|i| i.to_s.gsub(/-/,"_")}.each do |s|
|
16
|
+
self.send "parse_" + s,hash #if s.match(/rels/)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.find(*args)
|
22
|
+
case args[0]
|
23
|
+
when String
|
24
|
+
self.find_by_mbid(args)
|
25
|
+
when Hash
|
26
|
+
self.find_by_query(args)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# Relations
|
31
|
+
|
32
|
+
def track_rels
|
33
|
+
return @track_rels if @track_rels
|
34
|
+
xml = Request.get("#{self.resource_name}/#{self.mid}",:inc => "track-rels")
|
35
|
+
parse_track_rels(xml[self.resource_name][0])
|
36
|
+
@track_rels
|
37
|
+
end
|
38
|
+
|
39
|
+
def url_rels
|
40
|
+
return @url_rels if @url_rels
|
41
|
+
xml = Request.get("#{self.resource_name}/#{self.mid}",:inc => "url-rels")
|
42
|
+
parse_url_rels(xml[self.resource_name][0])
|
43
|
+
@url_rels
|
44
|
+
end
|
45
|
+
|
46
|
+
def artist_rels
|
47
|
+
return @artist_rels if @artist_rels
|
48
|
+
xml = Request.get("#{self.resource_name}/#{self.mid}",:inc => "artist-rels")
|
49
|
+
parse_artist_rels(xml[self.resource_name][0])
|
50
|
+
@artist_rels
|
51
|
+
end
|
52
|
+
|
53
|
+
def release_rels
|
54
|
+
return @release_rels if @release_rels
|
55
|
+
xml = Request.get("#{self.resource_name}/#{self.mid}",:inc => "release-rels")
|
56
|
+
parse_release_rels(xml[self.resource_name][0])
|
57
|
+
@release_rels
|
58
|
+
end
|
59
|
+
|
60
|
+
def resource_name
|
61
|
+
self.class.to_s.downcase.split("::")[1]
|
62
|
+
end
|
63
|
+
|
64
|
+
def self.resource_name
|
65
|
+
self.to_s.downcase.split("::")[1]
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
def self.find_by_mbid(args)
|
71
|
+
mid = args[0]
|
72
|
+
options = args[1] || {}
|
73
|
+
|
74
|
+
raise InvalidUUID.new(mid) unless mid =~ /^\w{8}-\w{4}-\w{4}-\w{4}-\w{12}$/
|
75
|
+
|
76
|
+
xml = Request.get("#{self.resource_name}/#{mid}",options)
|
77
|
+
return self.new(xml[self.resource_name][0],options)
|
78
|
+
end
|
79
|
+
|
80
|
+
def self.find_by_query(args)
|
81
|
+
options = args[0]
|
82
|
+
|
83
|
+
objs = []
|
84
|
+
|
85
|
+
xml = Request.get("#{self.resource_name}/",options)
|
86
|
+
xml[self.resource_name + "-list"][self.resource_name].each do |o|
|
87
|
+
objs.push(self.new(o,options))
|
88
|
+
end
|
89
|
+
objs.sort_by { |o| o.score }
|
90
|
+
objs
|
91
|
+
end
|
92
|
+
|
93
|
+
def parse_aliases(xml)
|
94
|
+
@aliases = []
|
95
|
+
xml["alias-list"]["alias"].each do |a|
|
96
|
+
@aliases.push(a)
|
97
|
+
end if xml["alias-list"]
|
98
|
+
end
|
99
|
+
|
100
|
+
def parse_artist(xml)
|
101
|
+
@artist = Artist.new(xml["artist"][0])
|
102
|
+
end
|
103
|
+
|
104
|
+
[:track, :artist, :release, :label, :url].each do |symbol|
|
105
|
+
class_eval(<<-EVAL,__FILE__,__LINE__)
|
106
|
+
def parse_#{symbol}_rels(xml)
|
107
|
+
if xml["relation-list"] && xml["relation-list"]["#{symbol.to_s.capitalize}"]
|
108
|
+
@#{symbol}_rels =
|
109
|
+
#{symbol.to_s.capitalize}Relations.new(xml["relation-list"]["#{symbol.to_s.capitalize}"])
|
110
|
+
else
|
111
|
+
@#{symbol}_rels = #{symbol.to_s.capitalize}Relations.new
|
112
|
+
end
|
113
|
+
end
|
114
|
+
EVAL
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|