1
0
Fork 0

Stability improvements, new release

This commit is contained in:
Gregory Eremin 2013-01-20 16:36:52 +04:00
parent 7d5bde8616
commit 531fcc9908
21 changed files with 322 additions and 211 deletions

View File

@ -3,7 +3,11 @@
### Ruby Version
**IMPORTANT!**
Ruby version 1.9+ required. No support for 1.8.7 anymore, update your application already!
Ruby version 1.9+ required. No support for 1.8.7 anymore, if you still on 1.8 consider using gem [version 0.5.2](https://github.com/magnolia-fan/musicbrainz/tree/v0.5.2#musicbrainz-web-service-wrapper-) and bundle it like this:
```ruby
gem 'musicbrainz', '0.5.2'
```
### Installation
```
@ -11,7 +15,7 @@ gem install musicbrainz
```
or add this line to your Gemfile
```ruby
gem "musicbrainz"
gem 'musicbrainz'
```
### Configuration
@ -34,7 +38,7 @@ end
### Usage
```ruby
require "musicbrainz"
require 'musicbrainz'
# Search for artists
@suggestions = MusicBrainz::Artist.search("Jet")
@ -47,50 +51,80 @@ require "musicbrainz"
@empire_tracks = @kasabian.release_groups[8].releases.first.tracks
```
### Api
### Models
MusicBrainz::Artist
```ruby
@artists = MusicBrainz::Artist.search(query)
@artist = MusicBrainz::Artist.find_by_name(name)
@artist = MusicBrainz::Artist.find(mbid)
@artist.id
@artist.type
@artist.name
@artist.country
@artist.date_begin
@artist.date_end
# Class Methods:
MusicBrainz::Artist.find(id)
MusicBrainz::Artist.find_by_name(name)
MusicBrainz::Artist.search(name)
MusicBrainz::Artist.discography(id)
# Instance Methods:
@artist.release_groups
# Fields
{
:id => String,
:type => String,
:name => String,
:country => String,
:date_begin => Date,
:date_end => Date,
:urls => Hash
}
```
MusicBrainz::ReleaseGroup
```ruby
@release_group = MusicBrainz::ReleaseGroup.find(mbid)
@release_group.id
@release_group.type
@release_group.title
@release_group.first_release_date
# Class Methods
MusicBrainz::ReleaseGroup.find(id)
# Instance Methods
@release_group.releases
# Fields
{
:id => String,
:type => String,
:title => String,
:desc => String,
:first_release_date => Date
}
```
MusicBrainz::Release
```ruby
@release = MusicBrainz::Release.find(mbid)
@release.id
@release.title
@release.status
@release.date
@release.country
# Class Methods
MusicBrainz::Release.find(id)
# Instance Methods
@release.tracks
# Fields
{
:id => String,
:title => String,
:status => String,
:format => String,
:date => Date,
:country => String
}
```
MusicBrainz::Track
```ruby
@track = MusicBrainz::Track.find(mbid)
@track.position
@track.recording_id
@track.title
@track.length
# Class Methods
MusicBrainz::Track.find(id)
# Fields
{
:position => Integer,
:recording_id => String,
:title => String,
:length => Integer
}
```
### Testing

2
lib/mb.rb Normal file
View File

@ -0,0 +1,2 @@
require "musicbrainz"
MB = MusicBrainz

View File

@ -1,5 +1,9 @@
module MusicBrainz
module Client
class Client
include ClientModules::TransparentProxy
include ClientModules::FailsafeProxy
include ClientModules::CachingProxy
def http
@faraday ||= Faraday.new do |f|
f.request :url_encoded # form-encode POST params
@ -11,14 +15,18 @@ module MusicBrainz
def load(resource, query, params)
raise Exception.new("You need to run MusicBrainz.configure before querying") if MusicBrainz.config.nil?
response = contents_of(build_url(resource, query))
xml = Nokogiri::XML.parse(response).remove_namespaces!.xpath('/metadata')
data = params[:binding].parse(xml)
url = build_url(resource, query)
response = get_contents(url)
return nil if response[:status] != 200
xml = Nokogiri::XML.parse(response[:body]).remove_namespaces!.xpath('/metadata')
data = binding_class_for(params[:binding]).parse(xml)
if params[:create_model]
params[:create_model].new(data)
model_class_for(params[:create_model]).new(data)
elsif params[:create_models]
models = data.map{ |item| params[:create_models].new(item) }
models = data.map{ |item| model_class_for(params[:create_models]).new(item) }
models.sort!{ |a, b| a.send(params[:sort]) <=> b.send(params[:sort]) } if params[:sort]
models
else
@ -26,13 +34,7 @@ module MusicBrainz
end
end
def contents_of(url)
if method_defined? :get_contents
get_contents url
else
http.get url
end
end
private
def build_url(resource, params)
"#{MusicBrainz.config.web_service_url}#{resource.to_s.gsub('_', '-')}" <<
@ -40,17 +42,31 @@ module MusicBrainz
params.map do |key, value|
key = key.to_s.gsub('_', '-')
value = if value.is_a?(Array)
value.map{ |el| el.to_s.gsub('_', '-') }.join('+')
value.map{ |el| el.to_s.gsub('_', '-') }.join(?+)
else
value.to_s
end
"#{key}=#{value}"
end.join('&')
[key, value].join(?=)
end.join(?&)
end
include ClientModules::TransparentProxy
include ClientModules::FailsafeProxy
include ClientModules::CachingProxy
extend self
def binding_class_for(key)
MusicBrainz::Bindings.const_get(constantized(key))
end
def model_class_for(key)
MusicBrainz.const_get(constantized(key))
end
def constantized(key)
key.to_s.split(?_).map(&:capitalize).join.to_sym
end
end
module ClientHelper
def client
@client ||= Client.new
end
end
extend ClientHelper
end

View File

@ -10,22 +10,26 @@ module MusicBrainz
end
def get_contents(url)
return super unless MusicBrainz.config.perform_caching
return super unless caching?
token = Digest::SHA256.hexdigest(url)
file_path = "#{cache_path}/#{token[0..1]}/#{token[2..3]}/#{token[4..-1]}.xml"
hash = Digest::SHA256.hexdigest(url)
dir_path = [cache_path, *(0..2).map{ |i| hash.slice(2*i, 2) }].join(?/)
file_path = [dir_path, '/', hash.slice(6, 58), '.xml'].join
response = nil
response = { body: nil, status: 500 }
if File.exist?(file_path)
response = File.open(file_path, 'rb').gets
response = {
body: File.open(file_path, 'rb').gets,
status: 200
}
else
response = super
unless response.nil? or response.empty?
FileUtils.mkdir_p file_path.split('/')[0..-2].join('/')
if response[:status] == 200
FileUtils.mkpath(dir_path)
File.open(file_path, 'wb') do |f|
f.puts response
f.chmod 0755
f.puts(response[:body])
f.chmod(0755)
f.close
end
end
@ -33,6 +37,10 @@ module MusicBrainz
response
end
def caching?
MusicBrainz.config.perform_caching
end
end
end
end

View File

@ -2,21 +2,36 @@ module MusicBrainz
module ClientModules
module FailsafeProxy
def get_contents(url)
response = nil
return super unless failsafe?
response = { body: nil, status: 500 }
MusicBrainz.config.tries_limit.times do
time_passed = Time.now.to_f - @last_query_time ||= 0.0
if time_passed < MusicBrainz.config.query_interval
sleep(MusicBrainz.config.query_interval - time_passed)
end
response = super
@last_query_time = Time.now.to_f
break if response.status == 200
break if response[:status] == 200
end
response.body rescue nil
response
end
def time_passed
Time.now.to_f - @last_query_time ||= 0.0
end
def time_to_wait
MusicBrainz.config.query_interval - time_passed
end
def ready?
time_passed > MusicBrainz.config.query_interval
end
def wait_util_ready!
sleep(time_to_wait) unless ready?
@last_query_time = Time.now.to_f
end
def failsafe?
MusicBrainz.config.tries_limit > 1 && MusicBrainz.config.query_interval.to_f > 0
end
end
end

View File

@ -2,7 +2,10 @@ module MusicBrainz
module ClientModules
module TransparentProxy
def get_contents(url)
http.get url
response = http.get(url)
{ body: response.body, status: response.status }
rescue
{ body: nil, status: 500 }
end
end
end

View File

@ -19,24 +19,42 @@ module MusicBrainz
@perform_caching = DEFAULT_PERFORM_CACHING
end
def user_agent_string
def valid?
%w[ app_name app_version contact ].each do |param|
raise "#{param} must be set" if instance_variable_get("@#{param}").nil?
unless instance_variable_defined?(:"@#{param}")
raise Exception.new("Application identity parameter '#{param}' missing")
end
end
"#{@app_name}/#{@app_version} ( #{@contact} )"
unless tries_limit.nil? && query_interval.nil?
raise Exception.new("'tries_limit' parameter must be 1 or greater") if tries_limit.to_i < 1
raise Exception.new("'query_interval' parameter must be greater than zero") if query_interval.to_f < 0
end
if perform_caching
raise Exception.new("'cache_path' parameter must be set") if cache_path.nil?
end
true
end
end
module Configurable
def configure
raise "Configuration missing" unless block_given?
raise Exception.new("Configuration block missing") unless block_given?
yield @config ||= MusicBrainz::Configuration.new
config.valid?
end
def config
raise Exception.new("Configuration missing") unless instance_variable_defined?(:@config)
@config
end
def apply_test_configuration!
configure do |c|
c.app_name = "gem musicbrainz (development mode)"
c.app_version = MusicBrainz::VERSION
c.contact = `git config user.email`.chomp
end
end
end
extend Configurable
end

View File

@ -1,10 +1,23 @@
module MusicBrainz
class Middleware < Faraday::Middleware
def call(env)
env[:request_headers]["User-Agent"] = MusicBrainz.config.user_agent_string
env[:request_headers]["Via"] = "gem musicbrainz/#{VERSION} (#{GH_PAGE_URL})"
env[:request_headers].merge!(
"User-Agent" => user_agent_string,
"Via" => via_string
)
@app.call(env)
end
def user_agent_string
"#{config.app_name}/#{config.app_version} ( #{config.contact} )"
end
def via_string
"gem musicbrainz/#{VERSION} (#{GH_PAGE_URL})"
end
def config
MusicBrainz.config
end
end
end

View File

@ -1,38 +1,34 @@
module MusicBrainz
class Artist
include BaseModel
class Artist < BaseModel
field :id, String
field :type, String
field :name, String
field :country, String
field :date_begin, Time
field :date_end, Time
field :date_begin, Date
field :date_end, Date
field :urls, Hash
attr_writer :release_groups
def release_groups
@release_groups ||= Client::load(:release_group, { artist: id }, {
binding: MusicBrainz::Bindings::ArtistReleaseGroups,
create_models: MusicBrainz::ReleaseGroup,
@release_groups ||= client.load(:release_group, { artist: id }, {
binding: :artist_release_groups,
create_models: :release_group,
sort: :first_release_date
}) unless @id.nil?
end
class << self
def find(id)
Client.load(:artist, { id: id, inc: [:url_rels] }, {
binding: MusicBrainz::Bindings::Artist,
create_model: MusicBrainz::Artist
client.load(:artist, { id: id, inc: [:url_rels] }, {
binding: :artist,
create_model: :artist
})
end
def search(name)
name = CGI.escape(name).gsub(/\!/, '\!')
Client.load(:artist, { query: "artist:#{name}", limit: 10 }, {
binding: MusicBrainz::Bindings::ArtistSearch
client.load(:artist, { query: "artist:#{name}", limit: 10 }, {
binding: :artist_search
})
end

View File

@ -1,29 +1,43 @@
module MusicBrainz
module BaseModel
def self.included(klass)
class BaseModel
def self.inherited(klass)
klass.send(:include, InstanceMethods)
klass.send(:extend, ClassMethods)
end
module ClassMethods
def field(name, type)
self.class_exec do
attr_reader name
fields[name] = type
attr_reader name
define_method("#{name}=") do |val|
instance_variable_set("@#{name}", validate_type(val, type))
end
define_method("#{name}=") do |val|
instance_variable_set("@#{name}", validate_type(val, type))
end
end
def fields
instance_variable_set(:@fields, {}) unless instance_variable_defined?(:@fields)
instance_variable_get(:@fields)
end
def client
MusicBrainz.client
end
end
module InstanceMethods
def initialize(params = {})
params.each do |field, value|
self.send :"#{field}=", value
self.send(:"#{field}=", value)
end
end
def client
MusicBrainz.client
end
private
def validate_type(val, type)
if type == Integer
val.to_i
@ -31,7 +45,7 @@ module MusicBrainz
val.to_f
elsif type == String
val.to_s
elsif type == Time
elsif type == Date
if val.nil? or val == ""
val = "2030-12-31"
elsif val.split("-").length == 1
@ -39,7 +53,7 @@ module MusicBrainz
elsif val.split("-").length == 2
val << "-31"
end
Time.utc(*val.split("-"))
Date.new(*val.split(?-).map(&:to_i))
else
val
end

View File

@ -1,29 +1,25 @@
module MusicBrainz
class Release
include BaseModel
class Release < BaseModel
field :id, String
field :title, String
field :status, String
field :format, String
field :date, Time
field :date, Date
field :country, String
attr_writer :tracks
def tracks
@tracks ||= Client::load(:release, { id: id, inc: [:recordings, :media], limit: 100 }, {
binding: MusicBrainz::Bindings::ReleaseTracks,
create_models: MusicBrainz::Track,
@tracks ||= client.load(:release, { id: id, inc: [:recordings, :media], limit: 100 }, {
binding: :release_tracks,
create_models: :track,
sort: :position
}) unless @id.nil?
end
class << self
def find(id)
Client.load(:release, { id: id, inc: [:media] }, {
binding: MusicBrainz::Bindings::Release,
create_model: MusicBrainz::Release
client.load(:release, { id: id, inc: [:media] }, {
binding: :release,
create_model: :release
})
end
end

View File

@ -1,29 +1,26 @@
module MusicBrainz
class ReleaseGroup
include BaseModel
class ReleaseGroup < BaseModel
field :id, String
field :type, String
field :title, String
field :desc, String
field :first_release_date, Time
field :first_release_date, Date
alias_method :disambiguation, :desc
attr_writer :releases
def releases
@releases ||= Client::load(:release, { release_group: id, inc: [:media], limit: 100 }, {
binding: MusicBrainz::Bindings::ReleaseGroupReleases,
create_models: MusicBrainz::Release,
@releases ||= client.load(:release, { release_group: id, inc: [:media], limit: 100 }, {
binding: :release_group_releases,
create_models: :release,
sort: :date
}) unless @id.nil?
end
class << self
def find(id)
Client.load(:release_group, { id: id }, {
binding: MusicBrainz::Bindings::ReleaseGroup,
create_model: MusicBrainz::ReleaseGroup
client.load(:release_group, { id: id }, {
binding: :release_group,
create_model: :release_group
})
end
end

View File

@ -1,7 +1,5 @@
module MusicBrainz
class Track
include BaseModel
class Track < BaseModel
field :position, Integer
field :recording_id, String
field :title, String
@ -9,9 +7,9 @@ module MusicBrainz
class << self
def find(id)
Client.load(:recording, { id: id }, {
binding: MusicBrainz::Bindings::Track,
create_model: MusicBrainz::Track
client.load(:recording, { id: id }, {
binding: :track,
create_model: :track
})
end
end

View File

@ -1,3 +1,3 @@
module MusicBrainz
VERSION = "0.8"
VERSION = "0.7.2"
end

View File

@ -3,18 +3,19 @@ require File.expand_path('../lib/musicbrainz/version', __FILE__)
Gem::Specification.new do |gem|
gem.authors = ["Gregory Eremin"]
gem.email = ["magnolia_fan@me.com"]
gem.summary = %q{MusicBrainz Web Service wrapper with ActiveRecord-style models}
gem.summary = %q{ MusicBrainz Web Service wrapper with ActiveRecord-style models }
gem.homepage = "http://github.com/magnolia-fan/musicbrainz"
gem.files = %x{ git ls-files }.split($\)
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
gem.executables = []
gem.test_files = gem.files.grep(%r{^spec/})
gem.name = "musicbrainz"
gem.require_paths = %w[ lib ]
gem.version = MusicBrainz::VERSION
gem.license = "MIT"
gem.add_dependency("faraday")
gem.add_dependency("nokogiri")
gem.add_development_dependency("rspec")
gem.add_dependency('faraday')
gem.add_dependency('nokogiri')
gem.add_development_dependency('rspec')
gem.add_development_dependency('awesome_print')
end

View File

@ -0,0 +1,62 @@
# encoding: utf-8
require "ostruct"
require "spec_helper"
describe MusicBrainz::ClientModules::CachingProxy do
let(:old_cache_path){ File.join(File.dirname(__FILE__), '..', '..', 'tmp', 'spec_cache') }
let(:tmp_cache_path){ File.join(File.dirname(__FILE__), '..', '..', 'tmp', 'cache_module_spec_cache') }
let(:test_mbid){ "69b39eab-6577-46a4-a9f5-817839092033" }
let(:test_cache_file){ "#{tmp_cache_path}/03/48/ec/6c2bee685d9a96f95ed46378f624714e7a4650b0d44c1a8eee5bac2480.xml" }
let(:test_response_file){ File.join(File.dirname(__FILE__), "../fixtures/kasabian.xml") }
let(:test_response){ File.open(test_response_file).read }
before(:all) do
MusicBrainz.config.cache_path = tmp_cache_path
end
after(:all) do
MusicBrainz.config.cache_path = old_cache_path
MusicBrainz.config.perform_caching = true
MusicBrainz.config.query_interval = 1.5
end
context "with cache enabled" do
it "calls http only once when requesting the resource twice" do
MusicBrainz.config.perform_caching = true
File.exist?(test_cache_file).should be_false
# Stubbing
MusicBrainz.client.http.stub(:get).and_return(OpenStruct.new(status: 200, body: test_response))
MusicBrainz.client.http.should_receive(:get).once
2.times do
artist = MusicBrainz::Artist.find(test_mbid)
artist.should be_a_kind_of(MusicBrainz::Artist)
File.exist?(test_cache_file).should be_true
end
MusicBrainz.client.clear_cache
end
end
context "with cache disabled" do
it "calls http twice when requesting the resource twice" do
MusicBrainz.config.perform_caching = false
File.exist?(test_cache_file).should be_false
# Hacking for test performance purposes
MusicBrainz.config.query_interval = 0.0
# Stubbing
MusicBrainz.client.http.stub(:get).and_return(OpenStruct.new(status: 200, body: test_response))
MusicBrainz.client.http.should_receive(:get).twice
2.times do
artist = MusicBrainz::Artist.find(test_mbid)
artist.should be_a_kind_of(MusicBrainz::Artist)
File.exist?(test_cache_file).should be_false
end
end
end
end

View File

@ -55,6 +55,6 @@ describe MusicBrainz::Artist do
release_groups.first.id.should == "533cbc5f-ec7e-32ab-95f3-8d1f804a5176"
release_groups.first.type.should == "Single"
release_groups.first.title.should == "Club Foot"
release_groups.first.first_release_date.should == Time.utc(2004, 5, 10)
release_groups.first.first_release_date.should == Date.new(2004, 5, 10)
end
end

View File

@ -19,7 +19,7 @@ describe MusicBrainz::ReleaseGroup do
release_group.id.should == "6f33e0f0-cde2-38f9-9aee-2c60af8d1a61"
release_group.type.should == "Album"
release_group.title.should == "Empire"
release_group.first_release_date.should == Time.utc(2006, 8, 28)
release_group.first_release_date.should == Date.new(2006, 8, 28)
end
it "gets correct release group's releases" do
@ -28,7 +28,7 @@ describe MusicBrainz::ReleaseGroup do
releases.first.id.should == "2225dd4c-ae9a-403b-8ea0-9e05014c778f"
releases.first.status.should == "Official"
releases.first.title.should == "Empire"
releases.first.date.should == Time.utc(2006, 8, 28)
releases.first.date.should == Date.new(2006, 8, 28)
releases.first.country.should == "GB"
end
end

View File

@ -19,7 +19,7 @@ describe MusicBrainz::Release do
release.id.should == "2225dd4c-ae9a-403b-8ea0-9e05014c778f"
release.title.should == "Empire"
release.status.should == "Official"
release.date.should == Time.utc(2006, 8, 28)
release.date.should == Date.new(2006, 8, 28)
release.country.should == "GB"
end

View File

@ -2,16 +2,18 @@ require "rubygems"
require "bundler/setup"
require "musicbrainz"
RSpec.configure do |c|
c.order = 'random'
end
MusicBrainz.configure do |c|
test_email = %x{ git config --global --get user.email }.gsub(/\n/, "")
test_email = `git config user.email`.chomp
test_email = "magnolia_fan@me.com" if test_email.empty?
c.app_name = "MusicBrainzGemTestSuite"
c.app_version = MusicBrainz::VERSION
c.contact = test_email
c.cache_path = File.join(File.dirname(__FILE__), '..', 'tmp', 'spec_cache')
c.perform_caching = true
end
RSpec.configure do |config|
# Configuration is not currently necessary
end

View File

@ -1,64 +0,0 @@
# encoding: utf-8
require "ostruct"
require "spec_helper"
describe MusicBrainz::Tools::Cache do
before(:all) do
@old_cache_path = MusicBrainz::Tools::Cache.cache_path
@tmp_cache_path = File.join(File.dirname(__FILE__), "../../tmp/cache/test")
@test_mbid = "69b39eab-6577-46a4-a9f5-817839092033"
@test_cache_file = "#{@tmp_cache_path}/03/48/ec6c2bee685d9a96f95ed46378f624714e7a4650b0d44c1a8eee5bac2480.xml"
end
after(:all) do
MusicBrainz.config.cache_path = @old_cache_path
end
before(:each) do
file_path = File.join(File.dirname(__FILE__), "../fixtures/kasabian.xml")
@test_response = File.open(file_path).read
end
context "with cache enabled" do
it "calls http only once when requesting the resource twice" do
MusicBrainz.config.cache_path = @tmp_cache_path
File.exist?(@test_cache_file).should be_false
# Stubbing
MusicBrainz::Client.http.stub(:get).and_return(OpenStruct.new(status: 200, body: @test_response))
MusicBrainz::Client.http.should_receive(:get).once
2.times do
artist = MusicBrainz::Artist.find(@test_mbid)
artist.should be_a_kind_of(MusicBrainz::Artist)
File.exist?(@test_cache_file).should be_true
end
MusicBrainz::Client.clear_cache
end
end
context "with cache disabled" do
it "calls http twice when requesting the resource twice" do
MusicBrainz.config.perform_caching = false
File.exist?(@test_cache_file).should be_false
# Hacking for test performance purposes
MusicBrainz.config.query_interval = 0.0
# Stubbing
MusicBrainz::Client.http.stub(:get).and_return(OpenStruct.new(status: 200, body: @test_response))
MusicBrainz::Client.http.should_receive(:get).twice
2.times do
artist = MusicBrainz::Artist.find(@test_mbid)
artist.should be_a_kind_of(MusicBrainz::Artist)
File.exist?(@test_cache_file).should be_false
end
MusicBrainz.config.perform_caching = true
MusicBrainz.config.query_interval = 1.5
end
end
end