├── lib ├── mb.rb ├── musicbrainz │ ├── version.rb │ ├── models │ │ ├── medium.rb │ │ ├── track.rb │ │ ├── recording.rb │ │ ├── artist.rb │ │ ├── release_group.rb │ │ ├── release.rb │ │ └── base_model.rb │ ├── bindings │ │ ├── discid_releases.rb │ │ ├── release_mediums.rb │ │ ├── release_group_releases.rb │ │ ├── release_tracks.rb │ │ ├── artist_release_groups.rb │ │ ├── medium.rb │ │ ├── track.rb │ │ ├── release_group.rb │ │ ├── release_group_search.rb │ │ ├── artist.rb │ │ ├── recording_search.rb │ │ ├── recording.rb │ │ ├── relations.rb │ │ ├── artist_search.rb │ │ └── release.rb │ ├── client_modules │ │ ├── transparent_proxy.rb │ │ ├── failsafe_proxy.rb │ │ └── caching_proxy.rb │ ├── middleware.rb │ ├── deprecated.rb │ ├── configuration.rb │ └── client.rb └── musicbrainz.rb ├── .gitignore ├── spec ├── models │ ├── medium_spec.rb │ ├── track_spec.rb │ ├── recording_spec.rb │ ├── base_model_spec.rb │ ├── artist_spec.rb │ ├── release_group_spec.rb │ └── release_spec.rb ├── support │ └── mock_helpers.rb ├── fixtures │ ├── recording │ │ ├── find_b3015bab-1540-4d4e-9f30-14872a1525f7.xml │ │ └── search_bound_for_the_floor_local_h.xml │ ├── release_group │ │ ├── find_6f33e0f0-cde2-38f9-9aee-2c60af8d1a61.xml │ │ ├── search_kasabian_empire.xml │ │ └── search_kasabian_empire_album.xml │ ├── release │ │ ├── find_2225dd4c-ae9a-403b-8ea0-9e05014c778f.xml │ │ ├── find_b94cb547-cf7a-4357-894c-53c3bf33b093.xml │ │ ├── find_by_discid_pmzhT6ZlFiwSRCdVwV0eqire5_Y-.xml │ │ ├── find_2225dd4c-ae9a-403b-8ea0-9e05014c778f_tracks.xml │ │ └── list_6f33e0f0-cde2-38f9-9aee-2c60af8d1a61.xml │ └── artist │ │ ├── search_kasabian.xml │ │ ├── find_69b39eab-6577-46a4-a9f5-817839092033.xml │ │ └── release_groups_69b39eab-6577-46a4-a9f5-817839092033.xml ├── spec_helper.rb ├── bindings │ ├── recording_spec.rb │ ├── track_spec.rb │ ├── release_group_search_spec.rb │ ├── medium_spec.rb │ ├── recording_search_spec.rb │ ├── relations_spec.rb │ └── release_spec.rb ├── deprecated │ ├── cache_config_spec.rb │ └── proxy_config_spec.rb └── client_modules │ └── cache_spec.rb ├── Rakefile ├── Gemfile ├── Contributors ├── CHANGELOG.md ├── .github └── workflows │ └── test.yml ├── musicbrainz.gemspec ├── LICENSE ├── Gemfile.lock └── README.md /lib/mb.rb: -------------------------------------------------------------------------------- 1 | require "musicbrainz" 2 | MB = MusicBrainz 3 | -------------------------------------------------------------------------------- /lib/musicbrainz/version.rb: -------------------------------------------------------------------------------- 1 | module MusicBrainz 2 | VERSION = "0.8.0" 3 | end 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .ruby-version 2 | coverage 3 | .bundle 4 | tmp 5 | .gem 6 | pkg/* 7 | /vendor 8 | -------------------------------------------------------------------------------- /spec/models/medium_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe MusicBrainz::Medium 4 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rake 2 | begin 3 | require 'bundler/setup' 4 | rescue LoadError 5 | puts 'You must `gem install bundler` and `bundle install` to run rake tasks' 6 | end 7 | 8 | Bundler::GemHelper.install_tasks 9 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | 5 | group :test do 6 | gem 'rspec' 7 | end 8 | 9 | group :development, :test do 10 | gem 'rake', '~> 13.1.0' 11 | gem 'pry' 12 | gem 'awesome_print' 13 | end 14 | -------------------------------------------------------------------------------- /Contributors: -------------------------------------------------------------------------------- 1 | Huge thanks to that people for making this gem better! 2 | 3 | Applicat (https://github.com/Applicat) 4 | Jens Fahnenbruck (https://github.com/jigfox) 5 | Sander Nieuwenhuizen (https://github.com/munkius) 6 | Savater Sebastien (https://github.com/blakink) 7 | Thomas Wolfe (https://github.com/tomwolfe) 8 | -------------------------------------------------------------------------------- /lib/musicbrainz/models/medium.rb: -------------------------------------------------------------------------------- 1 | module MusicBrainz 2 | class Medium < BaseModel 3 | field :position, Integer 4 | field :format, String 5 | field :tracks, Array 6 | 7 | def tracks=(tracks_data) 8 | @tracks = tracks_data.map { |track_data| Track.new(track_data) } 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/musicbrainz/bindings/discid_releases.rb: -------------------------------------------------------------------------------- 1 | module MusicBrainz 2 | module Bindings 3 | module DiscidReleases 4 | def parse(xml) 5 | xml.xpath('./disc/release-list/release').map do |xml| 6 | MusicBrainz::Bindings::Release.parse(xml) 7 | end 8 | end 9 | 10 | extend self 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/musicbrainz/bindings/release_mediums.rb: -------------------------------------------------------------------------------- 1 | module MusicBrainz 2 | module Bindings 3 | module ReleaseMediums 4 | def parse(xml) 5 | xml.xpath('./release/medium-list/medium').map do |xml| 6 | MusicBrainz::Bindings::Medium.parse(xml) 7 | end 8 | end 9 | 10 | extend self 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/musicbrainz/bindings/release_group_releases.rb: -------------------------------------------------------------------------------- 1 | module MusicBrainz 2 | module Bindings 3 | module ReleaseGroupReleases 4 | def parse(xml) 5 | xml.xpath('./release-list/release').map do |xml| 6 | MusicBrainz::Bindings::Release.parse(xml) 7 | end 8 | end 9 | 10 | extend self 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/musicbrainz/bindings/release_tracks.rb: -------------------------------------------------------------------------------- 1 | module MusicBrainz 2 | module Bindings 3 | module ReleaseTracks 4 | def parse(xml) 5 | xml.xpath('./release/medium-list/medium/track-list/track').map do |xml| 6 | MusicBrainz::Bindings::Track.parse(xml) 7 | end 8 | end 9 | 10 | extend self 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/musicbrainz/bindings/artist_release_groups.rb: -------------------------------------------------------------------------------- 1 | module MusicBrainz 2 | module Bindings 3 | module ArtistReleaseGroups 4 | def parse(xml) 5 | xml.xpath('./release-group-list/release-group').map do |xml| 6 | MusicBrainz::Bindings::ReleaseGroup.parse(xml) 7 | end 8 | end 9 | 10 | extend self 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/musicbrainz/client_modules/transparent_proxy.rb: -------------------------------------------------------------------------------- 1 | module MusicBrainz 2 | module ClientModules 3 | module TransparentProxy 4 | def get_contents(url) 5 | response = http.get(url) 6 | { body: response.body, status: response.status } 7 | rescue Faraday::Error::ClientError 8 | { body: nil, status: 500 } 9 | end 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /spec/support/mock_helpers.rb: -------------------------------------------------------------------------------- 1 | module MockHelpers 2 | def mock(url:, fixture:) 3 | allow_any_instance_of(MusicBrainz::Client).to receive(:get_contents) 4 | .with(url) 5 | .and_return({ status: 200, body: read_fixture(fixture)}) 6 | end 7 | 8 | def read_fixture(name) 9 | spec_path = File.join(File.dirname(__FILE__), "..") 10 | file_path = File.join(spec_path, "fixtures", name) 11 | File.open(file_path).read 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /spec/fixtures/recording/find_b3015bab-1540-4d4e-9f30-14872a1525f7.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Empire 5 | 233013 6 | 2006-08-25 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /lib/musicbrainz/models/track.rb: -------------------------------------------------------------------------------- 1 | module MusicBrainz 2 | class Track < BaseModel 3 | field :id, String 4 | field :position, Integer 5 | field :recording_id, String 6 | field :title, String 7 | field :length, Integer 8 | 9 | class << self 10 | def find(id) 11 | client.load(:recording, { id: id }, { 12 | binding: :track, 13 | create_model: :track 14 | }) 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require "rubygems" 2 | require "bundler/setup" 3 | require "musicbrainz" 4 | require "pry" 5 | require "awesome_print" 6 | require_relative 'support/mock_helpers' 7 | 8 | RSpec.configure do |c| 9 | include MockHelpers 10 | 11 | c.order = 'random' 12 | c.color = true 13 | end 14 | 15 | MusicBrainz.configure do |c| 16 | c.app_name = "MusicBrainzGemTestSuite" 17 | c.app_version = MusicBrainz::VERSION 18 | c.contact = "root@localhost" 19 | end 20 | -------------------------------------------------------------------------------- /lib/musicbrainz/bindings/medium.rb: -------------------------------------------------------------------------------- 1 | module MusicBrainz 2 | module Bindings 3 | module Medium 4 | def parse(xml) 5 | { 6 | position: (xml.xpath('./position').text rescue nil), 7 | format: (xml.xpath('./format').text rescue nil), 8 | tracks: xml.xpath('./track-list/track').map do |track_xml| 9 | MusicBrainz::Bindings::Track.parse(track_xml) 10 | end 11 | } 12 | end 13 | 14 | extend self 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/musicbrainz/models/recording.rb: -------------------------------------------------------------------------------- 1 | module MusicBrainz 2 | class Recording < BaseModel 3 | field :id, String 4 | field :mbid, String 5 | field :title, String 6 | field :artist, String 7 | field :releases, String 8 | field :score, Integer 9 | field :isrcs, Array 10 | 11 | class << self 12 | def find(id) 13 | super({ id: id, inc: [:isrcs] }) 14 | end 15 | 16 | def search(track_name, artist_name) 17 | super({recording: track_name, artist: artist_name}) 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/musicbrainz/bindings/track.rb: -------------------------------------------------------------------------------- 1 | module MusicBrainz 2 | module Bindings 3 | module Track 4 | def parse(xml) 5 | { 6 | id: (xml.attribute('id').value rescue nil), 7 | position: (xml.xpath('./position').text rescue nil), 8 | recording_id: (xml.xpath('./recording').attribute('id').value rescue nil), 9 | title: (xml.xpath('./recording/title').text rescue nil), 10 | length: (xml.xpath('./recording/length').text rescue nil) 11 | } 12 | end 13 | 14 | extend self 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## musicbrainz (unreleased) ## 2 | 3 | # 0.7.7 (Dezember 1, 2014) ## 4 | 5 | * Handle nil values returned from Base model search at Artist#find_by_name and ReleaseGroup#find_by_artist_and_title [#24] 6 | 7 | # 0.7.6 (June 14, 2013) ## 8 | 9 | * Improves urls attribute to return an array if there are multiple urls for a relation type. [#19] 10 | 11 | # 0.7.5 (May 6, 2013) ## 12 | 13 | * Created new track_search binding to allow searching for tracks. [#18] 14 | * Adds release group urls. [#17] 15 | 16 | # 0.1.0 (July 18, 2011) ## 17 | 18 | * Initial version. -------------------------------------------------------------------------------- /lib/musicbrainz/middleware.rb: -------------------------------------------------------------------------------- 1 | module MusicBrainz 2 | class Middleware < Faraday::Middleware 3 | def call(env) 4 | env[:request_headers].merge!( 5 | "User-Agent" => user_agent_string, 6 | "Via" => via_string 7 | ) 8 | @app.call(env) 9 | end 10 | 11 | def user_agent_string 12 | "#{config.app_name}/#{config.app_version} ( #{config.contact} )" 13 | end 14 | 15 | def via_string 16 | "gem musicbrainz/#{VERSION} (#{GH_PAGE_URL})" 17 | end 18 | 19 | def config 20 | MusicBrainz.config 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/musicbrainz/bindings/release_group.rb: -------------------------------------------------------------------------------- 1 | module MusicBrainz 2 | module Bindings 3 | module ReleaseGroup 4 | def parse(xml) 5 | xml = xml.xpath('./release-group') unless xml.xpath('./release-group').empty? 6 | { 7 | id: (xml.attribute('id').value rescue nil), 8 | type: (xml.attribute('type').value rescue nil), 9 | title: (xml.xpath('./title').text rescue nil), 10 | desc: (xml.xpath('./disambiguation').text rescue nil), 11 | first_release_date: (xml.xpath('./first-release-date').text rescue nil) 12 | }.merge(Relations.parse(xml)) 13 | end 14 | 15 | extend self 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/musicbrainz/bindings/release_group_search.rb: -------------------------------------------------------------------------------- 1 | module MusicBrainz 2 | module Bindings 3 | module ReleaseGroupSearch 4 | def parse(xml) 5 | xml.xpath('./release-group-list/release-group').map do |xml| 6 | { 7 | id: (xml.attribute('id').value rescue nil), 8 | mbid: (xml.attribute('id').value rescue nil), # Old shit 9 | title: (xml.xpath('./title').text.gsub(/[`’]/, "'") rescue nil), 10 | type: (xml.attribute('type').value rescue nil), 11 | score: (xml.attribute('score').value.to_i rescue nil) 12 | } rescue nil 13 | end.delete_if{ |item| item.nil? } 14 | end 15 | 16 | extend self 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | # Adopted from 2 | # https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-ruby 3 | name: Ruby CI 4 | 5 | on: 6 | push: 7 | branches: [ master ] 8 | pull_request: 9 | branches: [ master ] 10 | 11 | jobs: 12 | test: 13 | runs-on: ubuntu-latest 14 | strategy: 15 | matrix: 16 | ruby-version: ['3.3', '3.4'] 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Set up Ruby ${{ matrix.ruby-version }} 20 | uses: ruby/setup-ruby@v1.207.0 21 | with: 22 | ruby-version: ${{ matrix.ruby-version }} 23 | - name: Install dependencies 24 | run: bundle install 25 | - name: Run tests 26 | run: bundle exec rspec 27 | -------------------------------------------------------------------------------- /lib/musicbrainz/bindings/artist.rb: -------------------------------------------------------------------------------- 1 | module MusicBrainz 2 | module Bindings 3 | module Artist 4 | def parse(xml) 5 | xml = xml.xpath('./artist') 6 | 7 | return {} if xml.empty? 8 | 9 | { 10 | id: (xml.attribute('id').value rescue nil), 11 | type: (xml.attribute('type').value rescue nil), 12 | name: (xml.xpath('./name').text.gsub(/[`’]/, "'") rescue nil), 13 | country: (xml.xpath('./country').text rescue nil), 14 | date_begin: (xml.xpath('./life-span/begin').text rescue nil), 15 | date_end: (xml.xpath('./life-span/end').text rescue nil) 16 | }.merge(Relations.parse(xml)) 17 | end 18 | 19 | extend self 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /musicbrainz.gemspec: -------------------------------------------------------------------------------- 1 | require File.expand_path('../lib/musicbrainz/version', __FILE__) 2 | 3 | Gem::Specification.new do |gem| 4 | gem.authors = ["Gregory Eremin"] 5 | gem.email = ["magnolia_fan@me.com"] 6 | gem.summary = %q{ MusicBrainz Web Service wrapper with ActiveRecord-style models } 7 | gem.homepage = "http://github.com/localhots/musicbrainz" 8 | 9 | gem.files = %x{ git ls-files }.split($\) 10 | gem.executables = [] 11 | gem.test_files = gem.files.grep(%r{^spec/}) 12 | gem.name = "musicbrainz" 13 | gem.require_paths = %w[ lib ] 14 | gem.version = MusicBrainz::VERSION 15 | gem.license = "MIT" 16 | 17 | gem.add_runtime_dependency('faraday') 18 | gem.add_runtime_dependency('nokogiri') 19 | end 20 | -------------------------------------------------------------------------------- /lib/musicbrainz/bindings/recording_search.rb: -------------------------------------------------------------------------------- 1 | module MusicBrainz 2 | module Bindings 3 | module RecordingSearch 4 | def parse(xml) 5 | xml.xpath('./recording-list/recording').map do |xml| 6 | { 7 | id: (xml.attribute('id').value rescue nil), 8 | mbid: (xml.attribute('id').value rescue nil), # Old shit 9 | title: (xml.xpath('./title').text.gsub(/[`’]/, "'") rescue nil), 10 | artist: (xml.xpath('./artist-credit/name-credit/artist/name').text rescue nil), 11 | releases: (xml.xpath('./release-list/release/title').map{ |xml| xml.text } rescue []), 12 | score: (xml.attribute('score').value.to_i rescue nil) 13 | } rescue nil 14 | end.delete_if{ |item| item.nil? } 15 | end 16 | 17 | extend self 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/musicbrainz/deprecated.rb: -------------------------------------------------------------------------------- 1 | module MusicBrainz 2 | module Deprecated 3 | module ProxyConfig 4 | def query_interval 5 | MusicBrainz.config.query_interval 6 | end 7 | 8 | def query_interval=(value) 9 | MusicBrainz.config.query_interval = value 10 | end 11 | end 12 | 13 | module CacheConfig 14 | def cache_path 15 | MusicBrainz.config.cache_path 16 | end 17 | 18 | def cache_path=(value) 19 | MusicBrainz.config.cache_path = value 20 | end 21 | end 22 | end 23 | 24 | module Tools 25 | module Proxy 26 | extend Deprecated::ProxyConfig 27 | end 28 | 29 | module Cache 30 | extend Deprecated::CacheConfig 31 | end 32 | end 33 | 34 | extend Deprecated::ProxyConfig 35 | extend Deprecated::CacheConfig 36 | end 37 | -------------------------------------------------------------------------------- /lib/musicbrainz/bindings/recording.rb: -------------------------------------------------------------------------------- 1 | module MusicBrainz 2 | module Bindings 3 | module Recording 4 | def parse(xml) 5 | xml = xml.xpath('./recording') unless xml.xpath('./recording').empty? 6 | { 7 | id: (xml.attribute('id').value rescue nil), 8 | mbid: (xml.attribute('id').value rescue nil), # Old shit 9 | title: (xml.xpath('./title').text.gsub(/[`’]/, "'") rescue nil), 10 | artist: (xml.xpath('./artist-credit/name-credit/artist/name').text rescue nil), 11 | releases: (xml.xpath('./release-list/release/title').map{ |xml| xml.text } rescue []), 12 | score: (xml.attribute('score').value.to_i rescue nil), 13 | isrcs: (xml.xpath('./isrc-list/isrc/@id').map(&:text) rescue []) 14 | } 15 | end 16 | 17 | extend self 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/musicbrainz/bindings/relations.rb: -------------------------------------------------------------------------------- 1 | module MusicBrainz 2 | module Bindings 3 | module Relations 4 | def parse(xml) 5 | hash = { urls: {} } 6 | 7 | xml.xpath('./relation-list[@target-type="url"]/relation').map do |xml| 8 | next unless type = xml.attribute('type') 9 | 10 | type = type.value.downcase.split(" ").join("_").to_sym 11 | target = xml.xpath('./target').text 12 | 13 | if hash[:urls][type].nil? then hash[:urls][type] = target 14 | elsif hash[:urls][type].is_a?(Array) then hash[:urls][type] << target 15 | else hash[:urls][type] = [hash[:urls][type]]; hash[:urls][type] << target 16 | end 17 | end 18 | 19 | hash 20 | end 21 | 22 | extend self 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/bindings/recording_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe MusicBrainz::Bindings::Recording do 4 | describe '.parse' do 5 | subject(:parse) { described_class.parse(xml) } 6 | 7 | let(:xml) { Nokogiri::XML(<<~XML).at_xpath('recording') } 8 | 9 | Empire 10 | 233013 11 | 2006-08-25 12 | 13 | 14 | 15 | 16 | XML 17 | 18 | it 'contains recording attributes' do 19 | expect(parse).to include( 20 | id: 'b3015bab-1540-4d4e-9f30-14872a1525f7', 21 | mbid: 'b3015bab-1540-4d4e-9f30-14872a1525f7', title: 'Empire', 22 | isrcs: ['JPB600760301'] 23 | ) 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /spec/deprecated/cache_config_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe MusicBrainz::Deprecated::CacheConfig do 4 | before(:all) { 5 | @old_cache_path = MusicBrainz.config.cache_path 6 | } 7 | 8 | before(:each) { 9 | MusicBrainz.config.cache_path = nil 10 | } 11 | 12 | after(:all) { 13 | MusicBrainz.config.cache_path = @old_cache_path 14 | } 15 | 16 | it "allows deprecated use of cache_path" do 17 | MusicBrainz.config.cache_path = "test1" 18 | 19 | expect(MusicBrainz::Tools::Cache.cache_path).to eq "test1" 20 | expect(MusicBrainz.cache_path).to eq "test1" 21 | end 22 | 23 | it "allows deprecated use of cache_path=" do 24 | MusicBrainz::Tools::Cache.cache_path = "test2" 25 | expect(MusicBrainz.config.cache_path).to eq "test2" 26 | 27 | MusicBrainz.cache_path = "test3" 28 | expect(MusicBrainz.config.cache_path).to eq "test3" 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /spec/deprecated/proxy_config_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe MusicBrainz::Deprecated::ProxyConfig do 4 | before(:all) { 5 | @old_query_interval = MusicBrainz.config.query_interval 6 | } 7 | 8 | before(:each) { 9 | MusicBrainz.config.query_interval = nil 10 | } 11 | 12 | after(:all) { 13 | MusicBrainz.config.query_interval = @old_query_interval 14 | } 15 | 16 | it "allows deprecated use of query_interval" do 17 | MusicBrainz.config.query_interval = 2 18 | 19 | expect(MusicBrainz::Tools::Proxy.query_interval).to eq 2 20 | expect(MusicBrainz.query_interval).to eq 2 21 | end 22 | 23 | it "allows deprecated use of query_interval=" do 24 | MusicBrainz::Tools::Proxy.query_interval = 3 25 | expect(MusicBrainz.config.query_interval).to eq 3 26 | 27 | MusicBrainz.query_interval = 4 28 | expect(MusicBrainz.config.query_interval).to eq 4 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /spec/models/track_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe MusicBrainz::Track do 4 | describe '.find' do 5 | before(:each) { 6 | mock url: 'http://musicbrainz.org/ws/2/recording/b3015bab-1540-4d4e-9f30-14872a1525f7?', 7 | fixture: 'recording/find_b3015bab-1540-4d4e-9f30-14872a1525f7.xml' 8 | } 9 | let(:track) { 10 | MusicBrainz::Track.find("b3015bab-1540-4d4e-9f30-14872a1525f7") 11 | } 12 | 13 | it "gets no exception while loading release info" do 14 | expect { track }.to_not raise_error(Exception) 15 | end 16 | 17 | it "gets correct instance" do 18 | expect(track).to be_an_instance_of(MusicBrainz::Track) 19 | end 20 | 21 | it "gets correct track data" do 22 | expect(track.recording_id).to eq "b3015bab-1540-4d4e-9f30-14872a1525f7" 23 | expect(track.title).to eq "Empire" 24 | expect(track.length).to eq 233013 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/musicbrainz/bindings/artist_search.rb: -------------------------------------------------------------------------------- 1 | module MusicBrainz 2 | module Bindings 3 | module ArtistSearch 4 | def parse(xml) 5 | xml.xpath('./artist-list/artist').map do |xml| 6 | { 7 | id: (xml.attribute('id').value rescue nil), 8 | mbid: (xml.attribute('id').value rescue nil), # Old shit 9 | name: (xml.xpath('./name').text.gsub(/[`’]/, "'") rescue nil), 10 | sort_name: (xml.xpath('./sort-name').gsub(/[`’]/, "'") rescue nil), 11 | type: (xml.attribute('type').value rescue nil), 12 | score: (xml.attribute('score').value.to_i rescue nil), 13 | desc: (xml.xpath('./disambiguation').value rescue nil), 14 | aliases: (xml.xpath('./alias-list/alias').map{ |xml| xml.text } rescue []) 15 | } rescue nil 16 | end.delete_if{ |item| item.nil? } 17 | end 18 | 19 | extend self 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /spec/bindings/track_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe MusicBrainz::Bindings::Track do 4 | describe '.parse' do 5 | subject(:parse) { described_class.parse(xml) } 6 | 7 | let(:xml) { Nokogiri::XML(<<~XML).at_xpath('track') } 8 | 9 | 1 10 | 1 11 | 233013 12 | 13 | Empire 14 | 233013 15 | 2006-08-25 16 | 17 | 18 | XML 19 | 20 | it 'contains track attributes' do 21 | expect(parse).to include( 22 | id: '0501cf38-48b7-3026-80d2-717d574b3d6a', position: '1', length: '233013', 23 | recording_id: 'b3015bab-1540-4d4e-9f30-14872a1525f7', title: 'Empire' 24 | ) 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/musicbrainz/client_modules/failsafe_proxy.rb: -------------------------------------------------------------------------------- 1 | module MusicBrainz 2 | module ClientModules 3 | module FailsafeProxy 4 | def get_contents(url) 5 | return super unless failsafe? 6 | 7 | response = { body: nil, status: 500 } 8 | MusicBrainz.config.tries_limit.times do 9 | wait_util_ready! 10 | response = super 11 | break if response[:status] == 200 12 | end 13 | 14 | response 15 | end 16 | 17 | def time_passed 18 | Time.now.to_f - @last_query_time ||= 0.0 19 | end 20 | 21 | def time_to_wait 22 | MusicBrainz.config.query_interval - time_passed 23 | end 24 | 25 | def ready? 26 | time_passed > MusicBrainz.config.query_interval 27 | end 28 | 29 | def wait_util_ready! 30 | sleep(time_to_wait) unless ready? 31 | @last_query_time = Time.now.to_f 32 | end 33 | 34 | def failsafe? 35 | MusicBrainz.config.tries_limit > 1 && MusicBrainz.config.query_interval.to_f > 0 36 | end 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /spec/bindings/release_group_search_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe MusicBrainz::Bindings::ReleaseGroupSearch do 4 | describe '.parse' do 5 | let(:response) { 6 | <<-XML 7 | 8 | 9 | 10 | Empire 11 | 12 | 13 | 14 | XML 15 | } 16 | let(:xml) { 17 | Nokogiri::XML.parse(response) 18 | } 19 | let(:metadata) { 20 | described_class.parse(xml.remove_namespaces!.xpath('/metadata')) 21 | } 22 | 23 | it "gets correct release group data" do 24 | expect(metadata).to eq [ 25 | { 26 | id: '246bc928-2dc8-35ba-80ee-7a0079de1632', 27 | mbid: '246bc928-2dc8-35ba-80ee-7a0079de1632', 28 | title: 'Empire', 29 | type: 'Single', 30 | score: 100, 31 | } 32 | ] 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2024 Gregory Eremin 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /lib/musicbrainz/bindings/release.rb: -------------------------------------------------------------------------------- 1 | module MusicBrainz 2 | module Bindings 3 | module Release 4 | def parse(xml) 5 | xml = xml.xpath('./release') unless xml.xpath('./release').empty? 6 | 7 | hash = { 8 | id: (xml.attribute('id').value rescue nil), 9 | type: (xml.xpath('./release-group').attribute('type').value rescue nil), 10 | title: (xml.xpath('./title').text rescue nil), 11 | status: (xml.xpath('./status').text rescue nil), 12 | country: (xml.xpath('./country').text rescue nil), 13 | date: (xml.xpath('./date').text rescue nil), 14 | asin: (xml.xpath('./asin').text rescue nil), 15 | barcode: (xml.xpath('./barcode').text rescue nil), 16 | quality: (xml.xpath('./quality').text rescue nil) 17 | } 18 | 19 | formats = (xml.xpath('./medium-list/medium/format') rescue []).map(&:text) 20 | 21 | hash[:format] = formats.uniq.map do |format| 22 | format_count = formats.select{|f| f == format}.length 23 | format_count == 1 ? format : "#{format_count}x#{format}" 24 | end.join(' + ') 25 | 26 | hash 27 | end 28 | 29 | extend self 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/musicbrainz/models/artist.rb: -------------------------------------------------------------------------------- 1 | module MusicBrainz 2 | class Artist < BaseModel 3 | field :id, String 4 | field :type, String 5 | field :name, String 6 | field :country, String 7 | field :date_begin, Date 8 | field :date_end, Date 9 | field :urls, Hash 10 | 11 | def release_groups 12 | @release_groups ||= client.load(:release_group, { artist: id, inc: [:url_rels] }, { 13 | binding: :artist_release_groups, 14 | create_models: :release_group, 15 | sort: :first_release_date 16 | }) unless @id.nil? 17 | end 18 | 19 | class << self 20 | def find(id) 21 | client.load(:artist, { id: id, inc: [:url_rels] }, { 22 | binding: :artist, 23 | create_model: :artist 24 | }) 25 | end 26 | 27 | def search(name) 28 | super({artist: name}) 29 | end 30 | 31 | def discography(mbid) 32 | artist = find(mbid) 33 | artist.release_groups.each { |rg| rg.releases.each { |r| r.tracks } } 34 | artist 35 | end 36 | 37 | def find_by_name(name) 38 | matches = search(name) 39 | if matches and not matches.empty? 40 | find(matches.first[:id]) 41 | end 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | musicbrainz (0.8.0) 5 | faraday 6 | nokogiri 7 | 8 | GEM 9 | remote: https://rubygems.org/ 10 | specs: 11 | awesome_print (1.9.2) 12 | coderay (1.1.3) 13 | diff-lcs (1.5.1) 14 | faraday (2.9.0) 15 | faraday-net_http (>= 2.0, < 3.2) 16 | faraday-net_http (3.1.0) 17 | net-http 18 | method_source (1.0.0) 19 | net-http (0.4.1) 20 | uri 21 | nokogiri (1.16.3-x86_64-linux) 22 | racc (~> 1.4) 23 | pry (0.14.2) 24 | coderay (~> 1.1) 25 | method_source (~> 1.0) 26 | racc (1.7.3) 27 | rake (13.1.0) 28 | rspec (3.13.0) 29 | rspec-core (~> 3.13.0) 30 | rspec-expectations (~> 3.13.0) 31 | rspec-mocks (~> 3.13.0) 32 | rspec-core (3.13.0) 33 | rspec-support (~> 3.13.0) 34 | rspec-expectations (3.13.0) 35 | diff-lcs (>= 1.2.0, < 2.0) 36 | rspec-support (~> 3.13.0) 37 | rspec-mocks (3.13.0) 38 | diff-lcs (>= 1.2.0, < 2.0) 39 | rspec-support (~> 3.13.0) 40 | rspec-support (3.13.1) 41 | uri (0.13.0) 42 | 43 | PLATFORMS 44 | x86_64-linux 45 | 46 | DEPENDENCIES 47 | awesome_print 48 | musicbrainz! 49 | pry 50 | rake (~> 13.1.0) 51 | rspec 52 | 53 | BUNDLED WITH 54 | 2.3.26 55 | -------------------------------------------------------------------------------- /spec/bindings/medium_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe MusicBrainz::Bindings::Medium do 4 | describe '.parse' do 5 | subject(:parse) { described_class.parse(xml) } 6 | 7 | let(:xml) { Nokogiri::XML(<<~XML).at_xpath('medium') } 8 | 9 | 1 10 | CD 11 | 12 | 13 | 1 14 | 1 15 | 233013 16 | 17 | Empire 18 | 233013 19 | 2006-08-25 20 | 21 | 22 | 23 | 24 | XML 25 | 26 | it 'contains medium attributes' do 27 | expect(parse).to include(position: '1', format: 'CD') 28 | end 29 | 30 | it 'contains track attributes' do 31 | expect(parse[:tracks].first).to include( 32 | id: '0501cf38-48b7-3026-80d2-717d574b3d6a', position: '1', length: '233013', 33 | recording_id: 'b3015bab-1540-4d4e-9f30-14872a1525f7', title: 'Empire' 34 | ) 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /spec/fixtures/release_group/find_6f33e0f0-cde2-38f9-9aee-2c60af8d1a61.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Empire 5 | 2006-08-28 6 | Album 7 | 8 | 9 | http://en.wikipedia.org/wiki/Empire_(Kasabian_album) 10 | 11 | 12 | http://lyrics.wikia.com/Kasabian:Empire_(2006) 13 | 14 | 15 | http://www.bbc.co.uk/music/reviews/389x 16 | 17 | 18 | http://www.discogs.com/master/89980 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /lib/musicbrainz/models/release_group.rb: -------------------------------------------------------------------------------- 1 | module MusicBrainz 2 | class ReleaseGroup < BaseModel 3 | field :id, String 4 | field :type, String 5 | field :title, String 6 | field :desc, String 7 | field :first_release_date, Date 8 | field :urls, Hash 9 | 10 | alias_method :disambiguation, :desc 11 | 12 | def releases 13 | @releases ||= client.load(:release, { release_group: id, inc: [:media, :release_groups], limit: 100 }, { 14 | binding: :release_group_releases, 15 | create_models: :release, 16 | sort: :date 17 | }) unless @id.nil? 18 | end 19 | 20 | class << self 21 | def find(id) 22 | client.load(:release_group, { id: id, inc: [:url_rels] }, { 23 | binding: :release_group, 24 | create_model: :release_group 25 | }) 26 | end 27 | 28 | def search(artist_name, title, type = nil) 29 | if type 30 | super({artist: artist_name, releasegroup: title, type: type}) 31 | else 32 | super({artist: artist_name, releasegroup: title}) 33 | end 34 | end 35 | 36 | def find_by_artist_and_title(artist_name, title, type = nil ) 37 | matches = search(artist_name, title, type) 38 | matches.nil? || matches.empty? ? nil : find(matches.first[:id]) 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/musicbrainz/models/release.rb: -------------------------------------------------------------------------------- 1 | module MusicBrainz 2 | class Release < BaseModel 3 | field :id, String 4 | field :type, String 5 | field :title, String 6 | field :status, String 7 | field :format, String 8 | field :date, Date 9 | field :country, String 10 | field :asin, String 11 | field :barcode, String 12 | field :quality, String 13 | 14 | def mediums 15 | @mediums ||= client.load(:release, { id: id, inc: [:recordings, :media], limit: 100 }, { 16 | binding: :release_mediums, 17 | create_models: :medium, 18 | sort: :position 19 | }) unless @id.nil? 20 | end 21 | 22 | def tracks 23 | @tracks ||= client.load(:release, { id: id, inc: [:recordings, :media], limit: 100 }, { 24 | binding: :release_tracks, 25 | create_models: :track, 26 | sort: :position 27 | }) unless @id.nil? 28 | end 29 | 30 | class << self 31 | def find(id) 32 | client.load(:release, { id: id, inc: [:media, :release_groups] }, { 33 | binding: :release, 34 | create_model: :release 35 | }) 36 | end 37 | 38 | def find_by_discid(id) 39 | client.load(:discid, { id: id }, { 40 | binding: :discid_releases, 41 | create_models: :release 42 | }) 43 | end 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/musicbrainz.rb: -------------------------------------------------------------------------------- 1 | require "digest/sha1" 2 | require "fileutils" 3 | require "date" 4 | 5 | require "faraday" 6 | require "nokogiri" 7 | 8 | require "musicbrainz/version" 9 | require "musicbrainz/deprecated" 10 | require "musicbrainz/middleware" 11 | require "musicbrainz/configuration" 12 | 13 | require "musicbrainz/client_modules/transparent_proxy" 14 | require "musicbrainz/client_modules/failsafe_proxy" 15 | require "musicbrainz/client_modules/caching_proxy" 16 | require "musicbrainz/client" 17 | 18 | require "musicbrainz/models/base_model" 19 | require "musicbrainz/models/artist" 20 | require "musicbrainz/models/medium" 21 | require "musicbrainz/models/release_group" 22 | require "musicbrainz/models/release" 23 | require "musicbrainz/models/track" 24 | require "musicbrainz/models/recording" 25 | 26 | require "musicbrainz/bindings/artist" 27 | require "musicbrainz/bindings/artist_search" 28 | require "musicbrainz/bindings/artist_release_groups" 29 | require "musicbrainz/bindings/discid_releases" 30 | require "musicbrainz/bindings/medium" 31 | require "musicbrainz/bindings/relations" 32 | require "musicbrainz/bindings/release_group" 33 | require "musicbrainz/bindings/release_group_search" 34 | require "musicbrainz/bindings/release_group_releases" 35 | require "musicbrainz/bindings/release" 36 | require "musicbrainz/bindings/release_mediums" 37 | require "musicbrainz/bindings/release_tracks" 38 | require "musicbrainz/bindings/track" 39 | require "musicbrainz/bindings/recording" 40 | require "musicbrainz/bindings/recording_search" 41 | 42 | module MusicBrainz 43 | GH_PAGE_URL = "http://git.io/brainz" 44 | end 45 | -------------------------------------------------------------------------------- /spec/models/recording_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe MusicBrainz::Recording do 4 | describe '.find' do 5 | before(:each) { 6 | mock url: 'http://musicbrainz.org/ws/2/recording/b3015bab-1540-4d4e-9f30-14872a1525f7?inc=isrcs', 7 | fixture: 'recording/find_b3015bab-1540-4d4e-9f30-14872a1525f7.xml' 8 | } 9 | let(:recording) { 10 | MusicBrainz::Recording.find("b3015bab-1540-4d4e-9f30-14872a1525f7") 11 | } 12 | 13 | it "gets no exception while loading release info" do 14 | expect{ recording }.to_not raise_error(Exception) 15 | end 16 | 17 | it "gets correct instance" do 18 | expect(recording).to be_an_instance_of(MusicBrainz::Recording) 19 | end 20 | 21 | it "gets correct track data" do 22 | expect(recording.title).to eq "Empire" 23 | expect(recording.isrcs).to eq ["JPB600760301"] 24 | end 25 | end 26 | 27 | describe '.search' do 28 | before(:each) { 29 | mock url: 'http://musicbrainz.org/ws/2/recording?query=recording:"Bound+for+the+floor" AND artist:"Local+H"&limit=10', 30 | fixture: 'recording/search_bound_for_the_floor_local_h.xml' 31 | } 32 | let(:matches) { 33 | MusicBrainz::Recording.search('Bound for the floor', 'Local H') 34 | } 35 | 36 | it "searches tracks (aka recordings) by artist name and title" do 37 | expect(matches).to_not be_empty 38 | expect(matches.first[:id]).to eq 'bb940e26-4265-4909-b128-4906d8b1079e' 39 | expect(matches.first[:title]).to eq "Bound for the Floor" 40 | expect(matches.first[:artist]).to eq "Local H" 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/musicbrainz/client_modules/caching_proxy.rb: -------------------------------------------------------------------------------- 1 | module MusicBrainz 2 | module ClientModules 3 | module CachingProxy 4 | def cache_path 5 | MusicBrainz.config.cache_path 6 | end 7 | 8 | def clear_cache 9 | FileUtils.rm_r(cache_path) if cache_path && File.exist?(cache_path) 10 | end 11 | 12 | def get_contents(url) 13 | return super unless caching? 14 | 15 | hash = Digest::SHA256.hexdigest(url) 16 | dir_path = [cache_path, *(0..2).map{ |i| hash.slice(2*i, 2) }].join(?/) 17 | file_path = [dir_path, '/', hash.slice(6, 58), '.xml'].join 18 | 19 | response = { body: nil, status: 500 } 20 | 21 | if File.exist?(file_path) 22 | get_cached_contents(file_path) 23 | else 24 | response = get_live_contents(file_path, url) 25 | if response[:status] == 200 26 | cache_contents!(file_path, response[:body]) 27 | end 28 | response 29 | end 30 | end 31 | 32 | def caching? 33 | MusicBrainz.config.perform_caching 34 | end 35 | 36 | private 37 | 38 | def get_cached_contents(file_path) 39 | { 40 | body: File.open(file_path, 'rb').gets, 41 | status: 200 42 | } 43 | end 44 | 45 | def get_live_contents(file_path, url) 46 | http.get(url) 47 | end 48 | 49 | def cache_contents!(file_path, body) 50 | dir_path = File.dirname(file_path) 51 | FileUtils.mkpath(dir_path) 52 | File.open(file_path, 'wb') do |f| 53 | f.puts(body) 54 | f.chmod(0755) 55 | f.close 56 | end 57 | end 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /spec/bindings/recording_search_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe MusicBrainz::Bindings::RecordingSearch do 4 | describe '.parse' do 5 | let(:response) { 6 | <<-XML 7 | 8 | 9 | 10 | King Fred 11 | 12 | 13 | 14 | Too Much Joy 15 | 16 | 17 | 18 | 19 | 20 | Green Eggs and Crack 21 | 22 | 23 | 24 | 25 | 26 | XML 27 | } 28 | let(:xml) { 29 | Nokogiri::XML.parse(response).remove_namespaces! 30 | } 31 | let(:metadata) { 32 | described_class.parse(xml.xpath('/metadata')) 33 | } 34 | 35 | it "gets correct Recording data" do 36 | expect(metadata).to eq [ 37 | { 38 | id: '0b382a13-32f0-4743-9248-ba5536a6115e', 39 | mbid: '0b382a13-32f0-4743-9248-ba5536a6115e', 40 | title: 'King Fred', 41 | artist: 'Too Much Joy', 42 | releases: ['Green Eggs and Crack'], 43 | score: 100, 44 | } 45 | ] 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /spec/fixtures/release/find_2225dd4c-ae9a-403b-8ea0-9e05014c778f.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Empire 5 | Official 6 | normal 7 | 8 | eng 9 | 10 | 11 | 12 | Empire 13 | 2006-08-25 14 | Album 15 | 16 | 2006-08-28 17 | GB 18 | 19 | 20 | 2006-08-28 21 | 22 | United Kingdom 23 | United Kingdom 24 | 25 | GB 26 | 27 | 28 | 29 | 30 | B000H49P28 31 | 32 | true 33 | 1 34 | true 35 | false 36 | 37 | 38 | 39 | 1 40 | CD 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /spec/fixtures/release/find_b94cb547-cf7a-4357-894c-53c3bf33b093.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Humanoid 5 | Official 6 | normal 7 | 8 | eng 9 | 10 | 11 | 12 | Humanoid 13 | English version 14 | 2009-10-02 15 | Album 16 | 17 | 2009-10-06 18 | US 19 | 20 | 21 | 2009-10-06 22 | 23 | United States 24 | United States 25 | 26 | US 27 | 28 | 29 | 30 | 31 | 602527197692 32 | B002NOYX6I 33 | 34 | false 35 | 0 36 | false 37 | false 38 | 39 | 40 | 41 | 1 42 | CD 43 | 44 | 45 | 46 | 2 47 | CD 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /spec/models/base_model_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe MusicBrainz::BaseModel do 4 | describe '#validate_type' do 5 | describe 'Date' do 6 | let(:xml) { 7 | Nokogiri::XML.parse(response) 8 | } 9 | let(:bindings) { 10 | MusicBrainz::Bindings::ReleaseGroup.parse(xml) 11 | } 12 | let(:release_group) { 13 | MusicBrainz::ReleaseGroup.new(bindings) 14 | } 15 | 16 | context 'nil value' do 17 | let(:response) { 18 | <<-XML 19 | 20 | 21 | 22 | XML 23 | } 24 | 25 | it 'returns 2030-12-31' do 26 | expect(release_group.first_release_date).to eq Date.new(2030, 12, 31) 27 | end 28 | end 29 | 30 | context 'year only' do 31 | let(:response) { 32 | <<-XML 33 | 34 | 1995 35 | 36 | XML 37 | } 38 | 39 | it 'returns 1995-12-31' do 40 | expect(release_group.first_release_date).to eq Date.new(1995, 12, 31) 41 | end 42 | end 43 | 44 | context 'year and month only' do 45 | let(:response) { 46 | <<-XML 47 | 48 | 1995-04 49 | 50 | XML 51 | } 52 | 53 | it 'returns 1995-04-30' do 54 | expect(release_group.first_release_date).to eq Date.new(1995, 4, 30) 55 | end 56 | end 57 | 58 | context 'year, month and day' do 59 | let(:response) { 60 | <<-XML 61 | 62 | 1995-04-30 63 | 64 | XML 65 | } 66 | 67 | it 'returns 1995-04-30' do 68 | expect(release_group.first_release_date).to eq Date.new(1995, 4, 30) 69 | end 70 | end 71 | end 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /lib/musicbrainz/configuration.rb: -------------------------------------------------------------------------------- 1 | module MusicBrainz 2 | class Configuration 3 | attr_accessor :app_name, :app_version, :contact, 4 | :web_service_url, 5 | :query_interval, :tries_limit, 6 | :cache_path, :perform_caching 7 | 8 | DEFAULT_WEB_SERVICE_URL = "http://musicbrainz.org/ws/2/" 9 | DEFAULT_QUERY_INTERVAL = 1.5 10 | DEFAULT_TRIES_LIMIT = 5 11 | DEFAULT_CACHE_PATH = File.join(File.dirname(__FILE__), "..", "tmp", "cache") 12 | DEFAULT_PERFORM_CACHING = false 13 | 14 | def initialize 15 | @web_service_url = DEFAULT_WEB_SERVICE_URL 16 | @query_interval = DEFAULT_QUERY_INTERVAL 17 | @tries_limit = DEFAULT_TRIES_LIMIT 18 | @cache_path = DEFAULT_CACHE_PATH 19 | @perform_caching = DEFAULT_PERFORM_CACHING 20 | end 21 | 22 | def valid? 23 | %w[ app_name app_version contact ].each do |param| 24 | unless instance_variable_defined?(:"@#{param}") 25 | raise Exception.new("Application identity parameter '#{param}' missing") 26 | end 27 | end 28 | unless tries_limit.nil? && query_interval.nil? 29 | raise Exception.new("'tries_limit' parameter must be 1 or greater") if tries_limit.to_i < 1 30 | raise Exception.new("'query_interval' parameter must be greater than zero") if query_interval.to_f < 0 31 | end 32 | if perform_caching 33 | raise Exception.new("'cache_path' parameter must be set") if cache_path.nil? 34 | end 35 | true 36 | end 37 | end 38 | 39 | module Configurable 40 | def configure 41 | raise Exception.new("Configuration block missing") unless block_given? 42 | yield @config ||= MusicBrainz::Configuration.new 43 | config.valid? 44 | end 45 | 46 | def config 47 | raise Exception.new("Configuration missing") unless instance_variable_defined?(:@config) 48 | @config 49 | end 50 | 51 | def apply_test_configuration! 52 | configure do |c| 53 | c.app_name = "gem musicbrainz (development mode)" 54 | c.app_version = MusicBrainz::VERSION 55 | c.contact = `git config user.email`.chomp 56 | end 57 | end 58 | end 59 | extend Configurable 60 | end 61 | -------------------------------------------------------------------------------- /lib/musicbrainz/client.rb: -------------------------------------------------------------------------------- 1 | module MusicBrainz 2 | class Client 3 | include ClientModules::TransparentProxy 4 | include ClientModules::FailsafeProxy 5 | include ClientModules::CachingProxy 6 | 7 | def http 8 | @faraday ||= Faraday.new do |f| 9 | f.request :url_encoded # form-encode POST params 10 | f.adapter Faraday.default_adapter # make requests with Net::HTTP 11 | f.use MusicBrainz::Middleware # run requests with correct headers 12 | end 13 | end 14 | 15 | def load(resource, query, params) 16 | raise Exception.new("You need to run MusicBrainz.configure before querying") if MusicBrainz.config.nil? 17 | 18 | url = build_url(resource, query) 19 | response = get_contents(url) 20 | 21 | return nil if response[:status] != 200 22 | 23 | xml = Nokogiri::XML.parse(response[:body]).remove_namespaces!.xpath('/metadata') 24 | data = binding_class_for(params[:binding]).parse(xml) 25 | 26 | if params[:create_model] 27 | model_class_for(params[:create_model]).new(data) 28 | elsif params[:create_models] 29 | models = data.map{ |item| model_class_for(params[:create_models]).new(item) } 30 | models.sort!{ |a, b| a.send(params[:sort]) <=> b.send(params[:sort]) } if params[:sort] 31 | models 32 | else 33 | data 34 | end 35 | end 36 | 37 | private 38 | 39 | def build_url(resource, params) 40 | "#{MusicBrainz.config.web_service_url}#{resource.to_s.gsub('_', '-')}" << 41 | ((id = params.delete(:id)) ? "/#{id}?" : "?") << 42 | params.map do |key, value| 43 | key = key.to_s.gsub('_', '-') 44 | value = if value.is_a?(Array) 45 | value.map{ |el| el.to_s.gsub('_', '-') }.join(?+) 46 | else 47 | value.to_s 48 | end 49 | [key, value].join(?=) 50 | end.join(?&) 51 | end 52 | 53 | def binding_class_for(key) 54 | MusicBrainz::Bindings.const_get(constantized(key)) 55 | end 56 | 57 | def model_class_for(key) 58 | MusicBrainz.const_get(constantized(key)) 59 | end 60 | 61 | def constantized(key) 62 | key.to_s.split(?_).map(&:capitalize).join.to_sym 63 | end 64 | end 65 | 66 | module ClientHelper 67 | def client 68 | @client ||= Client.new 69 | end 70 | end 71 | extend ClientHelper 72 | end 73 | -------------------------------------------------------------------------------- /spec/bindings/relations_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe MusicBrainz::Bindings::Relations do 4 | describe '.parse' do 5 | describe 'attributes' do 6 | describe 'urls' do 7 | context '1 url for relation type' do 8 | let(:response) { 9 | <<-XML 10 | 11 | 12 | 13 | https://plus.google.com/+Madonna 14 | 15 | 16 | 17 | XML 18 | } 19 | let(:xml) { 20 | Nokogiri::XML.parse(response) 21 | } 22 | let(:artist) { 23 | described_class.parse(xml.xpath('./artist')) 24 | } 25 | 26 | it 'returns a string' do 27 | expect(artist[:urls][:social_network]).to eq 'https://plus.google.com/+Madonna' 28 | end 29 | end 30 | 31 | context 'multiple urls for relation types' do 32 | let(:response) { 33 | <<-XML 34 | 35 | 36 | 37 | https://plus.google.com/+Madonna 38 | 39 | 40 | https://www.facebook.com/madonna 41 | 42 | 43 | 44 | XML 45 | } 46 | let(:xml) { 47 | Nokogiri::XML.parse(response) 48 | } 49 | let(:artist) { 50 | described_class.parse(xml.xpath('./artist')) 51 | } 52 | 53 | it 'returns an array' do 54 | expect(artist[:urls][:social_network]).to eq [ 55 | 'https://plus.google.com/+Madonna', 56 | 'https://www.facebook.com/madonna', 57 | ] 58 | end 59 | end 60 | end 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /spec/client_modules/cache_spec.rb: -------------------------------------------------------------------------------- 1 | require "ostruct" 2 | require "spec_helper" 3 | 4 | describe MusicBrainz::ClientModules::CachingProxy do 5 | let(:test_mbid){ "69b39eab-6577-46a4-a9f5-817839092033" } 6 | let(:tmp_cache_path){ File.join(File.dirname(__FILE__), '..', '..', 'tmp', 'cache_module_spec_cache') } 7 | let(:test_cache_file){ "#{tmp_cache_path}/03/48/ec/6c2bee685d9a96f95ed46378f624714e7a4650b0d44c1a8eee5bac2480.xml" } 8 | let(:test_response){ read_fixture('artist/find_69b39eab-6577-46a4-a9f5-817839092033.xml') } 9 | 10 | before(:all) do 11 | MusicBrainz.config.cache_path = File.join(File.dirname(__FILE__), '..', '..', 'tmp', 'cache_module_spec_cache') 12 | end 13 | 14 | before do 15 | File.delete(test_cache_file) if File.exist?(test_cache_file) 16 | end 17 | 18 | after(:all) do 19 | MusicBrainz.config.cache_path = File.join(File.dirname(__FILE__), '..', '..', 'tmp', 'spec_cache') 20 | MusicBrainz.config.perform_caching = true 21 | MusicBrainz.config.query_interval = 1.5 22 | end 23 | 24 | context "with cache enabled" do 25 | it "calls http only once when requesting the resource twice" do 26 | MusicBrainz.config.perform_caching = true 27 | expect(File).to_not exist(test_cache_file) 28 | 29 | # Stubbing 30 | allow(MusicBrainz.client).to receive(:get_live_contents) 31 | .and_return({status: 200, body: test_response}) 32 | expect(MusicBrainz.client).to receive(:get_live_contents).once 33 | 34 | 2.times do 35 | artist = MusicBrainz::Artist.find(test_mbid) 36 | expect(artist).to be_kind_of(MusicBrainz::Artist) 37 | expect(File).to exist(test_cache_file) 38 | end 39 | 40 | MusicBrainz.client.clear_cache 41 | end 42 | end 43 | 44 | context "with cache disabled" do 45 | it "calls http twice when requesting the resource twice" do 46 | MusicBrainz.config.perform_caching = false 47 | expect(File).to_not exist(test_cache_file) 48 | 49 | # Hacking for test performance purposes 50 | MusicBrainz.config.query_interval = 0.0 51 | 52 | # Stubbing 53 | allow(MusicBrainz.client.http).to receive(:get).and_return(OpenStruct.new(status: 200, body: test_response)) 54 | expect(MusicBrainz.client.http).to receive(:get).twice 55 | 56 | 2.times do 57 | artist = MusicBrainz::Artist.find(test_mbid) 58 | expect(artist).to be_kind_of(MusicBrainz::Artist) 59 | expect(File).to_not exist(test_cache_file) 60 | end 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /spec/fixtures/release_group/search_kasabian_empire.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Empire 6 | Album 7 | 8 | 9 | 10 | Kasabian 11 | Kasabian 12 | 13 | 14 | 15 | 16 | 17 | Empire 18 | Official 19 | 20 | 21 | Empire 22 | Official 23 | 24 | 25 | Empire 26 | Official 27 | 28 | 29 | Empire 30 | Official 31 | 32 | 33 | Empire 34 | Official 35 | 36 | 37 | Empire 38 | Official 39 | 40 | 41 | Empire 42 | Official 43 | 44 | 45 | 46 | 47 | rock 48 | 49 | 50 | alternative rock 51 | 52 | 53 | indie 54 | 55 | 56 | rock and indie 57 | 58 | 59 | 60 | 61 | Empire 62 | Single 63 | 64 | 65 | 66 | Kasabian 67 | Kasabian 68 | 69 | 70 | 71 | 72 | 73 | Empire 74 | Official 75 | 76 | 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /lib/musicbrainz/models/base_model.rb: -------------------------------------------------------------------------------- 1 | module MusicBrainz 2 | class BaseModel 3 | def self.inherited(klass) 4 | klass.send(:include, InstanceMethods) 5 | klass.send(:extend, ClassMethods) 6 | end 7 | 8 | module ClassMethods 9 | def field(name, type) 10 | fields[name] = type 11 | attr_reader name 12 | 13 | define_method("#{name}=") do |val| 14 | instance_variable_set("@#{name}", validate_type(val, type)) 15 | end 16 | end 17 | 18 | def fields 19 | instance_variable_set(:@fields, {}) unless instance_variable_defined?(:@fields) 20 | instance_variable_get(:@fields) 21 | end 22 | 23 | def client 24 | MusicBrainz.client 25 | end 26 | 27 | def find(hash) 28 | underscored_name = underscore_name.to_sym 29 | client.load(underscored_name, hash, { binding: underscored_name, create_model: underscored_name }) 30 | end 31 | 32 | def search(hash) 33 | hash = escape_strings(hash) 34 | query_val = build_query(hash) 35 | underscored_name = underscore_name 36 | client.load(underscored_name.to_sym, { query: query_val, limit: 10 }, { binding: underscored_name.insert(-1,"_search").to_sym }) 37 | end 38 | 39 | class ::String 40 | def underscore 41 | self.gsub(/::/, '/'). 42 | gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2'). 43 | gsub(/([a-z\d])([A-Z])/,'\1_\2'). 44 | tr("-", "_"). 45 | downcase 46 | end 47 | end 48 | 49 | def build_query(hash) 50 | return ["#{hash.first[0].to_s}:\"#{hash.first[1]}\""] if hash.size ==1 51 | arr ||= [] 52 | hash.each { |k, v| arr << "#{k.to_s}:\"#{hash[k]}\"" } 53 | arr.join(' AND ') 54 | end 55 | 56 | def escape_strings(hash) 57 | hash.each { |k, v| hash[k] = CGI.escape(v).gsub(/\!/, '\!') } 58 | hash 59 | end 60 | 61 | def underscore_name 62 | # self.name[13..-1] => removes MusicBrainz:: 63 | self.name[13..-1].underscore 64 | end 65 | 66 | # these probably should be private... but I'm not sure how to get it to work in a module... 67 | # private_class_method :build_query, :escape_strings 68 | end 69 | 70 | module InstanceMethods 71 | def initialize(params = {}) 72 | params.each do |field, value| 73 | self.send(:"#{field}=", value) 74 | end 75 | end 76 | 77 | def client 78 | MusicBrainz.client 79 | end 80 | 81 | private 82 | 83 | def validate_type(val, type) 84 | if type == Integer 85 | val.to_i 86 | elsif type == Float 87 | val.to_f 88 | elsif type == String 89 | val.to_s 90 | elsif type == Date 91 | val = if val.nil? or val == "" 92 | [2030, 12, 31] 93 | elsif val.split("-").length == 1 94 | [val.split("-").first.to_i, 12, 31] 95 | elsif val.split("-").length == 2 96 | val = val.split("-").map(&:to_i) 97 | [val.first, val.last, -1] 98 | else 99 | val.split("-").map(&:to_i) 100 | end 101 | 102 | Date.new(*val) 103 | elsif type == Array 104 | Array(val) 105 | else 106 | val 107 | end 108 | end 109 | end 110 | end 111 | end 112 | -------------------------------------------------------------------------------- /spec/models/artist_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe MusicBrainz::Artist do 4 | describe '.search' do 5 | before(:each) { 6 | mock url: 'http://musicbrainz.org/ws/2/artist?query=artist:"Kasabian"&limit=10', 7 | fixture: 'artist/search_kasabian.xml' 8 | } 9 | let(:matches) { 10 | MusicBrainz::Artist.search('Kasabian') 11 | } 12 | 13 | it "searches artist by name" do 14 | expect(matches).to_not be_empty 15 | expect(matches.first[:name]).to eq("Kasabian") 16 | end 17 | 18 | it "should return search results in the right order and pass back the correct score" do 19 | expect(matches.length).to be >= 2 20 | expect(matches[0][:score]).to eq 100 21 | expect(matches[0][:id]).to eq "69b39eab-6577-46a4-a9f5-817839092033" 22 | expect(matches[1][:score]).to eq 73 23 | expect(matches[1][:id]).to eq "d76aed40-1332-44db-9b19-aee7ec17464b" 24 | end 25 | end 26 | 27 | describe '.find' do 28 | before(:each) { 29 | mock url: 'http://musicbrainz.org/ws/2/artist/69b39eab-6577-46a4-a9f5-817839092033?inc=url-rels', 30 | fixture: 'artist/find_69b39eab-6577-46a4-a9f5-817839092033.xml' 31 | } 32 | let(:artist) { 33 | MusicBrainz::Artist.find('69b39eab-6577-46a4-a9f5-817839092033') 34 | } 35 | 36 | it "gets no exception while loading artist info" do 37 | expect{ artist }.to_not raise_error(Exception) 38 | expect(artist.id).to eq '69b39eab-6577-46a4-a9f5-817839092033' 39 | expect(artist.name).to eq 'Kasabian' 40 | end 41 | end 42 | 43 | describe '.find_by_name' do 44 | before(:each) { 45 | mock url: 'http://musicbrainz.org/ws/2/artist?query=artist:"Kasabian"&limit=10', 46 | fixture: 'artist/search_kasabian.xml' 47 | mock url: 'http://musicbrainz.org/ws/2/artist/69b39eab-6577-46a4-a9f5-817839092033?inc=url-rels', 48 | fixture: 'artist/find_69b39eab-6577-46a4-a9f5-817839092033.xml' 49 | } 50 | let(:artist) { 51 | MusicBrainz::Artist.find_by_name('Kasabian') 52 | } 53 | 54 | it "gets correct instance" do 55 | expect(artist).to be_instance_of(MusicBrainz::Artist) 56 | end 57 | 58 | it "gets correct result by name" do 59 | expect(artist.id).to eq "69b39eab-6577-46a4-a9f5-817839092033" 60 | end 61 | 62 | it "gets correct artist data" do 63 | expect(artist.id).to eq "69b39eab-6577-46a4-a9f5-817839092033" 64 | expect(artist.type).to eq "Group" 65 | expect(artist.name).to eq "Kasabian" 66 | expect(artist.country).to eq "GB" 67 | expect(artist.date_begin.year).to eq 1997 68 | end 69 | 70 | describe '#release_groups' do 71 | before(:each) { 72 | mock url: 'http://musicbrainz.org/ws/2/release-group?artist=69b39eab-6577-46a4-a9f5-817839092033&inc=url-rels', 73 | fixture: 'artist/release_groups_69b39eab-6577-46a4-a9f5-817839092033.xml' 74 | } 75 | let(:release_groups) { 76 | artist.release_groups 77 | } 78 | 79 | it "gets correct artist's release groups" do 80 | release_groups = MusicBrainz::Artist.find_by_name('Kasabian').release_groups 81 | expect(release_groups.length).to be >= 16 82 | expect(release_groups.first.id).to eq "5547285f-578f-3236-85aa-b65cc0923b58" 83 | expect(release_groups.first.type).to eq "Single" 84 | expect(release_groups.first.title).to eq "Reason Is Treason" 85 | expect(release_groups.first.first_release_date).to eq Date.new(2004, 2, 23) 86 | expect(release_groups.first.urls[:discogs]).to eq 'https://www.discogs.com/master/125172' 87 | end 88 | end 89 | end 90 | end 91 | -------------------------------------------------------------------------------- /spec/bindings/release_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe MusicBrainz::Bindings::Release do 4 | describe '.parse' do 5 | describe 'attributes' do 6 | describe 'format' do 7 | context 'single cd' do 8 | let(:response) { 9 | <<-XML 10 | 11 | 12 | 13 | CD 14 | 15 | 16 | 17 | XML 18 | } 19 | let(:xml) { 20 | Nokogiri::XML.parse(response) 21 | } 22 | let(:release) { 23 | described_class.parse(xml) 24 | } 25 | 26 | it 'returns CD' do 27 | expect(release[:format]).to eq 'CD' 28 | end 29 | end 30 | 31 | context 'multiple cds' do 32 | let(:response) { 33 | <<-XML 34 | 35 | 36 | 37 | CD 38 | 39 | 40 | 41 | bonus disc 42 | CD 43 | 44 | 45 | 46 | XML 47 | } 48 | let(:xml) { 49 | Nokogiri::XML.parse(response) 50 | } 51 | let(:release) { 52 | described_class.parse(xml) 53 | } 54 | 55 | it 'returns 2xCD' do 56 | expect(release[:format]).to eq '2xCD' 57 | end 58 | end 59 | 60 | context 'different formats' do 61 | let(:response) { 62 | <<-XML 63 | 64 | 65 | 66 | DVD 67 | 68 | 69 | CD 70 | 71 | 72 | 73 | XML 74 | } 75 | let(:xml) { 76 | Nokogiri::XML.parse(response) 77 | } 78 | let(:release) { 79 | described_class.parse(xml) 80 | } 81 | 82 | it 'returns DVD + CD' do 83 | expect(release[:format]).to eq 'DVD + CD' 84 | end 85 | end 86 | 87 | context 'different formats plus multiple mediums with same format' do 88 | let(:response) { 89 | <<-XML 90 | 91 | 92 | 93 | CD 94 | 95 | 96 | CD 97 | 98 | 99 | DVD 100 | 101 | 102 | 103 | XML 104 | } 105 | let(:xml) { 106 | Nokogiri::XML.parse(response) 107 | } 108 | let(:release) { 109 | described_class.parse(xml) 110 | } 111 | 112 | it 'returns 2xCD + DVD' do 113 | expect(release[:format]).to eq '2xCD + DVD' 114 | end 115 | end 116 | end 117 | end 118 | end 119 | end 120 | -------------------------------------------------------------------------------- /spec/models/release_group_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe MusicBrainz::ReleaseGroup do 4 | before(:each) { 5 | mock url: 'http://musicbrainz.org/ws/2/release-group/6f33e0f0-cde2-38f9-9aee-2c60af8d1a61?inc=url-rels', 6 | fixture: 'release_group/find_6f33e0f0-cde2-38f9-9aee-2c60af8d1a61.xml' 7 | 8 | mock url: 'http://musicbrainz.org/ws/2/release-group?query=artist:"Kasabian" AND releasegroup:"Empire"&limit=10', 9 | fixture: 'release_group/search_kasabian_empire.xml' 10 | 11 | mock url: 'http://musicbrainz.org/ws/2/release-group?query=artist:"Kasabian" AND releasegroup:"Empire" AND type:"Album"&limit=10', 12 | fixture: 'release_group/search_kasabian_empire_album.xml' 13 | 14 | mock url: 'http://musicbrainz.org/ws/2/release?release-group=6f33e0f0-cde2-38f9-9aee-2c60af8d1a61&inc=media+release-groups&limit=100', 15 | fixture: 'release/list_6f33e0f0-cde2-38f9-9aee-2c60af8d1a61.xml' 16 | } 17 | 18 | describe '.find' do 19 | let(:release_group) { 20 | MusicBrainz::ReleaseGroup.find("6f33e0f0-cde2-38f9-9aee-2c60af8d1a61") 21 | } 22 | 23 | it "gets no exception while loading release group info" do 24 | expect { release_group }.to_not raise_error(Exception) 25 | end 26 | 27 | it "gets correct instance" do 28 | expect(release_group).to be_an_instance_of(MusicBrainz::ReleaseGroup) 29 | end 30 | 31 | it "gets correct release group data" do 32 | expect(release_group.id).to eq "6f33e0f0-cde2-38f9-9aee-2c60af8d1a61" 33 | expect(release_group.type).to eq "Album" 34 | expect(release_group.title).to eq "Empire" 35 | expect(release_group.first_release_date).to eq Date.new(2006, 8, 28) 36 | expect(release_group.urls[:wikipedia]).to eq 'http://en.wikipedia.org/wiki/Empire_(Kasabian_album)' 37 | end 38 | end 39 | 40 | describe '.search' do 41 | context 'without type filter' do 42 | let(:matches) { 43 | MusicBrainz::ReleaseGroup.search('Kasabian', 'Empire') 44 | } 45 | 46 | it "searches release group by artist name and title" do 47 | expect(matches.length).to be > 0 48 | expect(matches.first[:title]).to eq 'Empire' 49 | expect(matches.first[:type]).to eq 'Album' 50 | end 51 | end 52 | 53 | context 'with type filter' do 54 | let(:matches) { 55 | MusicBrainz::ReleaseGroup.search('Kasabian', 'Empire', 'Album') 56 | } 57 | 58 | it "searches release group by artist name and title" do 59 | expect(matches.length).to be > 0 60 | expect(matches.first[:title]).to eq 'Empire' 61 | expect(matches.first[:type]).to eq 'Album' 62 | end 63 | end 64 | end 65 | 66 | describe '.find_by_artist_and_title' do 67 | let(:release_group) { 68 | MusicBrainz::ReleaseGroup.find_by_artist_and_title('Kasabian', 'Empire') 69 | } 70 | 71 | it "gets first release group by artist name and title" do 72 | expect(release_group.id).to eq '6f33e0f0-cde2-38f9-9aee-2c60af8d1a61' 73 | end 74 | end 75 | 76 | describe '#releases' do 77 | let(:releases) { 78 | MusicBrainz::ReleaseGroup.find("6f33e0f0-cde2-38f9-9aee-2c60af8d1a61").releases 79 | } 80 | 81 | it "gets correct release group's releases" do 82 | expect(releases.length).to be >= 5 83 | expect(releases.first.id).to eq "2225dd4c-ae9a-403b-8ea0-9e05014c778f" 84 | expect(releases.first.status).to eq "Official" 85 | expect(releases.first.title).to eq "Empire" 86 | expect(releases.first.date).to eq Date.new(2006, 8, 28) 87 | expect(releases.first.country).to eq "GB" 88 | expect(releases.first.type).to eq "Album" 89 | end 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /spec/models/release_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe MusicBrainz::Release do 4 | before(:each) { 5 | mock url: 'http://musicbrainz.org/ws/2/release/2225dd4c-ae9a-403b-8ea0-9e05014c778f?inc=media+release-groups', 6 | fixture: 'release/find_2225dd4c-ae9a-403b-8ea0-9e05014c778f.xml' 7 | 8 | mock url: 'http://musicbrainz.org/ws/2/release/2225dd4c-ae9a-403b-8ea0-9e05014c778f?inc=recordings+media&limit=100', 9 | fixture: 'release/find_2225dd4c-ae9a-403b-8ea0-9e05014c778f_tracks.xml' 10 | 11 | mock url: 'http://musicbrainz.org/ws/2/release/b94cb547-cf7a-4357-894c-53c3bf33b093?inc=media+release-groups', 12 | fixture: 'release/find_b94cb547-cf7a-4357-894c-53c3bf33b093.xml' 13 | 14 | mock url: 'http://musicbrainz.org/ws/2/discid/pmzhT6ZlFiwSRCdVwV0eqire5_Y-?', 15 | fixture: 'release/find_by_discid_pmzhT6ZlFiwSRCdVwV0eqire5_Y-.xml' 16 | } 17 | 18 | describe '.find' do 19 | let(:release) { 20 | MusicBrainz::Release.find("b94cb547-cf7a-4357-894c-53c3bf33b093") 21 | } 22 | 23 | it "gets no exception while loading release info" do 24 | expect { release }.to_not raise_error(Exception) 25 | end 26 | 27 | it "gets correct instance" do 28 | expect(release).to be_an_instance_of(MusicBrainz::Release) 29 | end 30 | 31 | it "gets correct release data" do 32 | expect(release.id).to eq "b94cb547-cf7a-4357-894c-53c3bf33b093" 33 | expect(release.title).to eq "Humanoid" 34 | expect(release.status).to eq "Official" 35 | expect(release.date).to eq Date.new(2009, 10, 6) 36 | expect(release.country).to eq "US" 37 | expect(release.asin).to eq 'B002NOYX6I' 38 | expect(release.barcode).to eq '602527197692' 39 | expect(release.quality).to eq 'normal' 40 | expect(release.type).to eq 'Album' 41 | end 42 | end 43 | 44 | describe '#mediums' do 45 | subject(:mediums) { 46 | release.mediums 47 | } 48 | 49 | let(:release) { 50 | MusicBrainz::Release.find("2225dd4c-ae9a-403b-8ea0-9e05014c778f") 51 | } 52 | 53 | it "gets correct release mediums" do 54 | expect(mediums.length).to eq 1 55 | expect(mediums.first.position).to eq 1 56 | expect(mediums.first.format).to eq "CD" 57 | end 58 | 59 | it "gets correct mediums tracks" do 60 | expect(mediums.first.tracks.length).to eq 11 61 | expect(mediums.first.tracks.first.position).to eq 1 62 | expect(mediums.first.tracks.first.recording_id).to eq "b3015bab-1540-4d4e-9f30-14872a1525f7" 63 | expect(mediums.first.tracks.first.title).to eq "Empire" 64 | expect(mediums.first.tracks.first.length).to eq 233013 65 | end 66 | end 67 | 68 | describe '#tracks' do 69 | let(:release) { 70 | MusicBrainz::Release.find("2225dd4c-ae9a-403b-8ea0-9e05014c778f") 71 | } 72 | let(:tracks) { 73 | release.tracks 74 | } 75 | 76 | it "gets correct release tracks" do 77 | expect(tracks.length).to eq 11 78 | expect(tracks.first.position).to eq 1 79 | expect(tracks.first.recording_id).to eq "b3015bab-1540-4d4e-9f30-14872a1525f7" 80 | expect(tracks.first.title).to eq "Empire" 81 | expect(tracks.first.length).to eq 233013 82 | end 83 | end 84 | 85 | describe '.find_by_discid' do 86 | let(:releases) { 87 | MusicBrainz::Release.find_by_discid("pmzhT6ZlFiwSRCdVwV0eqire5_Y-") 88 | } 89 | 90 | it "gets a list of matching releases for a discid" do 91 | expect(releases.length).to eq 2 92 | expect(releases.first.id).to eq "7a31cd5f-6a57-4fca-a731-c521df1d3b78" 93 | expect(releases.first.title).to eq "Kveikur" 94 | expect(releases.first.asin).to eq "B00C1GBOU6" 95 | end 96 | end 97 | end 98 | -------------------------------------------------------------------------------- /spec/fixtures/artist/search_kasabian.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Kasabian 6 | Kasabian 7 | GB 8 | 9 | United Kingdom 10 | United Kingdom 11 | 12 | false 13 | 14 | 15 | 16 | Leicester 17 | Leicester 18 | 19 | false 20 | 21 | 22 | 23 | 0000000106652390 24 | 25 | 26 | 1997 27 | false 28 | 29 | 30 | 31 | rock 32 | 33 | 34 | alternative rock 35 | 36 | 37 | british 38 | 39 | 40 | indie rock 41 | 42 | 43 | indie 44 | 45 | 46 | alternative 47 | 48 | 49 | neo-psychedelia 50 | 51 | 52 | alternative dance 53 | 54 | 55 | dance-punk 56 | 57 | 58 | indietronica 59 | 60 | 61 | rock and indie 62 | 63 | 64 | post-baggy 65 | 66 | 67 | 68 | 69 | Mother Kasabian 70 | Mother Kasabian 71 | 72 | Stockholm 73 | Stockholm 74 | 75 | false 76 | 77 | 78 | 79 | 2011 80 | false 81 | 82 | 83 | 84 | psychedelic 85 | 86 | 87 | experimental 88 | 89 | 90 | metal 91 | 92 | 93 | doom 94 | 95 | 96 | stoner 97 | 98 | 99 | sludge 100 | 101 | 102 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## MusicBrainz Web Service wrapper 2 | 3 | ```ruby 4 | gem 'musicbrainz', '0.5.2' 5 | ``` 6 | 7 | ### Installation 8 | ``` 9 | gem install musicbrainz 10 | ``` 11 | or add this line to your Gemfile 12 | ```ruby 13 | gem 'musicbrainz' 14 | ``` 15 | 16 | ### Configuration 17 | ```ruby 18 | MusicBrainz.configure do |c| 19 | # Application identity (required) 20 | c.app_name = "My Music App" 21 | c.app_version = "1.0" 22 | c.contact = "support@mymusicapp.com" 23 | 24 | # Cache config (optional) 25 | c.cache_path = "/tmp/musicbrainz-cache" 26 | c.perform_caching = true 27 | 28 | # Querying config (optional) 29 | c.query_interval = 1.2 # seconds 30 | c.tries_limit = 2 31 | end 32 | ``` 33 | 34 | ### Usage 35 | ```ruby 36 | require 'musicbrainz' 37 | 38 | # Search for artists 39 | @suggestions = MusicBrainz::Artist.search("Jet") 40 | 41 | # Find artist by name or mbid 42 | @foo_fighters = MusicBrainz::Artist.find_by_name("Foo Fighters") 43 | @kasabian = MusicBrainz::Artist.find("69b39eab-6577-46a4-a9f5-817839092033") 44 | 45 | # Use them like ActiveRecord models 46 | @empire_tracks = @kasabian.release_groups[8].releases.first.tracks 47 | ``` 48 | 49 | ### Models 50 | 51 | MusicBrainz::Artist 52 | ```ruby 53 | # Class Methods: 54 | MusicBrainz::Artist.find(id) 55 | MusicBrainz::Artist.find_by_name(name) 56 | MusicBrainz::Artist.search(name) 57 | MusicBrainz::Artist.discography(id) 58 | 59 | # Instance Methods: 60 | @artist.release_groups 61 | 62 | # Fields 63 | { 64 | :id => String, 65 | :type => String, 66 | :name => String, 67 | :country => String, 68 | :date_begin => Date, 69 | :date_end => Date, 70 | :urls => Hash 71 | } 72 | ``` 73 | 74 | MusicBrainz::ReleaseGroup 75 | ```ruby 76 | # Class Methods 77 | MusicBrainz::ReleaseGroup.find(id) 78 | MusicBrainz::ReleaseGroup.find_by_artist_and_title(artist_name, title, 'Album') # 3rd arg optional 79 | MusicBrainz::ReleaseGroup.search(artist_name, title) 80 | MusicBrainz::ReleaseGroup.search(artist_name, title, 'Album') # 3rd arg optional 81 | 82 | # Instance Methods 83 | @release_group.releases 84 | 85 | # Fields 86 | { 87 | :id => String, 88 | :type => String, 89 | :title => String, 90 | :desc => String, 91 | :first_release_date => Date, 92 | :urls => Hash 93 | } 94 | ``` 95 | 96 | MusicBrainz::Release 97 | ```ruby 98 | # Class Methods 99 | MusicBrainz::Release.find(id) 100 | 101 | # Instance Methods 102 | @release.tracks 103 | 104 | # Fields 105 | { 106 | :id => String, 107 | :type => String, 108 | :title => String, 109 | :status => String, 110 | :format => String, 111 | :date => Date, 112 | :country => String, 113 | :asin => String, 114 | :barcode => String, 115 | :quality => String 116 | } 117 | ``` 118 | 119 | MusicBrainz::Track (depreciated, now called Recording) 120 | ```ruby 121 | # Class Methods 122 | MusicBrainz::Track.find(id) 123 | 124 | # Fields 125 | { 126 | :position => Integer, 127 | :recording_id => String, 128 | :title => String, 129 | :length => Integer 130 | } 131 | ``` 132 | 133 | MusicBrainz::Recording 134 | ```ruby 135 | # Class Methods 136 | MusicBrainz::Recording.find(id) 137 | MusicBrainz::Recording.search(track_name, artist_name) 138 | 139 | # Fields 140 | { 141 | :id => Integer, 142 | :mbid => Integer, 143 | :title => String, 144 | :artist => String, 145 | :releases => String, 146 | :score => Integer 147 | } 148 | ``` 149 | 150 | ### Testing 151 | ``` 152 | bundle exec rspec 153 | ``` 154 | 155 | ### Debug console 156 | ``` 157 | bundle exec irb -r musicbrainz 158 | ``` 159 | 160 | ### Contributing 161 | 162 | * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet 163 | * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it 164 | * Fork the project 165 | * **Start a feature/bugfix branch (IMPORTANT)** 166 | * Commit and push until you are happy with your contribution 167 | * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally. 168 | * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it. 169 | 170 | ### License 171 | 172 | [MIT](https://raw.github.com/localhots/musicbrainz/master/LICENSE) 173 | -------------------------------------------------------------------------------- /spec/fixtures/release_group/search_kasabian_empire_album.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Empire 6 | 2006-08-25 7 | Album 8 | 9 | 10 | Kasabian 11 | 12 | Kasabian 13 | Kasabian 14 | 15 | 16 | 17 | 18 | 19 | Empire 20 | Official 21 | 22 | 23 | Empire 24 | Official 25 | 26 | 27 | Empire 28 | Official 29 | 30 | 31 | Empire 32 | Official 33 | 34 | 35 | Empire 36 | Official 37 | 38 | 39 | Empire 40 | Official 41 | 42 | 43 | Empire 44 | Official 45 | 46 | 47 | Empire 48 | Official 49 | 50 | 51 | Empire 52 | Official 53 | 54 | 55 | Empire 56 | Official 57 | 58 | 59 | Empire 60 | Official 61 | 62 | 63 | Empire 64 | Official 65 | 66 | 67 | Empire 68 | Official 69 | 70 | 71 | Empire 72 | Official 73 | 74 | 75 | 76 | 77 | rock 78 | 79 | 80 | alternative rock 81 | 82 | 83 | indie rock 84 | 85 | 86 | indie 87 | 88 | 89 | rock and indie 90 | 91 | 92 | 93 | 94 | Empire / West Ryder Pauper Lunatic Asylum 95 | 2011 96 | Album 97 | 98 | Compilation 99 | 100 | 101 | 102 | Kasabian 103 | 104 | Kasabian 105 | Kasabian 106 | 107 | 108 | 109 | 110 | 111 | Empire / West Ryder Pauper Lunatic Asylum 112 | Official 113 | 114 | 115 | 116 | 117 | 118 | -------------------------------------------------------------------------------- /spec/fixtures/release/find_by_discid_pmzhT6ZlFiwSRCdVwV0eqire5_Y-.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 217171 5 | 6 | 150 7 | 35000 8 | 63711 9 | 86440 10 | 105891 11 | 128017 12 | 154668 13 | 177007 14 | 200379 15 | 16 | 17 | 18 | Kveikur 19 | Official 20 | normal 21 | Cardboard/Paper Sleeve 22 | 23 | isl 24 | 25 | 26 | 2013-06-17 27 | XE 28 | 29 | 30 | 2013-06-17 31 | 32 | Europe 33 | Europe 34 | 35 | XE 36 | 37 | 38 | 39 | 40 | 2013-06-18 41 | 42 | United States 43 | United States 44 | 45 | US 46 | 47 | 48 | 49 | 50 | 634904060626 51 | B00C1GBOU6 52 | 53 | true 54 | 14 55 | true 56 | true 57 | 58 | 59 | 60 | 1 61 | CD 62 | 63 | 64 | 217171 65 | 66 | 150 67 | 35000 68 | 63711 69 | 86440 70 | 105891 71 | 128017 72 | 154668 73 | 177007 74 | 200379 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | Kveikur 84 | Official 85 | normal 86 | Cardboard/Paper Sleeve 87 | 88 | eng 89 | 90 | 91 | 2013-06-14 92 | TW 93 | 94 | 95 | 2013-06-14 96 | 97 | Taiwan 98 | Taiwan 99 | 100 | TW 101 | 102 | 103 | CN-71 104 | 105 | 106 | 107 | 108 | 4712765169088 109 | 110 | true 111 | 1 112 | true 113 | false 114 | 115 | 116 | 117 | 1 118 | CD 119 | 120 | 121 | 217171 122 | 123 | 150 124 | 35000 125 | 63711 126 | 86440 127 | 105891 128 | 128017 129 | 154668 130 | 177007 131 | 200379 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | -------------------------------------------------------------------------------- /spec/fixtures/release/find_2225dd4c-ae9a-403b-8ea0-9e05014c778f_tracks.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Empire 5 | Official 6 | normal 7 | 8 | eng 9 | 10 | 11 | 2006-08-28 12 | GB 13 | 14 | 15 | 2006-08-28 16 | 17 | United Kingdom 18 | United Kingdom 19 | 20 | GB 21 | 22 | 23 | 24 | 25 | B000H49P28 26 | 27 | true 28 | 1 29 | true 30 | false 31 | 32 | 33 | 34 | 1 35 | CD 36 | 37 | 38 | 1 39 | 1 40 | 233013 41 | 42 | Empire 43 | 233013 44 | 2006-08-25 45 | 46 | 47 | 48 | 2 49 | 2 50 | 207373 51 | 52 | Shoot the Runner 53 | 207373 54 | album version 55 | 2006-08-25 56 | 57 | 58 | 59 | 3 60 | 3 61 | 173160 62 | 63 | Last Trip (In Flight) 64 | 173160 65 | 2006-08-25 66 | 67 | 68 | 69 | 4 70 | 4 71 | 148733 72 | 73 | Me Plus One 74 | 148733 75 | 2006-08-25 76 | 77 | 78 | 79 | 5 80 | 5 81 | 248653 82 | 83 | Sun Rise Light Flies 84 | 248653 85 | 2006-08-25 86 | 87 | 88 | 89 | 6 90 | 6 91 | 107973 92 | 93 | Apnoea 94 | 107973 95 | 2006-08-25 96 | 97 | 98 | 99 | 7 100 | 7 101 | 254373 102 | 103 | By My Side 104 | 254373 105 | 2006-08-25 106 | 107 | 108 | 109 | 8 110 | 8 111 | 319626 112 | 113 | Stuntman 114 | 319626 115 | 2006-08-25 116 | 117 | 118 | 119 | 9 120 | 9 121 | 135053 122 | 123 | Seek & Destroy 124 | 135053 125 | 2006-08-25 126 | 127 | 128 | 129 | 10 130 | 10 131 | 199866 132 | 133 | British Legion 134 | 199866 135 | 2006-08-25 136 | 137 | 138 | 139 | 11 140 | 11 141 | 1156293 142 | 143 | The Doberman / [untitled] 144 | 333973 145 | 2006-08-28 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | -------------------------------------------------------------------------------- /spec/fixtures/artist/find_69b39eab-6577-46a4-a9f5-817839092033.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Kasabian 5 | Kasabian 6 | 7 | 0000000106652390 8 | 9 | GB 10 | 11 | United Kingdom 12 | United Kingdom 13 | 14 | GB 15 | 16 | 17 | 18 | Leicester 19 | Leicester 20 | 21 | GB-LCE 22 | 23 | 24 | 25 | 1997 26 | 27 | 28 | 29 | https://www.allmusic.com/artist/mn0000361541 30 | forward 31 | 32 | 33 | https://www.bbc.co.uk/music/artists/69b39eab-6577-46a4-a9f5-817839092033 34 | forward 35 | 2020-11-19 36 | true 37 | 38 | 39 | https://www.discogs.com/artist/235133 40 | forward 41 | 42 | 43 | https://open.spotify.com/artist/11wRdbnoYqRddKBrpHt4Ue 44 | forward 45 | 46 | 47 | https://www.deezer.com/artist/1247 48 | forward 49 | 50 | 51 | https://commons.wikimedia.org/wiki/File:Kasabian_at_Brixton_Academy_2009.jpg 52 | forward 53 | 54 | 55 | https://www.imdb.com/name/nm1868442/ 56 | forward 57 | 58 | 59 | https://www.last.fm/music/Kasabian 60 | forward 61 | 62 | 63 | https://genius.com/artists/Kasabian 64 | forward 65 | 66 | 67 | https://muzikum.eu/en/kasabian/lyrics 68 | forward 69 | 70 | 71 | https://myspace.com/kasabian 72 | forward 73 | 74 | 75 | https://www.kasabian.co.uk/ 76 | forward 77 | 78 | 79 | http://id.loc.gov/authorities/names/n2005068609 80 | forward 81 | 82 | 83 | https://catalogue.bnf.fr/ark:/12148/cb145796883 84 | forward 85 | 86 | 87 | https://d-nb.info/gnd/10336310-5 88 | forward 89 | 90 | 91 | https://rateyourmusic.com/artist/kasabian 92 | forward 93 | 94 | 95 | http://www.worldcat.org/identities/lccn-n2005068609/ 96 | forward 97 | true 98 | 99 | 100 | https://play.google.com/store/music/artist?id=A2chuocuj6o6rswcqvmb6h2fzn4 101 | forward 102 | 2020-10-12 103 | true 104 | 105 | 106 | https://itunes.apple.com/us/artist/id20478838 107 | forward 108 | 109 | 110 | https://secondhandsongs.com/artist/25440 111 | forward 112 | 113 | 114 | https://twitter.com/kasabianhq 115 | forward 116 | 117 | 118 | https://www.facebook.com/kasabian 119 | forward 120 | 121 | 122 | https://www.instagram.com/kasabianofficial/ 123 | forward 124 | 125 | 126 | https://www.songkick.com/artists/175029 127 | forward 128 | 129 | 130 | https://soundcloud.com/kasabian 131 | forward 132 | 133 | 134 | https://music.apple.com/gb/artist/20478838 135 | forward 136 | 137 | 138 | https://music.youtube.com/channel/UCmJDzMXYTdyjtp5VNONQRKg 139 | forward 140 | 141 | 142 | https://tidal.com/artist/1556 143 | forward 144 | 145 | 146 | http://viaf.org/viaf/138707200 147 | forward 148 | 149 | 150 | https://www.wikidata.org/wiki/Q272517 151 | forward 152 | 153 | 154 | https://www.youtube.com/channel/UC_niYTRmUdRvVNdGYIA1vcg 155 | forward 156 | 157 | 158 | https://www.youtube.com/user/KasabianVEVO 159 | forward 160 | 161 | 162 | https://www.youtube.com/user/KasabianTour 163 | forward 164 | true 165 | 166 | 167 | 168 | 169 | -------------------------------------------------------------------------------- /spec/fixtures/release/list_6f33e0f0-cde2-38f9-9aee-2c60af8d1a61.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Empire 6 | Official 7 | normal 8 | Jewel Case 9 | 10 | eng 11 | 12 | 13 | 14 | Empire 15 | 2006-08-28 16 | Album 17 | 18 | 2006 19 | XE 20 | 21 | 22 | 2006 23 | 24 | Europe 25 | Europe 26 | 27 | XE 28 | 29 | 30 | 31 | 32 | 828768934227 33 | 34 | false 35 | 0 36 | false 37 | false 38 | 39 | 40 | 41 | 1 42 | CD 43 | 44 | 45 | 46 | 47 | 48 | Empire 49 | Official 50 | normal 51 | 52 | eng 53 | 54 | 55 | 56 | Empire 57 | 2006-08-28 58 | Album 59 | 60 | 2006-08-28 61 | GB 62 | 63 | 64 | 2006-08-28 65 | 66 | United Kingdom 67 | United Kingdom 68 | 69 | GB 70 | 71 | 72 | 73 | 74 | B000H49P28 75 | 76 | false 77 | 0 78 | false 79 | false 80 | 81 | 82 | 83 | 1 84 | 85 | 86 | 87 | 88 | 89 | Empire 90 | Official 91 | normal 92 | 93 | eng 94 | 95 | 96 | 97 | Empire 98 | 2006-08-28 99 | Album 100 | 101 | 2006-09-19 102 | US 103 | 104 | 105 | 2006-09-19 106 | 107 | United States 108 | United States 109 | 110 | US 111 | 112 | 113 | 114 | 115 | 828768832325 116 | B000HEVYQS 117 | 118 | false 119 | 0 120 | false 121 | false 122 | 123 | 124 | 125 | 1 126 | CD 127 | 128 | 129 | 130 | 131 | 132 | Empire 133 | Official 134 | normal 135 | 136 | eng 137 | 138 | 139 | 140 | Empire 141 | 2006-08-28 142 | Album 143 | 144 | 2006-09-22 145 | XW 146 | 147 | 148 | 2006-09-22 149 | 150 | [Worldwide] 151 | [Worldwide] 152 | 153 | XW 154 | 155 | 156 | 157 | 158 | 159 | false 160 | 0 161 | false 162 | false 163 | 164 | 165 | 166 | 1 167 | Digital Media 168 | 169 | 170 | 171 | 172 | 173 | Empire 174 | Official 175 | normal 176 | 177 | eng 178 | 179 | 180 | 181 | Empire 182 | 2006-08-28 183 | Album 184 | 185 | 2006-08-28 186 | GB 187 | 188 | 189 | 2006-08-28 190 | 191 | United Kingdom 192 | United Kingdom 193 | 194 | GB 195 | 196 | 197 | 198 | 199 | 828768849927 200 | B000H49P28 201 | 202 | false 203 | 0 204 | false 205 | false 206 | 207 | 208 | 209 | 1 210 | CD 211 | 212 | 213 | 214 | 215 | 216 | Empire 217 | Official 218 | normal 219 | Jewel Case 220 | 221 | eng 222 | 223 | 224 | 225 | Empire 226 | 2006-08-28 227 | Album 228 | 229 | 2006 230 | GB 231 | 232 | 233 | 2006 234 | 235 | United Kingdom 236 | United Kingdom 237 | 238 | GB 239 | 240 | 241 | 242 | 243 | 828768934227 244 | 245 | false 246 | 0 247 | false 248 | false 249 | 250 | 251 | 252 | 1 253 | CD 254 | 255 | 256 | 257 | 258 | 259 | Empire 260 | Official 261 | normal 262 | 263 | eng 264 | 265 | 266 | 267 | Empire 268 | 2006-08-28 269 | Album 270 | 271 | 2006-12-20 272 | JP 273 | 274 | 275 | 2006-12-20 276 | 277 | Japan 278 | Japan 279 | 280 | JP 281 | 282 | 283 | 284 | 285 | 4988017645901 286 | B000K2QL9W 287 | 288 | false 289 | 0 290 | false 291 | false 292 | 293 | 294 | 295 | 1 296 | CD 297 | 298 | 299 | 300 | 301 | 302 | Empire 303 | Official 304 | normal 305 | Jewel Case 306 | 307 | eng 308 | 309 | 310 | 311 | Empire 312 | 2006-08-28 313 | Album 314 | 315 | 2006-12-18 316 | GB 317 | 318 | 319 | 2006-12-18 320 | 321 | United Kingdom 322 | United Kingdom 323 | 324 | GB 325 | 326 | 327 | 328 | 329 | 330 | true 331 | 1 332 | true 333 | false 334 | 335 | 336 | 337 | 1 338 | Digital Media 339 | 340 | 341 | 342 | 343 | 344 | 345 | -------------------------------------------------------------------------------- /spec/fixtures/recording/search_bound_for_the_floor_local_h.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Bound for the Floor 6 | 7 | 8 | Local H 9 | 10 | Local H 11 | Local H 12 | 13 | Local H. 14 | 15 | 16 | 17 | 18 | 1997-04-28 19 | 20 | 21 | Blackrock 22 | Official 23 | 24 | 25 | Various Artists 26 | 27 | Various Artists 28 | Various Artists 29 | add compilations to this artist 30 | 31 | 32 | 33 | 34 | Blackrock 35 | Album 36 | 37 | Soundtrack 38 | 39 | 40 | 1997-04-28 41 | AU 42 | 43 | 44 | 1997-04-28 45 | 46 | Australia 47 | Australia 48 | 49 | AU 50 | 51 | 52 | 53 | 54 | 55 | 15 56 | 57 | 1 58 | 59 | 60 | 8 61 | Bound for the Floor 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | Bound for the Floor 71 | 224000 72 | 73 | 74 | Local H 75 | 76 | Local H 77 | Local H 78 | 79 | Local H. 80 | 81 | 82 | 83 | 84 | 2001 85 | 86 | 87 | Killer Buzz 88 | Official 89 | 90 | 91 | Various Artists 92 | 93 | Various Artists 94 | Various Artists 95 | add compilations to this artist 96 | 97 | 98 | 99 | 100 | Killer Buzz 101 | Album 102 | 103 | Compilation 104 | 105 | 106 | 2001 107 | US 108 | 109 | 110 | 2001 111 | 112 | United States 113 | United States 114 | 115 | US 116 | 117 | 118 | 119 | 120 | 121 | 32 122 | 123 | 1 124 | CD 125 | 126 | 127 | 11 128 | Bound for the Floor 129 | 224000 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | Bound for the Floor 139 | 230773 140 | 141 | 142 | Local H 143 | 144 | Local H 145 | Local H 146 | 147 | Local H. 148 | 149 | 150 | 151 | 152 | 153 | 154 | Unplugged & Burnt Out 155 | Official 156 | 157 | 158 | Various Artists 159 | 160 | Various Artists 161 | Various Artists 162 | add compilations to this artist 163 | 164 | 165 | 166 | 167 | Unplugged & Burnt Out 168 | Album 169 | 170 | Compilation 171 | 172 | 173 | 174 | 12 175 | 176 | 1 177 | 178 | 179 | 8 180 | Bound for the Floor 181 | 230773 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | Bound for the Floor 191 | 231880 192 | 193 | 194 | Local H 195 | 196 | Local H 197 | Local H 198 | 199 | Local H. 200 | 201 | 202 | 203 | 204 | 1997-11-25 205 | 206 | 207 | WAAF 107.3 FM: Royal Flush: Live On‐air 208 | Official 209 | 210 | 211 | Various Artists 212 | 213 | Various Artists 214 | Various Artists 215 | add compilations to this artist 216 | 217 | 218 | 219 | 220 | WAAF 107.3 FM: Royal Flush: Live On‐air 221 | Album 222 | 223 | Live 224 | 225 | 226 | 1997-11-25 227 | US 228 | 229 | 230 | 1997-11-25 231 | 232 | United States 233 | United States 234 | 235 | US 236 | 237 | 238 | 239 | 240 | 241 | 26 242 | 243 | 1 244 | CD 245 | 246 | 247 | 25 248 | Bound for the Floor 249 | 231880 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | Bound for the Floor 259 | 218360 260 | live 261 | 262 | 263 | Local H 264 | 265 | Local H 266 | Local H 267 | 268 | Local H. 269 | 270 | 271 | 272 | 273 | 1998 274 | 275 | 276 | The Point Platinum Version 1.0 277 | Official 278 | 279 | 280 | Various Artists 281 | 282 | Various Artists 283 | Various Artists 284 | add compilations to this artist 285 | 286 | 287 | 288 | 289 | The Point Platinum Version 1.0 290 | Album 291 | 292 | Compilation 293 | 294 | 295 | 1998 296 | US 297 | 298 | 299 | 1998 300 | 301 | United States 302 | United States 303 | 304 | US 305 | 306 | 307 | 308 | 309 | 310 | 32 311 | 312 | 2 313 | CD 314 | 315 | 316 | 6 317 | Bound for the Floor 318 | 218360 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | Bound for the Floor 328 | 223866 329 | 330 | 331 | Local H 332 | 333 | Local H 334 | Local H 335 | 336 | Local H. 337 | 338 | 339 | 340 | 341 | 342 | 343 | Radio 104 WMRQ Back to School 344 | 345 | 346 | Various Artists 347 | 348 | Various Artists 349 | Various Artists 350 | add compilations to this artist 351 | 352 | 353 | 354 | 355 | Radio 104 WMRQ Back to School 356 | Album 357 | 358 | Compilation 359 | 360 | 361 | 362 | 15 363 | 364 | 1 365 | 366 | 367 | 2 368 | Bound for the Floor 369 | 223866 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | Bound For the Floor 379 | 225000 380 | 381 | 382 | Local H 383 | 384 | Local H 385 | Local H 386 | 387 | Local H. 388 | 389 | 390 | 391 | 392 | 2018-02-06 393 | 394 | 395 | Live In Europe 396 | Official 397 | 398 | 399 | Local H 400 | 401 | Local H 402 | Local H 403 | 404 | 405 | 406 | 407 | Live In Europe 408 | Album 409 | 410 | Live 411 | 412 | 413 | 2018-02-06 414 | US 415 | 416 | 417 | 2018-02-06 418 | 419 | United States 420 | United States 421 | 422 | US 423 | 424 | 425 | 426 | 427 | 428 | 18 429 | 430 | 1 431 | CD 432 | 433 | 434 | 9 435 | Bound For the Floor 436 | 225000 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | Bound for the Floor 446 | 223947 447 | 448 | 449 | Local H 450 | 451 | Local H 452 | Local H 453 | 454 | Local H. 455 | 456 | 457 | 458 | 459 | 1996 460 | 461 | 462 | Promo Only: Modern Rock Radio, July 1996 463 | Promotion 464 | 465 | 466 | Various Artists 467 | 468 | Various Artists 469 | Various Artists 470 | add compilations to this artist 471 | 472 | 473 | 474 | 475 | Promo Only: Modern Rock Radio, July 1996 476 | Album 477 | 478 | Compilation 479 | 480 | 481 | 1996 482 | US 483 | 484 | 485 | 1996 486 | 487 | United States 488 | United States 489 | 490 | US 491 | 492 | 493 | 494 | 495 | 496 | 19 497 | 498 | 1 499 | CD 500 | 501 | 502 | 15 503 | Bound for the Floor 504 | 223947 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | 513 | Bound For The Floor 514 | 213000 515 | 516 | 517 | Local H 518 | 519 | Local H 520 | Local H 521 | 522 | Local H. 523 | 524 | 525 | 526 | 527 | 1999-06-26 528 | 529 | 530 | Live at The Metro / Smart Bar 531 | Official 532 | 533 | 534 | Local H 535 | 536 | Local H 537 | Local H 538 | 539 | 540 | 541 | 542 | Live at The Metro / Smart Bar 543 | Album 544 | 545 | Live 546 | 547 | 548 | 1999-06-26 549 | US 550 | 551 | 552 | 1999-06-26 553 | 554 | United States 555 | United States 556 | 557 | US 558 | 559 | 560 | 561 | 562 | 563 | 19 564 | 565 | 1 566 | CD 567 | 568 | 569 | 3 570 | Bound For The Floor 571 | 213000 572 | 573 | 574 | 575 | 576 | 577 | 578 | 579 | 580 | Bound For The Floor 581 | 381000 582 | 583 | 584 | Local H 585 | 586 | Local H 587 | Local H 588 | 589 | Local H. 590 | 591 | 592 | 593 | 594 | 2006-02-11 595 | 596 | 597 | Live at Durty Nellies 598 | Official 599 | 600 | 601 | Local H 602 | 603 | Local H 604 | Local H 605 | 606 | 607 | 608 | 609 | Live at Durty Nellies 610 | Album 611 | 612 | Live 613 | 614 | 615 | 2006-02-11 616 | US 617 | 618 | 619 | 2006-02-11 620 | 621 | United States 622 | United States 623 | 624 | US 625 | 626 | 627 | 628 | 629 | 630 | 17 631 | 632 | 2 633 | CD 634 | 635 | 636 | 2 637 | Bound For The Floor 638 | 381000 639 | 640 | 641 | 642 | 643 | 644 | 645 | 646 | 647 | 648 | -------------------------------------------------------------------------------- /spec/fixtures/artist/release_groups_69b39eab-6577-46a4-a9f5-817839092033.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Kasabian 6 | 2004-07-23 7 | Album 8 | 9 | 10 | https://www.allmusic.com/album/mw0000261419 11 | forward 12 | 13 | 14 | https://www.discogs.com/master/89974 15 | forward 16 | 17 | 18 | https://genius.com/albums/Kasabian/Kasabian 19 | forward 20 | 21 | 22 | https://rateyourmusic.com/release/album/kasabian/kasabian_f2/ 23 | forward 24 | 25 | 26 | https://www.bbc.co.uk/music/reviews/64w6 27 | forward 28 | 29 | 30 | https://www.wikidata.org/wiki/Q1535408 31 | forward 32 | 33 | 34 | 35 | 36 | Empire 37 | 2006-08-25 38 | Album 39 | 40 | 41 | https://www.allmusic.com/album/mw0000445384 42 | forward 43 | 44 | 45 | https://www.discogs.com/master/89980 46 | forward 47 | 48 | 49 | https://www.bbc.co.uk/music/reviews/389x 50 | forward 51 | 52 | 53 | https://www.wikidata.org/wiki/Q748870 54 | forward 55 | 56 | 57 | 58 | 59 | West Ryder Pauper Lunatic Asylum 60 | 2009-04-06 61 | Album 62 | 63 | 64 | https://www.allmusic.com/album/mw0000818188 65 | forward 66 | 67 | 68 | https://www.discogs.com/master/125155 69 | forward 70 | 71 | 72 | https://rateyourmusic.com/release/album/kasabian/west_ryder_pauper_lunatic_asylum/ 73 | forward 74 | 75 | 76 | https://www.bbc.co.uk/music/reviews/bn3n 77 | forward 78 | 79 | 80 | https://www.wikidata.org/wiki/Q1932087 81 | forward 82 | 83 | 84 | 85 | 86 | Velociraptor! 87 | 2011-09-16 88 | Album 89 | 90 | 91 | https://www.allmusic.com/album/mw0002169520 92 | forward 93 | 94 | 95 | https://www.discogs.com/master/368845 96 | forward 97 | 98 | 99 | https://www.bbc.co.uk/music/reviews/vcm5 100 | forward 101 | 102 | 103 | https://www.wikidata.org/wiki/Q1951148 104 | forward 105 | 106 | 107 | 108 | 109 | 48:13 110 | 2014-06-09 111 | Album 112 | 113 | 114 | https://www.allmusic.com/album/mw0002668450 115 | forward 116 | 117 | 118 | https://www.discogs.com/master/696552 119 | forward 120 | 121 | 122 | https://www.wikidata.org/wiki/Q16735842 123 | forward 124 | 125 | 126 | 127 | 128 | For Crying Out Loud 129 | 2017-05-05 130 | Album 131 | 132 | 133 | https://www.discogs.com/master/1174013 134 | forward 135 | 136 | 137 | https://www.wikidata.org/wiki/Q28951744 138 | forward 139 | 140 | 141 | 142 | 143 | The Alchemist’s Euphoria 144 | 2022-08-02 145 | Album 146 | 147 | 148 | https://www.discogs.com/master/2743487 149 | forward 150 | 151 | 152 | https://www.offiziellecharts.de/album-details-497860 153 | forward 154 | 155 | 156 | https://www.plattentests.de/rezi.php?show=18727 157 | forward 158 | 159 | 160 | https://www.wikidata.org/wiki/Q113456602 161 | forward 162 | 163 | 164 | 165 | 166 | Happenings 167 | 2024-07-05 168 | Album 169 | 170 | 171 | Empire / West Ryder Pauper Lunatic Asylum 172 | 2011 173 | Album 174 | 175 | Compilation 176 | 177 | 178 | 179 | Live at Brixton Academy 180 | 2005-07-04 181 | Album 182 | 183 | Live 184 | 185 | 186 | 187 | https://www.wikidata.org/wiki/Q1949413 188 | forward 189 | 190 | 191 | https://en.wikipedia.org/wiki/Live_from_Brixton_Academy 192 | forward 193 | 194 | 195 | 196 | 197 | iTunes Festival: London 2007 198 | 2007-08-03 199 | Album 200 | 201 | Live 202 | 203 | 204 | 205 | https://www.wikidata.org/wiki/Q3789916 206 | forward 207 | 208 | 209 | 210 | 211 | Live in London 212 | 2011 213 | Album 214 | 215 | Live 216 | 217 | 218 | 219 | Reason Is Treason 220 | 2004-02-23 221 | Single 222 | 223 | 224 | https://www.discogs.com/master/125172 225 | forward 226 | 227 | 228 | 229 | 230 | Club Foot 231 | 2004-05-10 232 | Single 233 | 234 | 235 | https://www.discogs.com/master/125150 236 | forward 237 | 238 | 239 | https://www.wikidata.org/wiki/Q2327037 240 | forward 241 | 242 | 243 | https://en.wikipedia.org/wiki/Club_Foot_(song) 244 | forward 245 | 246 | 247 | 248 | 249 | L.S.F. 250 | 2004-08-17 251 | Single 252 | 253 | 254 | https://www.discogs.com/master/125163 255 | forward 256 | 257 | 258 | https://www.wikidata.org/wiki/Q607352 259 | forward 260 | 261 | 262 | https://en.wikipedia.org/wiki/L.S.F._(song) 263 | forward 264 | 265 | 266 | 267 | 268 | Processed Beats 269 | 2004-10-13 270 | Single 271 | 272 | 273 | https://www.wikidata.org/wiki/Q1463837 274 | forward 275 | 276 | 277 | 278 | 279 | Cutt Off 280 | 2005-01-03 281 | Single 282 | 283 | 284 | https://www.wikidata.org/wiki/Q3699798 285 | forward 286 | 287 | 288 | https://en.wikipedia.org/wiki/Cutt_Off 289 | forward 290 | 291 | 292 | 293 | 294 | Empire 295 | 2006-07-24 296 | Single 297 | 298 | 299 | https://www.discogs.com/master/125174 300 | forward 301 | 302 | 303 | https://www.wikidata.org/wiki/Q1463830 304 | forward 305 | 306 | 307 | https://en.wikipedia.org/wiki/Empire_(Kasabian_song) 308 | forward 309 | 310 | 311 | 312 | 313 | Shoot the Runner 314 | 2006-11-06 315 | Single 316 | 317 | 318 | https://www.discogs.com/master/125179 319 | forward 320 | 321 | 322 | https://www.wikidata.org/wiki/Q1463795 323 | forward 324 | 325 | 326 | 327 | 328 | Me Plus One 329 | 2007-01-29 330 | Single 331 | 332 | 333 | https://www.discogs.com/master/125175 334 | forward 335 | 336 | 337 | https://www.wikidata.org/wiki/Q1120372 338 | forward 339 | 340 | 341 | https://en.wikipedia.org/wiki/Me_Plus_One 342 | forward 343 | 344 | 345 | 346 | 347 | Fire 348 | 2009-05-28 349 | Single 350 | 351 | 352 | https://www.wikidata.org/wiki/Q783320 353 | forward 354 | 355 | 356 | https://en.wikipedia.org/wiki/Fire_(Kasabian_song) 357 | forward 358 | 359 | 360 | 361 | 362 | Where Did All the Love Go? 363 | 2009-08-10 364 | Single 365 | 366 | 367 | https://www.wikidata.org/wiki/Q4019433 368 | forward 369 | 370 | 371 | https://en.wikipedia.org/wiki/Where_Did_All_the_Love_Go%3F 372 | forward 373 | 374 | 375 | 376 | 377 | Underdog 378 | 2009-10-22 379 | Single 380 | 381 | 382 | https://www.wikidata.org/wiki/Q4004596 383 | forward 384 | 385 | 386 | 387 | 388 | Days Are Forgotten 389 | 2011-08 390 | Single 391 | 392 | 393 | https://www.discogs.com/master/381246 394 | forward 395 | 396 | 397 | https://www.wikidata.org/wiki/Q3703828 398 | forward 399 | 400 | 401 | https://en.wikipedia.org/wiki/Days_Are_Forgotten 402 | forward 403 | 404 | 405 | 406 | 407 | Re‐Wired 408 | 2011-11-21 409 | Single 410 | 411 | 412 | 413 | --------------------------------------------------------------------------------