sandro-metamuse 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. data/.document +5 -0
  2. data/.gitignore +6 -0
  3. data/MIT_LICENSE +20 -0
  4. data/README.rdoc +7 -0
  5. data/Rakefile +48 -0
  6. data/VERSION +1 -0
  7. data/lib/array_ext.rb +5 -0
  8. data/lib/arrayish.rb +23 -0
  9. data/lib/httparty_ext.rb +16 -0
  10. data/lib/metamuse.rb +71 -0
  11. data/lib/metamuse/album.rb +19 -0
  12. data/lib/metamuse/artist.rb +43 -0
  13. data/lib/metamuse/association.rb +48 -0
  14. data/lib/metamuse/collection.rb +25 -0
  15. data/lib/metamuse/services.rb +16 -0
  16. data/lib/metamuse/services/echonest.rb +26 -0
  17. data/lib/metamuse/services/freebase.rb +43 -0
  18. data/lib/metamuse/services/lastfm.rb +27 -0
  19. data/lib/metamuse/services/lastfm/image.rb +41 -0
  20. data/lib/metamuse/services/music_brainz.rb +27 -0
  21. data/lib/metamuse/track.rb +13 -0
  22. data/lib/object_ext.rb +11 -0
  23. data/metamuse.gemspec +89 -0
  24. data/script/console +8 -0
  25. data/spec/fake_object.rb +38 -0
  26. data/spec/fake_object_spec.rb +66 -0
  27. data/spec/metamuse/album_spec.rb +4 -0
  28. data/spec/metamuse/artist_spec.rb +99 -0
  29. data/spec/metamuse/association_spec.rb +74 -0
  30. data/spec/metamuse/collection_spec.rb +4 -0
  31. data/spec/metamuse/services/echonest_spec.rb +28 -0
  32. data/spec/metamuse/services/freebase_spec.rb +24 -0
  33. data/spec/metamuse/services/lastfm/image_spec.rb +71 -0
  34. data/spec/metamuse/services/lastfm_spec.rb +30 -0
  35. data/spec/metamuse/services/music_brainz_spec.rb +12 -0
  36. data/spec/metamuse_spec.rb +21 -0
  37. data/spec/spec_helper.rb +27 -0
  38. data/spec/web_fixtures/GET_developer.echonest.com-api-search_artists_17a9da1.fixture +36 -0
  39. data/spec/web_fixtures/GET_developer.echonest.com-api-search_artists_350c352.fixture +36 -0
  40. data/spec/web_fixtures/GET_musicbrainz.org-ws-1-release-bb32aa1d-f37b-4134-8c0e-b43b7a6dab85_b976ba7.fixture +26 -0
  41. data/spec/web_fixtures/GET_www.freebase.com-api-service-mqlread_60ee2bd.fixture +938 -0
  42. data/spec/web_fixtures/GET_www.freebase.com-api-service-mqlread_b8b2565.fixture +107 -0
  43. metadata +106 -0
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
@@ -0,0 +1,6 @@
1
+ *.sw?
2
+ .DS_Store
3
+ coverage
4
+ rdoc
5
+ pkg
6
+ .api_keys.yml
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Sandro Turriate
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,7 @@
1
+ = metamuse
2
+
3
+ Search for music metadata
4
+
5
+ == Copyright
6
+
7
+ Copyright (c) 2009 Sandro Turriate. See MIT_LICENSE for details.
@@ -0,0 +1,48 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "metamuse"
8
+ gem.summary = %Q{Search for music metadata}
9
+ gem.email = "sandro.turriate@gmail.com"
10
+ gem.homepage = "http://github.com/sandro/metamuse"
11
+ gem.authors = ["Sandro Turriate"]
12
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
13
+ end
14
+
15
+ rescue LoadError
16
+ puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
17
+ end
18
+
19
+ require 'spec/rake/spectask'
20
+ Spec::Rake::SpecTask.new(:spec) do |spec|
21
+ spec.libs << 'lib' << 'spec'
22
+ spec.spec_files = FileList['spec/**/*_spec.rb']
23
+ end
24
+
25
+ Spec::Rake::SpecTask.new(:rcov) do |spec|
26
+ spec.libs << 'lib' << 'spec'
27
+ spec.pattern = 'spec/**/*_spec.rb'
28
+ spec.rcov = true
29
+ end
30
+
31
+
32
+ task :default => :spec
33
+
34
+ require 'rake/rdoctask'
35
+ Rake::RDocTask.new do |rdoc|
36
+ if File.exist?('VERSION.yml')
37
+ config = YAML.load(File.read('VERSION.yml'))
38
+ version = "#{config[:major]}.#{config[:minor]}.#{config[:patch]}"
39
+ else
40
+ version = ""
41
+ end
42
+
43
+ rdoc.rdoc_dir = 'rdoc'
44
+ rdoc.title = "metamuse #{version}"
45
+ rdoc.rdoc_files.include('README*')
46
+ rdoc.rdoc_files.include('lib/**/*.rb')
47
+ end
48
+
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
@@ -0,0 +1,5 @@
1
+ class Array
2
+ def self.insist(thing)
3
+ thing.is_a?(Array) ? thing : [thing]
4
+ end
5
+ end
@@ -0,0 +1,23 @@
1
+ module Arrayish
2
+ # List of array methods (that are not in +Object+) that need to be
3
+ # delegated to +collection+.
4
+ ARRAY_METHODS = (Array.instance_methods - Object.instance_methods).map { |n| n.to_s }
5
+
6
+ # List of additional methods that must be delegated to +collection+.
7
+ MUST_DEFINE = %w[to_a to_ary inspect]
8
+
9
+ (ARRAY_METHODS + MUST_DEFINE).uniq.each do |method|
10
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
11
+ def #{method}(*args, &block) # def each(*args, &block)
12
+ collection.send(:#{method}, *args, &block) # collection.send(:each, *args, &block)
13
+ end # end
14
+ RUBY
15
+ end
16
+
17
+ # Lie about our class. Borrowed from Rake::FileList
18
+ # Note: Does not work for case equality (<tt>===</tt>)
19
+ def is_a?(klass)
20
+ klass == Array || super(klass)
21
+ end
22
+ alias kind_of? is_a?
23
+ end
@@ -0,0 +1,16 @@
1
+ HTTParty::Request.class_eval do
2
+ class << self
3
+ attr_accessor :debug
4
+ end
5
+
6
+ def perform_with_debug
7
+ if self.class.debug
8
+ puts "HTTParty making #{http_method::METHOD} request to:"
9
+ puts uri
10
+ end
11
+ perform_without_debug
12
+ end
13
+
14
+ alias_method :perform_without_debug, :perform
15
+ alias_method :perform, :perform_with_debug
16
+ end
@@ -0,0 +1,71 @@
1
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
2
+
3
+ begin; require 'rubygems'; rescue LoadError; end
4
+ require 'cgi'
5
+ require 'httparty'
6
+
7
+ require 'httparty_ext'
8
+ require 'object_ext'
9
+ require 'array_ext'
10
+ require 'arrayish'
11
+
12
+ require 'metamuse/collection'
13
+ require 'metamuse/association'
14
+ require 'metamuse/services'
15
+ require 'metamuse/services/echonest'
16
+ require 'metamuse/services/freebase'
17
+ require 'metamuse/services/music_brainz'
18
+ require 'metamuse/services/lastfm'
19
+ require 'metamuse/services/lastfm/image'
20
+
21
+ module Metamuse
22
+ autoload :Artist, 'metamuse/artist'
23
+ autoload :Album, 'metamuse/album'
24
+ autoload :Track, 'metamuse/track'
25
+
26
+ class << self
27
+ attr_reader :echonest_api_key, :lastfm_api_key
28
+
29
+ def echonest_api_key=(key)
30
+ @echonest_api_key = key
31
+ Services::Echonest.api_key = echonest_api_key
32
+ end
33
+
34
+ def lastfm_api_key=(key)
35
+ @lastfm_api_key = key
36
+ Services::Lastfm.api_key = lastfm_api_key
37
+ end
38
+
39
+ def find_artist(name)
40
+ Services::Echonest.artist(name)
41
+ end
42
+
43
+ private
44
+
45
+ def api_keys
46
+ if File.exists?(name=".api_keys.yml")
47
+ YAML.load_file name
48
+ else
49
+ {}
50
+ end
51
+ end
52
+
53
+ def register_api_keys
54
+ api_keys.each do |service, value|
55
+ send("#{service}_api_key=", value)
56
+ end
57
+ end
58
+ end
59
+
60
+ module InstanceInitialize
61
+ def initialize(attrs={})
62
+ attrs.each do |k,v|
63
+ if self.class.has_many_set.values.include?(k)
64
+ send "#{k}=", v
65
+ else
66
+ instance_variable_set "@#{k}", v
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,19 @@
1
+ module Metamuse
2
+ class Album
3
+ include InstanceInitialize
4
+ extend Association
5
+ has_many :tracks, Track
6
+
7
+ attr_accessor :name, :release_date, :freebase_id, :mbid, :rank, :images
8
+
9
+ def <=>(other)
10
+ if rank
11
+ rank <=> other.rank
12
+ else
13
+ to_s <=> other.to_s
14
+ end
15
+ end
16
+
17
+ end
18
+ end
19
+
@@ -0,0 +1,43 @@
1
+ module Metamuse
2
+ class Artist
3
+ include InstanceInitialize
4
+ extend Association
5
+ has_many :albums, Album
6
+
7
+ attr_accessor :name, :echonest_id, :freebase_guid, :mbid
8
+
9
+ def self.build(name)
10
+ artist = Services::Freebase.artist(name)
11
+ artist.enhance_albums!
12
+ artist
13
+ end
14
+
15
+ def tracks
16
+ @tracks ||= albums.map{|a| a.tracks}.flatten
17
+ end
18
+
19
+ def enhance_albums!
20
+ return albums if albums.empty?
21
+ self.albums = albums.map do |album|
22
+ lastfm_album = lastfm_albums.detect{|a| a.name == album.name}
23
+ if lastfm_album
24
+ album.rank = lastfm_album.rank
25
+ album.mbid = lastfm_album.mbid
26
+ album.images = lastfm_album.images
27
+ end
28
+ album
29
+ end.sort
30
+ end
31
+
32
+ def fetch_albums_and_tracks!
33
+ artist = Services::Freebase.artist(name)
34
+ self.albums = artist.albums
35
+ end
36
+
37
+ private
38
+
39
+ def lastfm_albums
40
+ @lastfm_albums ||= Services::Lastfm.top_albums(name)
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,48 @@
1
+ module Metamuse
2
+ module Association
3
+ def self.extended(base)
4
+ class << base
5
+ attr_accessor :has_many_set, :belongs_to_set
6
+ end
7
+ base.belongs_to_set = base.has_many_set = {}
8
+ base.send :include, InstanceMethods
9
+ end
10
+
11
+ def has_many(accessor, klass)
12
+ has_many_set.merge! klass => accessor
13
+
14
+ define_method(accessor) do
15
+ ivar = "@#{accessor}"
16
+ if val = instance_variable_get(ivar)
17
+ val
18
+ else
19
+ instance_variable_set(ivar, Collection.new(self, klass))
20
+ end
21
+ end
22
+
23
+ define_method("#{accessor}=") do |items|
24
+ send(accessor).clear
25
+ send(accessor) << items
26
+ end
27
+ end
28
+
29
+ def belongs_to(accessor, klass)
30
+ belongs_to_set.merge! klass => accessor
31
+
32
+ define_method(accessor) do
33
+ instance_variable_get(:"@#{accessor}")
34
+ end
35
+
36
+ define_method(:"#{accessor}=") do |parent|
37
+ instance_variable_set(:"@#{accessor}", parent)
38
+ end
39
+ end
40
+
41
+ module InstanceMethods
42
+ def belongs(owner)
43
+ accessor = self.class.belongs_to_set[owner.class]
44
+ send("#{accessor}=", owner) if accessor
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,25 @@
1
+ module Metamuse
2
+ class Collection
3
+ include ::Arrayish
4
+ attr_reader :collection, :collection_class, :owner
5
+
6
+ def initialize(owner, collection_class)
7
+ @owner = owner
8
+ @collection_class = collection_class
9
+ @collection = []
10
+ end
11
+
12
+ alias_method :append, :<<
13
+ alias_method :push, :<<
14
+ alias_method :concat, :<<
15
+
16
+ def <<(items)
17
+ Array.insist(items).each do |item|
18
+ item = collection_class.new(item) unless collection_class === item
19
+ append item
20
+ item.belongs owner
21
+ end
22
+ self
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,16 @@
1
+ module Metamuse
2
+ module Services
3
+ module Service
4
+ def has_api_key()
5
+ class << self
6
+ attr_reader :api_key
7
+
8
+ def api_key=(key)
9
+ @api_key = key
10
+ default_params :api_key => api_key
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,26 @@
1
+ module Metamuse
2
+ module Services
3
+ class Echonest
4
+ extend Service
5
+ include HTTParty
6
+ base_uri 'http://developer.echonest.com/api'
7
+ default_params :version => 3
8
+ format :xml
9
+ has_api_key
10
+
11
+ def self.artist(name)
12
+ build_artist get('/search_artists', :query => {:query => name})
13
+ end
14
+
15
+ private
16
+
17
+ def self.build_artist(response)
18
+ if artists = response['response']['artists']
19
+ artist = artists['artist']
20
+ artist = artist.first if artist.is_a?(Array)
21
+ Metamuse::Artist.new :name => artist['name'], :echonest_id => artist['id']
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,43 @@
1
+ module Metamuse
2
+ module Services
3
+ class Freebase
4
+ include HTTParty
5
+ base_uri 'http://www.freebase.com'
6
+ format :json
7
+
8
+ def self.artist(name)
9
+ envelop = %({"query": #{album_and_track_mql(name)}})
10
+ response = get("/api/service/mqlread", :query => {:query => envelop})
11
+ build_artist response
12
+ end
13
+
14
+ def self.artist_search(name)
15
+ response = get("/api/service/search", :query => album_and_track_search_query(name))
16
+ build_artist response
17
+ end
18
+
19
+ private
20
+
21
+ def self.album_and_track_mql(name)
22
+ %({"album": [{ "id": null, "name": null, "track": [{ "name": null, "index": null, "sort": "index" }], "release_date": null, "release_type": null, "sort": "-release_date", "/common/topic/image": [{ "id": null, "optional": true, "limit": 3 }] }], "id": null, "name": "#{name}", "type": "/music/artist"})
23
+ end
24
+
25
+ def self.album_and_track_search_query(name)
26
+ {:query => name, :type => "/music/artist", :limit => 1, :mql_output => album_and_track_mql(name)}
27
+ end
28
+
29
+ def self.build_artist(response)
30
+ result = response['result']
31
+ artist = Metamuse::Artist.new :name => CGI.unescapeHTML(result['name']), :freebase_guid => result['guid']
32
+ albums = Array.insist result['album']
33
+ albums.each do |album|
34
+ data = {:name => CGI.unescapeHTML(album['name']), :release_date => album['release_date'], :freebase_id => album['freebase_id']}
35
+ tracks = album['track']
36
+ data[:tracks] = tracks.map {|t| t.update 'name' => CGI.unescapeHTML(t['name'])}
37
+ artist.albums << data
38
+ end
39
+ artist
40
+ end
41
+ end
42
+ end
43
+ end