Search code examples
ruby-on-railsrubyrake

Creating many albums using ActiveRecord


It's a rake task which searches for an artists and if it exists it will store it with artists albums. I've tried to use a gem but for some reason the gem returns something I don't really need. If I search an artist it works fine though.

result = ITunesSearchAPI.lookup(:id => 372976 , :entity => 'album')

will return this:

{"wrapperType"=>"artist", "artistType"=>"Artist", "artistName"=>"ABBA", "artistLinkUrl"=>"https://itunes.apple.com/us/artist/abba/id372976?uo=4", "artistId"=>372976, "amgArtistId"=>3492, "primaryGenreName"=>"Pop", "primaryGenreId"=>14} 

which is not what I need at all. Here's what I should get.

So I decided to code it myself and then I realized that it saves an empty model, everything in my Album is nil. 2 questions:

1) How can I fix it?

2) How can I save ALL albums, not just one?

require 'net/http'

  task :artist,[""] => :environment do |t, args|
    result = ITunesSearchAPI.search(:term => args.to_s, :entity => 'musicArtist')
    if result.empty? then puts "Nothing was found. Try another artist."
      puts result
    elsif result
      uniqueness = Artist.find_by(itunes_id: result[0]["artistId"])
      if uniqueness.nil?
        Artist.create(name: result[0]["artistName"], itunes_id: result[0]["artistId"])
        puts result
      else
        puts "The artist already exists in database"
      end
    end
    if uniqueness.nil?
    album = URI('https://itunes.apple.com/lookup')
    album_params = { :id => result[0]['artistId'], :entity => 'album'}
    album.query = URI.encode_www_form(album_params)
    album_response = Net::HTTP.get_response(album)
    puts album_response.body
    Album.create!(name: album_response.body[0]["collectionName"], artwork_url_100: album_response.body[0]["artworkUrl100"])
  end
end

Schema:

ActiveRecord::Schema.define(version: 20160418120725) do

  create_table "albums", force: true do |t|
    t.string   "name"
    t.string   "artwork_url_100"
    t.datetime "created_at"
    t.datetime "updated_at"
  end

  create_table "artists", force: true do |t|
    t.string   "name"
    t.integer  "itunes_id"
    t.datetime "created_at"
    t.datetime "updated_at"
  end

  add_index "artists", ["itunes_id"], name: "index_artists_on_itunes_id", unique: true

end

Solution

  • Answer to part 1. You probably need to add some model validations for uniqueness and presence. In your artist.rb file:

    class Artist < ActiveRecord::Base
      ...
      validates :itunes_id, presence: true, uniqueness: true
      validates :name, presence: true, uniqueness: true
      ...
    end
    

    That should prevent your model being saved in an invalid state. Each attribute itunes_id and name must both be present (not nil) and unique (you cannot have 2 'ABBA' artist records).

    More on ActiveRecord validation can be found at: http://guides.rubyonrails.org/active_record_validations.html

    Once you've got your validation rules in place then your code to check for existing records and updating them can simplify to:

    artist = Artist.where(itunes_id: result[0]["artistId"]).first_or_initialize
    artist.name = result[0]["artistName"]
    artist.save
    

    Then we get to check for any errors that prevented the record from persisting to the database:

    if artist.errors.any?
      puts "There were errors preventing the artist being saved:"
      artist.errors.full_messages.each do |message|
        puts " - #{message}"
      end
      puts "Result data: #{result}"
      exit
    end
    

    Once we are past this block (we haven't exited) then we know our artist object is a valid and persisted model object.

    Answer to part 2. You need to have a one-to-many (has_many) association between the Artist and Album model. Then you just need to iterate through the results array creating a new Album for each entry.

    Looking at your schema you need to add an integer attribute to the Album model called artist_id. You can create a migration with the command:

    rails g migration AddArtistToAlbums artist:references
    

    The magic command line syntax should generate a correct migration file that should look something like this:

    class AddArtistToAlbums < ActiveRecord::Migration
      def change
        add_reference :albums, :artist, index: true, foreign_key: true
      end
    end
    

    Run a rake db:migrate to update the database schema.

    In your artist.rb model file you can now add the the following:

    class Artist < ActiveRecord::Base
      ...
      has_many :albums
      ...
    end
    

    You can now access albums associated to an artist through it's association attribute albums.

    In your album.rb model file you can now add the the following:

    class Album < ActiveRecord::Base
      ...
      belongs_to :artist
      ...
    end
    

    You can now access artist associated to an album through it's association attribute artist.

    Before you dive straight in to interpreting the response body I would probably check to see if I got the right kind of request first:

    if !album_response.is_a?(Net::HTTPOK)
      puts "There was an error fetching albums."
      exit
    end
    

    Before you can deal with the response you need to parse the JSON. At the top of the file at require 'json' then parse the album_response.body like:

    album_response_json = JSON.parse(album_response.body)
    

    After that I'd also check to make sure the body is populated as expected.

    if !album_response_json.is_a?(Hash)
      puts "Response JSON is not a Hash as expected."
      exit
    end
    

    You can also check that the response hash has the expected results array.

    if !album_response_json["results"].is_a?(Array)
      puts "Response JSON does not contain the expected 'results' array."
      exit
    end
    

    Next, you were accessing a key value from the hash by index album_response.body[0] which would be an integer (23) based on your example JSON. I think you meant to access the first element of the results array.

    What you need to do is iterate over the results creating a new model object for each album. I noticed that in your example JSON response that there is a wrapperType of 'artist' which I presume you want to filter out so the code would look something like this:

    album_response_json["results"].each do |album_hash|
      next if album_hash["wrapperType"] == "artist"
      artist.albums.create!(name: album_hash["collectionName"], artwork_url_100: album_hash["artworkUrl100"])
    end
    

    You should now have the albums stored as expected.

    Note. I skipped past adding validations to the Album model but it would be a good idea.