r0uder r0uder - 7 months ago 19
Ruby Question

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

Answer

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.