From 557e19e5e7097a9672ee720f419d39ad2886d074 Mon Sep 17 00:00:00 2001 From: Seth Call Date: Sun, 9 Aug 2015 13:37:43 -0500 Subject: [PATCH] * allow better onboarding of JamTracks including audio from Tency Music (VRFS-3296, VRFS-3386) --- admin/config/initializers/jam_tracks.rb | 1 - db/manifest | 1 + db/up/jam_track_onboarding_enhancements.sql | 65 ++ ruby/lib/jam_ruby.rb | 3 + .../lib/jam_ruby/import/tency_stem_mapping.rb | 360 ++++++ ruby/lib/jam_ruby/jam_track_importer.rb | 1036 +++++++++++++++-- ruby/lib/jam_ruby/models/genre.rb | 3 +- ruby/lib/jam_ruby/models/genre_jam_track.rb | 8 + ruby/lib/jam_ruby/models/jam_track.rb | 114 +- ruby/lib/jam_ruby/models/jam_track_file.rb | 78 ++ ruby/lib/jam_ruby/models/jam_track_track.rb | 136 ++- ruby/spec/factories.rb | 2 +- ruby/spec/jam_ruby/jam_track_importer_spec.rb | 102 +- ruby/spec/jam_ruby/models/jam_track_spec.rb | 106 +- .../jam_ruby/models/jam_track_track_spec.rb | 1 + .../SessionMediaTracks.js.jsx.coffee | 2 + .../SessionMetronome.js.jsx.coffee | 1 + .../react-components/SessionTrack.css.scss | 2 + .../api_music_sessions_controller.rb | 4 +- .../controllers/api_recordings_controller.rb | 4 +- web/app/controllers/api_users_controller.rb | 2 +- web/app/views/api_jam_tracks/show.rabl | 2 +- .../views/api_jam_tracks/show_for_client.rabl | 6 +- web/lib/tasks/jam_tracks.rake | 58 +- .../api_jam_tracks_controller_spec.rb | 1 + web/spec/factories.rb | 2 +- web/spec/features/jamtrack_shopping_spec.rb | 4 +- 27 files changed, 1914 insertions(+), 190 deletions(-) create mode 100644 db/up/jam_track_onboarding_enhancements.sql create mode 100644 ruby/lib/jam_ruby/import/tency_stem_mapping.rb create mode 100644 ruby/lib/jam_ruby/models/genre_jam_track.rb create mode 100644 ruby/lib/jam_ruby/models/jam_track_file.rb diff --git a/admin/config/initializers/jam_tracks.rb b/admin/config/initializers/jam_tracks.rb index cd02eccee..33efd995c 100644 --- a/admin/config/initializers/jam_tracks.rb +++ b/admin/config/initializers/jam_tracks.rb @@ -16,7 +16,6 @@ class JamRuby::JamTrack end def jmep_json_generate - self.genre_id = nil if self.genre_id == '' self.licensor_id = nil if self.licensor_id == '' self.jmep_json = nil if self.jmep_json == '' self.time_signature = nil if self.time_signature == '' diff --git a/db/manifest b/db/manifest index 691ae0001..81df91ca8 100755 --- a/db/manifest +++ b/db/manifest @@ -298,3 +298,4 @@ musician_search.sql enhance_band_profile.sql alter_band_profile_rate_defaults.sql repair_band_profile.sql +jam_track_onboarding_enhancements.sql diff --git a/db/up/jam_track_onboarding_enhancements.sql b/db/up/jam_track_onboarding_enhancements.sql new file mode 100644 index 000000000..1336ecdf4 --- /dev/null +++ b/db/up/jam_track_onboarding_enhancements.sql @@ -0,0 +1,65 @@ +UPDATE instruments SET id = 'double bass', description = 'Double Bass' WHERE id = 'upright bass'; +INSERT INTO instruments (id, description, popularity) VALUES ('steel guitar', 'Steel Guitar', 1); +INSERT INTO instruments (id, description, popularity) VALUES ('orchestra', 'Orchestra', 1); +INSERT INTO instruments (id, description, popularity) VALUES ('glockenspiel', 'Glockenspiel', 1); +INSERT INTO instruments (id, description, popularity) VALUES ('dobro', 'Dobro', 1); +INSERT INTO instruments (id, description, popularity) VALUES ('harp', 'Harp', 1); +INSERT INTO instruments (id, description, popularity) VALUES ('vocoder', 'Vocoder', 1); +INSERT INTO instruments (id, description, popularity) VALUES ('flugelhorn', 'Flugelhorn', 1); +INSERT INTO instruments (id, description, popularity) VALUES ('timpani', 'Timpani', 1); +INSERT INTO instruments (id, description, popularity) VALUES ('bassoon', 'Bassoon', 1); +INSERT INTO instruments (id, description, popularity) VALUES ('charango', 'Charango', 1); +INSERT INTO instruments (id, description, popularity) VALUES ('theremin', 'Theremin', 1); +INSERT INTO instruments (id, description, popularity) VALUES ('sitar', 'Sitar', 1); +INSERT INTO instruments (id, description, popularity) VALUES ('piccolo', 'Piccolo', 1); +INSERT INTO instruments (id, description, popularity) VALUES ('bagpipes', 'Bagpipes', 1); +ALTER TABLE jam_tracks ADD COLUMN onboarding_exceptions JSON; +ALTER TABLE jam_track_tracks ADD COLUMN original_filename VARCHAR; +ALTER TABLE jam_tracks ADD COLUMN additional_info VARCHAR; +ALTER TABLE jam_tracks ADD COLUMN language VARCHAR NOT NULL DEFAULT 'eng'; +ALTER TABLE jam_tracks ADD COLUMN year INTEGER; +ALTER TABLE jam_tracks ADD COLUMN vendor_id VARCHAR; + +INSERT INTO jam_track_licensors (name, description) VALUES ('Tency Music', 'Tency Music is a music production company specialized in re-recordings.'); + +CREATE TABLE genres_jam_tracks ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + jam_track_id VARCHAR(64) NOT NULL REFERENCES jam_tracks(id) ON DELETE CASCADE, + genre_id VARCHAR(64) NOT NULL REFERENCES genres(id) ON DELETE CASCADE, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +INSERT INTO genres_jam_tracks (jam_track_id, genre_id) ((SELECT jam_tracks.id, jam_tracks.genre_id FROM jam_tracks)); +ALTER TABLE jam_tracks DROP COLUMN genre_id; + +-- holds precount, click.wav, click.txt +CREATE TABLE jam_track_files ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + jam_track_id VARCHAR(64) REFERENCES jam_tracks(id) ON DELETE CASCADE, + file_type VARCHAR NOT NULL, + original_filename VARCHAR NOT NULL, + precount_num INTEGER, + url VARCHAR, + md5 VARCHAR, + length bigint, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +INSERT INTO genres (id, description) VALUES ('soft rock', 'Soft Rock'); +INSERT INTO genres (id, description) VALUES ('rap', 'Rap'); +INSERT INTO genres (id, description) VALUES ('tv & movie soundtrack', 'TV & Movie Soundtrack'); +INSERT INTO genres (id, description) VALUES ('holiday', 'Holiday'); +INSERT INTO genres (id, description) VALUES ('kids', 'Kids'); +INSERT INTO genres (id, description) VALUES ('disco', 'Disco'); +INSERT INTO genres (id, description) VALUES ('soul', 'Soul'); +INSERT INTO genres (id, description) VALUES ('hard rock', 'Hard Rock'); +INSERT INTO genres (id, description) VALUES ('funk', 'Funk'); +INSERT INTO genres (id, description) VALUES ('dance', 'Dance'); +INSERT INTO genres (id, description) VALUES ('creole', 'Creole'); +INSERT INTO genres (id, description) VALUES ('traditional', 'Traditional'); +INSERT INTO genres (id, description) VALUES ('oldies', 'Oldies'); +INSERT INTO genres (id, description) VALUES ('world', 'World'); +INSERT INTO genres (id, description) VALUES ('musical', 'Musical'); +INSERT INTO genres (id, description) VALUES ('celtic', 'Celtic'); diff --git a/ruby/lib/jam_ruby.rb b/ruby/lib/jam_ruby.rb index 82484997e..6b2d70a20 100755 --- a/ruby/lib/jam_ruby.rb +++ b/ruby/lib/jam_ruby.rb @@ -206,6 +206,8 @@ require "jam_ruby/models/jam_track" require "jam_ruby/models/jam_track_track" require "jam_ruby/models/jam_track_right" require "jam_ruby/models/jam_track_tap_in" +require "jam_ruby/models/jam_track_file" +require "jam_ruby/models/genre_jam_track" require "jam_ruby/app/mailers/async_mailer" require "jam_ruby/app/mailers/batch_mailer" require "jam_ruby/app/mailers/progress_mailer" @@ -238,6 +240,7 @@ require "jam_ruby/models/performance_sample" require "jam_ruby/models/online_presence" require "jam_ruby/models/json_store" require "jam_ruby/models/musician_search" +require "jam_ruby/import/tency_stem_mapping" include Jampb diff --git a/ruby/lib/jam_ruby/import/tency_stem_mapping.rb b/ruby/lib/jam_ruby/import/tency_stem_mapping.rb new file mode 100644 index 000000000..769496562 --- /dev/null +++ b/ruby/lib/jam_ruby/import/tency_stem_mapping.rb @@ -0,0 +1,360 @@ +module JamRuby + + # this is probably a one-off class used to map Tency-named stems into JamKazam-named stems + class TencyStemMapping + + @@log = Logging.logger[TencyStemMapping] + + def s3_manager + @s3_manager ||= S3Manager.new('jamkazam-tency', APP_CONFIG.aws_access_key_id, APP_CONFIG.aws_secret_access_key) + end + + def initialize + @originals_folder = "/Volumes/sethcall/Dropbox/seth@jamkazam.com/JamTracks - Tency Music - Original Folder for Normalization Map" + @mapping_folder = "/Volumes/sethcall/Dropbox/seth@jamkazam.com/JamTracks - Tency Music" + @original_songs = {} + @mapping_songs = {} + @mappings = {} + end + + def create_map + tency_originals + tency_maps + + dump + end + + def create_mapping_map + tency_maps + + dump_map + end + + def hydrate + @original_songs = YAML.load_file('original_songs.yml') + @mapping_songs = YAML.load_file('mapping_songs.yml') + end + + def parse_sanitized_filename(filename) + instrument = nil + part = nil + + basename = File.basename(filename) + stem = basename.index('Stem') + + if stem + stripped = basename[(stem + 'Stem'.length)..-5] # takes of 'stem' and '.wav' + stripped.strip! + dash = stripped.index('-') + + if dash == 0 + stripped = stripped[1..-1].strip! + # now we should have something like "Vocal - Lead" (instrument - part) + instrument, part = stripped.split('-') + instrument.strip! if instrument + part.strip! if part + else + "no or misplaced dash for #{filename}" + end + + else + raise "no stem for #{filename}" + end + + [instrument, part] + end + + # For all the tracks that I have labeled manually as + # Instrument = Upright Bass and Part = Upright Bass, + # can you please change both the Instrument and Part to Double Bass instead? + # + def check_mappings + missing_instrument = 0 + missing_part = 0 + part_names = [] + + hydrate + @mapping_songs.each do |cache_id, data| + mapped_filename = data[:filename] + @@log.debug("parsing #{mapped_filename}") + instrument, part = parse_sanitized_filename(mapped_filename) + @@log.debug("parsed #{instrument} (#{part})") + missing_instrument = missing_instrument + 1 unless instrument + missing_part = missing_part + 1 unless part + part_names << mapped_filename unless part + end + + @@log.info("SUMMARY") + @@log.info("-------") + @@log.info("missing instruments:#{missing_instrument} missing parts: #{missing_part}") + @@log.info("files with no parts: #{part_names}") + + # files with no parts: + # ["Huey Lewis And The News - Heart And Soul - 31957/Heart And Soul Stem - Synth 2.wav", + # "ZZ Top - Tush - 20852/Tush Stem - Clicktrack.wav", + # "Crosby Stills And Nash - Teach Your Children - 15440/Teach Your Children Stem - Bass Guitar.wav", + # /Brad Paisley - She's Everything - 19886/She's Everything Stem - Clicktrack.wav", + # "Toby Keith - Beer For My Horses - 7221/Beer For My Horses Stem - Lap Steel.wav", + # Toby Keith - Beer For My Horses - 7221/Beer For My Horses Stem - Acoustic Guitar.wav" + + end + + def track_mapping(basename, instr_part) + instrument = instr_part[:instrument] + part = instr_part[:part] + + basename.downcase! + + info = @mappings[basename] + + unless info + info = {matches:[]} + @mappings[basename] = info + end + + info[:matches] << instr_part + end + + def correlate + mapped = 0 + unmapped = 0 + unmapped_details = [] + no_instrument = [] + common_unknown_instruments = {} + + hydrate + @mapping_songs.each do |cache_id, data| + # go through each track hand-mapped, and find it's matching song if any. + + mapped_filename = data[:filename] + found_original = @original_songs[cache_id] + if found_original + # mapping made + + original_filename = found_original[:filename] + original_basename = File.basename(original_filename).downcase + + mapped = mapped + 1 + + instrument, part = parse_sanitized_filename(mapped_filename) + instr_part = JamTrackImporter.determine_instrument(instrument, part) + + instr_part[:instrument] + + if instr_part[:instrument] + + # track the mapping of this one + track_mapping(original_basename, instr_part) + + else + @@log.error("unable to determine instrument for #{File.basename(mapped_filename)}") + no_instrument << ({filename: File.basename(mapped_filename), instrument: instrument, part: part}) + common_unknown_instruments["#{instrument}-(#{part})"] = 1 + end + + else + unmapped = unmapped + 1 + unmapped_details << {filename: mapped_filename} + end + end + + puts("SUMMARY") + puts("-------") + puts("MAPPED:#{mapped} UNMAPPED:#{unmapped}") + unmapped_details.each do |unmapped_detail| + puts "UNMAPPED FILE: #{File.basename(unmapped_detail[:filename])}" + end + puts("UNKNOWN INSTRUMENT: #{no_instrument.length}") + no_instrument.each do |item| + puts("UNKNOWN INSTRUMENT: #{item[:filename]}") + end + common_unknown_instruments.each do |key, value| + puts("#{key}") + end + @mappings.each do |basename, mapping| + matches = mapping[:matches] + counts = matches.each_with_object(Hash.new(0)) { |word,counts| counts[word] += 1 } + ordered_matches = counts.sort_by {|k, v| -v} + output = "" + ordered_matches.each do |match| + detail = match[0] + count = match[1] + output << "#{detail[:instrument]}(#{detail[:part]})/#{count}, " + end + + puts "map detail: #{basename}: #{output}" + + mapping[:ordered] = ordered_matches + mapping[:detail] = output + end + CSV.open("mapping.csv", "wb") do |csv| + @mappings.each do |basename, mapping| + item = mapping[:ordered] + + trust_worthy = item.length == 1 + unless trust_worthy + # if the 1st item is at least 4 'counts' more than the next item, we can consider it trust_worthy + if item[0][1] - 4 > item[1][1] + trust_worthy = true + end + end + csv << [ basename, item[0][0][:instrument], item[0][0][:part], item[0][1], trust_worthy ] + end + end + CSV.open("determinate-single-matches.csv", "wb") do |csv| + @mappings.each do |basename, mapping| + if mapping[:ordered].length == 1 && mapping[:ordered][0][1] == 1 + item = mapping[:ordered] + csv << [ basename, item[0][0][:instrument], item[0][0][:part], item[0][1] ] + end + end + end + CSV.open("determinate-multi-matches.csv", "wb") do |csv| + @mappings.each do |basename, mapping| + if mapping[:ordered].length == 1 && mapping[:ordered][0][1] > 1 + item = mapping[:ordered] + csv << [ basename, item[0][0][:instrument], item[0][0][:part], item[0][1] ] + end + end + end + CSV.open("ambiguous-matches.csv", "wb") do |csv| + @mappings.each do |basename, mapping| + if mapping[:ordered].length > 1 + csv << [ basename, mapping[:detail] ] + end + end + end + end + + def dump + File.open('original_songs.yml', 'w') {|f| f.write(YAML.dump(@original_songs)) } + File.open('mapping_songs.yml', 'w') {|f| f.write(YAML.dump(@mapping_songs)) } + end + def dump_map + File.open('mapping_songs.yml', 'w') {|f| f.write(YAML.dump(@mapping_songs)) } + end + + def md5(filepath) + Digest::MD5.file(filepath).hexdigest + end + + def tency_original_check + songs = Pathname.new(@originals_folder).children.select { |c| c.directory? } + songs.each do |song| + dirs = Pathname.new(song).children.select {|c| c.directory? } + + @@log.debug "SONG #{song}" + dirs.each do |dir| + @@log.debug "#{dir.basename.to_s}" + end + @@log.debug "" + end + end + + def tency_originals + songs = Pathname.new(@originals_folder).children.select { |c| c.directory? } + songs.each do |filename| + id = parse_id(filename.basename.to_s ) + files = Pathname.new(filename).children.select {|c| c.file? } + + # also look into any 1st level folders we might find + + dirs = Pathname.new(filename).children.select {|c| c.directory? } + dirs.each do |dir| + more_tracks = Pathname.new(dir).children.select {|c| c.file? } + files = files + more_tracks + end + + files.each do |file| + @@log.debug("processing original track #{file.to_s}") + md5 = md5(file.to_s) + song = {md5:md5, filename:file.to_s, id:id} + @original_songs[cache_id(id, md5)] = song + end + end + + end + + def tency_maps + songs = Pathname.new(@mapping_folder).children.select { |c| c.directory? } + songs.each do |song_filename| + id = parse_id_mapped(song_filename.basename.to_s ) + @@log.debug "processing song #{song_filename.to_s}" + + tracks = Pathname.new(song_filename).children.select {|c| c.file? } + tracks.each do |track| + if track.to_s.include? "Stem" + @@log.debug("processing mapped track #{track.to_s}") + md5 = md5(track.to_s) + + song = {md5:md5, filename:track.to_s} + @mapping_songs[cache_id(id, md5)] = song + end + end + end + end + + def cache_id(id, md5) + "#{id}-#{md5}" + end + + def parse_id(filename) + #amy-winehouse_you-know-i-m-no-good-feat-ghostface-killah_11767 + + index = filename.rindex('_') + if index + id = filename[(index + 1)..-1] + + if id.end_with?('/') + id = id[0...-1] + end + + id = id.to_i + + if id == 0 + raise "no valid ID in filename: #{filename}" + end + else + raise "no _ in filename: #{filename}" + end + id + end + + def parse_id_mapped(filename) + #Flyleaf - I'm So Sick - 15771 + + index = filename.rindex('-') + if index + id = filename[(index + 1)..-1] + + if id.end_with?('/') + id = id[0...-1] + end + + id.strip! + + id = id.to_i + + if id == 0 + raise "no valid ID in filename: #{filename}" + end + else + raise "no - in filename: #{filename}" + end + id + end + + + + def tency_originals2 + s3_manager.list_directories('mapper').each do |song_folder| + @@log.debug("searching through tency directory. song folder:'#{song_folder}'") + + id = parse_id(song_folder) + @@log.debug("ID #{id}") + + top_folder = s3_manager.list_directories(song_folder) + end + end + end +end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/jam_track_importer.rb b/ruby/lib/jam_ruby/jam_track_importer.rb index 70ef72e6a..502f88f0c 100644 --- a/ruby/lib/jam_ruby/jam_track_importer.rb +++ b/ruby/lib/jam_ruby/jam_track_importer.rb @@ -14,6 +14,7 @@ module JamRuby attr_accessor :name attr_accessor :reason attr_accessor :detail + attr_accessor :storage_format def jamkazam_s3_manager @s3_manager ||= S3Manager.new(APP_CONFIG.aws_bucket, APP_CONFIG.aws_access_key_id, APP_CONFIG.aws_secret_access_key) @@ -23,11 +24,141 @@ module JamRuby @public_s3_manager ||= S3Manager.new(APP_CONFIG.aws_bucket_public, APP_CONFIG.aws_access_key_id, APP_CONFIG.aws_secret_access_key) end + def initialize(storage_format = 'default') + @storage_format = storage_format + end + def finish(reason, detail) self.reason = reason self.detail = detail end + + # this method was created due to Tency-sourced data having no master track + # it goes through all audio tracks, and creates a master mix from it. (mix + normalize) + def create_master(metadata, metalocation) + + parsed_metalocation = parse_metalocation(metalocation) + + if parsed_metalocation.nil? + finish("invalid_metalocation", metalocation) + return + end + + original_artist = parsed_metalocation[1] + meta_name = parsed_metalocation[2] + + self.name = metadata[:name] || meta_name + + + audio_path = metalocation[0...-"/meta.yml".length] + + all_files = fetch_important_files(audio_path) + + audio_files = [] + master_found = false + all_files.each do |file| + + parsed_wav = parse_file(file) + if parsed_wav[:master] + master_found = true + elsif parsed_wav[:type] == :track + + audio_files << file + end + end + + if master_found + @@log.debug("master exists... skipping #{self.name} ") + finish('success', nil) + return + else + + tracks = [] + + #tmp_dir = Dir.mktmpdir + #tmp_dir = "/var/folders/05/1jpzfcln1hq9p666whnd7chr0000gn/T/d20150809-9945-1ykr85u" + Dir.mktmpdir do |tmp_dir| + @@log.debug("downloading all audio files in #{tmp_dir}") + audio_files.each do |s3_track| + track = File.join(tmp_dir, File.basename(s3_track)) + tracks << track + JamTrackImporter.song_storage_manager.download(s3_track, track) + end + + # first have to check if all are the same sample rate. If not, we have to make it so + + first_sample_rate = nil + normalize_needed = false + tracks.each do |track| + sample_rate = `soxi -r "#{track}"`.strip + + puts "SAMPLE RATE #{sample_rate}" + if first_sample_rate.nil? + first_sample_rate = sample_rate + else + if first_sample_rate != sample_rate + # we need to normalize all of them + normalize_needed = true + break + end + end + end + + normalized_tracks = [] + if normalize_needed + tracks.each do |track| + normalized_track = File.join(tmp_dir, 'normalized-' + File.basename(track)) + output = `sox "#{track}" "#{normalized_track}" rate #{first_sample_rate}` + @@log.debug("resampling #{normalized_track}; output: #{output}") + normalized_tracks << normalized_track + end + tracks = normalized_tracks + end + + + + temp_file = File.join(tmp_dir, "temp.wav") + output_filename = JamTrackImporter.remove_s3_special_chars("#{self.name} Master Mix.wav") + output_file = File.join(tmp_dir, output_filename) + command = "sox -m " + tracks.each do |track| + command << " \"#{track}\"" + end + command << " \"#{temp_file}\"" + + @@log.debug("mixing with cmd: " + command) + sox_output = `#{command}` + result_code = $?.to_i + + if result_code != 0 + @@log.error("unable to generate master mix") + finish("sox_master_mix_failure", sox_output) + else + + # now normalize the audio + + command = "sox --norm \"#{temp_file}\" \"#{output_file}\"" + @@log.debug("normalizing with cmd: " + command) + sox_output = `#{command}` + result_code = $?.to_i + if result_code != 0 + @@log.error("unable to normalize master mix") + finish("sox_master_mix_failure", sox_output) + else + + # now we need to upload the output back up + s3_target = audio_path + '/' + output_filename + @@log.debug("uploading #{output_file} to #{s3_target}") + JamTrackImporter.song_storage_manager.upload(s3_target, output_file ) + finish('success', nil) + end + + end + end + end + end + def dry_run(metadata, metalocation) metadata ||= {} @@ -38,35 +169,92 @@ module JamRuby original_artist = parsed_metalocation[1] name = parsed_metalocation[2] + JamTrackImporter.summaries[:unique_artists] << original_artist + success = dry_run_metadata(metadata, original_artist, name) return unless success - dry_run_audio(metadata, "audio/#{original_artist}/#{name}") + audio_path = metalocation[0...-"/meta.yml".length] + + + dry_run_audio(metadata, audio_path) finish("success", nil) end + def is_tency_storage? + assert_storage_set + @storage_format == 'Tency' + end + + def assert_storage_set + raise "no storage_format set" if @storage_format.nil? + end + + def parse_metalocation(metalocation) + # metalocation = mapped/4 Non Blondes - What's Up - 6475/meta.yml + if is_tency_storage? - bits = metalocation.split('/') + suffix = '/meta.yml' - if bits.length != 4 - finish("invalid_metalocation", "metalocation not valid #{metalocation}") - return nil + unless metalocation.end_with? suffix + finish("invalid_metalocation", "metalocation not valid #{metalocation}") + return nil + end + + metalocation = metalocation[0...-suffix.length] + + first_path = metalocation.index('/') + if first_path.nil? + finish("invalid_metalocation", "metalocation not valid #{metalocation}") + return nil + end + metalocation = metalocation[(first_path + 1)..-1] + + bits = ['audio'] + # example: Sister Hazel - All For You - 10385 + first_dash = metalocation.index(' - ') + if first_dash + artist = metalocation[0...(first_dash)].strip + bits << artist + else + finish("invalid_metalocation", "metalocation not valid #{metalocation}") + return nil + end + + last_dash = metalocation.rindex('-') + if last_dash + song = metalocation[(first_dash+3)...last_dash].strip + bits << song + else + finish("invalid_metalocation", "metalocation not valid #{metalocation}") + return nil + end + + bits << 'meta.yml' + bits + else + bits = metalocation.split('/') + + if bits.length != 4 + finish("invalid_metalocation", "metalocation not valid #{metalocation}") + return nil + end + + if bits[0] != "audio" + finish("invalid_metalocation", "first bit is not 'audio' #{metalocation}") + return nil + end + + if bits[3] != 'meta.yml' + finish('invalid_metalocation', "last bit is not 'meta.yml' #{metalocation}") + return nil + end + + bits end - - if bits[0] != "audio" - finish("invalid_metalocation", "first bit is not 'audio' #{metalocation}") - return nil - end - - if bits[3] != 'meta.yml' - finish('invalid_metalocation', "last bit is not 'meta.yml' #{metalocation}") - return nil - end - - bits end # if you change this, it will (at least without some work )break development usage of jamtracks @@ -91,6 +279,83 @@ module JamRuby true end + def determine_genres(metadata) + + genres = [] + if metadata[:genres] + metadata[:genres].each do |genre| + if genre == 'hard/metal' + genres << Genre.find('hard rock') + genres << Genre.find('metal') + elsif genre == 'christmas' + genres << Genre.find('holiday') + elsif genre == 'alternative' + genres << Genre.find('alternative rock') + elsif genre == '80s' + # swallow + elsif genre == 'love' + # swallow + elsif genre == 'christian' || genre == 'gospel' + genres << Genre.find('religious') + elsif genre == 'punk/grunge' + genres << Genre.find('punk') + elsif genre == 'electro' + genres << Genre.find('electronic') + elsif genre == 'teen pop' + genres << Genre.find('pop') + elsif genre == "rock 'n roll" + genres << Genre.find('rock') + elsif genre == 'zouk/creole' + genres << Genre.find('creole') + elsif genre == 'world/folk' + genres << Genre.find('world') + genres << Genre.find('folk') + elsif genre == 'french pop' + # swallow + elsif genre == 'schlager' + #swallow + elsif genre == 'humour' + # swallow + elsif genre == 'oriental' + genres << genre.find('asian') + else + found = Genre.find_by_id(genre) + genres << found if found + end + + end + end + + genres + end + + def determine_language(metadata) + + found = ISO_639.find_by_code('eng') + + language = metadata[:language] + + if language + language.downcase! + + if language == 'instrumental' + return 'instrumental' + end + + if language.include? 'spanish' + found = ISO_639.find_by_code('spa') + elsif language.include? 'german' + found = ISO_639.find_by_code('ger') + elsif language.include? 'portuguese' + found = ISO_639.find_by_code('por') + elsif language.include? 'english' + found = ISO_639.find_by_code('eng') + end + end + + found[0] # 3 letter code + end + def synchronize_metadata(jam_track, metadata, metalocation, original_artist, name, options) metadata ||= {} @@ -104,14 +369,24 @@ module JamRuby jam_track.metalocation = metalocation jam_track.original_artist = metadata["original_artist"] || original_artist jam_track.name = self.name - jam_track.genre_id = 'rock' + jam_track.additional_info = metadata[:additional_info] + jam_track.year = metadata[:year] + jam_track.genres = determine_genres(metadata) + jam_track.language = determine_language(metadata) jam_track.plan_code = metadata["plan_code"] || gen_plan_code(jam_track.original_artist, jam_track.name) jam_track.price = 1.99 - jam_track.reproduction_royalty_amount = 0 - jam_track.licensor_royalty_amount = 0 + jam_track.reproduction_royalty_amount = nil + jam_track.reproduction_royalty = true + jam_track.public_performance_royalty = true + jam_track.licensor_royalty_amount = 0.4 jam_track.sales_region = 'Worldwide' jam_track.recording_type = 'Cover' jam_track.description = "This is a JamTrack audio file for use exclusively with the JamKazam service. This JamTrack is a high quality cover of the #{jam_track.original_artist} song \"#{jam_track.name}\"." + + if is_tency_storage? + jam_track.vendor_id = metadata[:id] + jam_track.licensor = JamTrackLicensor.find_by_name('Tency Music') + end else if !options[:resync_audio] #@@log.debug("#{self.name} skipped because it already exists in database") @@ -167,12 +442,18 @@ module JamRuby else instrument = 'electric guitar' end - elsif potential_instrument == 'electric gutiar' || potential_instrument == 'electric guitat' + elsif potential_instrument == 'acoustic' + instrument = 'acoustic guitar' + elsif potential_instrument == 'acoutic guitar' + instrument = 'electric guitar' + elsif potential_instrument == 'electric gutiar' || potential_instrument == 'electric guitat' || potential_instrument == 'electric guitary' instrument = 'electric guitar' elsif potential_instrument == 'keys' instrument = 'keyboard' elsif potential_instrument == 'vocal' || potential_instrument == 'vocals' instrument = 'voice' + elsif potential_instrument == 'upright bass' + instrument = 'double bass' elsif potential_instrument == 'bass' instrument = 'bass guitar' elsif potential_instrument == 'drum' @@ -185,8 +466,9 @@ module JamRuby else part = 'Sound FX' end - - + elsif potential_instrument == 'computer scratches' + instrument = 'computer' + part = 'Scratches' elsif potential_instrument == "sax" instrument = 'saxophone' elsif potential_instrument == "vocal back up" @@ -213,18 +495,43 @@ module JamRuby elsif potential_instrument == 'fretless bass' instrument = 'bass guitar' part = 'Fretless' + elsif potential_instrument == 'lap steel' || potential_instrument == 'pedal steel' + instrument = 'steel guitar' elsif potential_instrument == 'clock percussion' instrument = 'computer' part = 'Clock' - elsif potential_instrument == 'horns' + elsif potential_instrument == 'horns' || potential_instrument == 'horn' instrument = 'other' part = 'Horns' - elsif potential_instrument == 'strings' + elsif potential_instrument == 'english horn' instrument = 'other' + part = 'English Horn' + elsif potential_instrument == 'bass clarinet' + instrument = 'other' + part = 'Bass Clarinet' + elsif potential_instrument == 'recorder' + instrument = 'other' + part = 'Recorder' + elsif potential_instrument == 'marimba' + instrument = 'keyboard' + part = 'Marimba' + elsif potential_instrument == 'strings' + instrument = 'orchestra' part = 'Strings' - elsif potential_instrument == 'orchestration' - instrument = 'computer' - part = 'Orchestration' + elsif potential_instrument == 'celesta' + instrument = 'keyboard' + elsif potential_instrument == 'balalaika' + instrument = 'other' + part = 'Balalaika' + elsif potential_instrument == 'tanpura' + instrument = 'other' + part = 'Tanpura' + elsif potential_instrument == 'quena' + instrument = 'other' + part = 'Quena' + elsif potential_instrument == 'bouzouki' + instrument = 'other' + part = 'Bouzouki' elsif potential_instrument == 'claps' || potential_instrument == 'hand claps' instrument = 'computer' part = 'Claps' @@ -246,20 +553,44 @@ module JamRuby end - def parse_wav(file) + def parse_file(file) bits = file.split('/') filename = bits[bits.length - 1] # remove all but just the filename filename_no_ext = filename[0..-5] comparable_filename = filename_no_ext.downcase # remove .wav + type = nil master = false instrument = nil part = nil + precount_num = nil + no_precount_detail = nil + if comparable_filename == "click" || comparable_filename.include?("clicktrack") + if filename.end_with?('.txt') + type = :clicktxt + else + type = :clickwav + end + elsif comparable_filename.include? "precount" + type = :precount + index = comparable_filename.index('precount') + precount = comparable_filename[(index + 'precount'.length)..-1].strip + if precount.start_with?('_') + precount = precount[1..-1] + end + if precount.to_i == 0 + no_precount_detail = comparable_filename + else + precount_num = precount.to_i + end - if comparable_filename.include?("master mix") || comparable_filename.include?("mastered mix") + + elsif comparable_filename.include?("master mix") || comparable_filename.include?("mastered mix") master = true + type = :master else + type = :track stem_location = comparable_filename.index('stem -') unless stem_location stem_location = comparable_filename.index('stems -') @@ -294,72 +625,171 @@ module JamRuby result = determine_instrument(possible_instrument, possible_part) instrument = result[:instrument] part = result[:part] + else + if is_tency_storage? + # we can check to see if we can find mapping info for this filename + mapping = JamTrackImporter.tency_mapping[filename.downcase] + + if mapping && mapping[:trust] + instrument = mapping[:instrument] + part = mapping[:part] + end + + # tency mapping didn't work; let's retry with our own home-grown mapping + if instrument.nil? && !possible_instrument.nil? + result = determine_instrument(possible_instrument, possible_part) + instrument = result[:instrument] + part = result[:part] + end + end end + end - {filename: filename, master: master, instrument: instrument, part: part} + {filename: filename, master: master, instrument: instrument, part: part, type: type, precount_num: precount_num, no_precount_detail: no_precount_detail} end def dry_run_audio(metadata, s3_path) - all_files = fetch_wav_files(s3_path) + all_files = fetch_important_files(s3_path) all_files.each do |file| - if file.end_with?('.wav') - parsed_wav = parse_wav(file) - if parsed_wav[:master] - @@log.debug("#{self.name} master! filename: #{parsed_wav[:filename]}") - else - if !parsed_wav[:instrument] || !parsed_wav[:part] - @@log.warn("#{self.name} track! instrument: #{parsed_wav[:instrument] ? parsed_wav[:instrument] : 'N/A'}, part: #{parsed_wav[:part] ? parsed_wav[:part] : 'N/A'}, filename: #{parsed_wav[:filename]} ") - else - @@log.debug("#{self.name} track! instrument: #{parsed_wav[:instrument] ? parsed_wav[:instrument] : 'N/A'}, part: #{parsed_wav[:part] ? parsed_wav[:part] : 'N/A'}, filename: #{parsed_wav[:filename]} ") + + # ignore click/precount + parsed_wav = parse_file(file) + if parsed_wav[:master] + @@log.debug("#{self.name} master! filename: #{parsed_wav[:filename]}") + elsif parsed_wav[:type] == :track + + JamTrackImporter.summaries[:total_tracks] += 1 + + if parsed_wav[:instrument].nil? + detail = JamTrackImporter.summaries[:no_instrument_detail] + file_detail = detail[parsed_wav[:filename].downcase] + if file_detail.nil? + detail[parsed_wav[:filename].downcase] = 0 end + detail[parsed_wav[:filename].downcase] += 1 + + JamTrackImporter.summaries[:no_instrument] += 1 + end + + JamTrackImporter.summaries[:no_part] += 1 if parsed_wav[:part].nil? + + if !parsed_wav[:instrument] || !parsed_wav[:part] + @@log.warn("#{self.name} track! instrument: #{parsed_wav[:instrument] ? parsed_wav[:instrument] : 'N/A'}, part: #{parsed_wav[:part] ? parsed_wav[:part] : 'N/A'}, filename: #{parsed_wav[:filename]} ") + else + @@log.debug("#{self.name} track! instrument: #{parsed_wav[:instrument] ? parsed_wav[:instrument] : 'N/A'}, part: #{parsed_wav[:part] ? parsed_wav[:part] : 'N/A'}, filename: #{parsed_wav[:filename]} ") + end + elsif parsed_wav[:type] == :clickwav + + elsif parsed_wav[:type] == :clicktxt + + elsif parsed_wav[:type] == :precount + if parsed_wav[:precount_num].nil? + JamTrackImporter.summaries[:no_precount_num] += 1 + JamTrackImporter.summaries[:no_precount_detail] << parsed_wav[:no_precount_detail] + end + + else + JamTrackImporter.summaries[:unknown_filetype] += 1 + end + end + end + + + def set_custom_weight(track) + + slop = 800 + + instrument_weight = nil + # if there are any persisted tracks, do not sort from scratch; just stick new stuff at the end + + if track.persisted? + instrument_weight = track.position + else + if track.instrument_id == 'voice' + + if track.part && track.part.start_with?('Lead') + instrument_weight = 100 + elsif track.part && track.part.start_with?('Backing') + instrument_weight = 110 + else + instrument_weight = 120 + end + + elsif track.instrument_id == 'drums' + + if track.part && track.part == 'Drums' + instrument_weight = 150 + elsif track.part && track.part == 'Percussion' + instrument_weight = 160 + else + instrument_weight = 170 + end + + elsif track.instrument_id == 'bass guitar' && track.part && track.part == 'Bass' + instrument_weight = 180 + + elsif track.instrument_id == 'piano' && track.part && track.part == 'Piano' + instrument_weight = 250 + + elsif track.instrument_id == 'keyboard' + + if track.part && track.part.start_with?('Synth') + instrument_weight = 260 + elsif track.part && track.part.start_with?('Pads') + instrument_weight = 270 + else + instrument_weight = 280 + end + + elsif track.instrument_id == 'acoustic guitar' + if track.part && track.part.start_with?('Lead') + instrument_weight = 300 + elsif track.part && track.part.start_with?('Rhythm') + instrument_weight = 310 + else + instrument_weight = 320 + end + elsif track.instrument_id == 'electric guitar' + if track.part && track.part.start_with?('Lead') + instrument_weight = 400 + elsif track.part && track.part.start_with?('Solo') + instrument_weight = 410 + elsif track.part && track.part.start_with?('Rhythm') + instrument_weight = 420 + else + instrument_weight = 440 end else - @@log.debug("#{self.name} ignoring non-wav file #{file}") + instrument_weight = slop + end + + if track.track_type == 'Master' + instrument_weight = 1000 end end + + instrument_weight end def sort_tracks(tracks) - def set_custom_weight(track) - weight = 5 - # if there are any persisted tracks, do not sort from scratch; just stick new stuff at the end - - if track.persisted? - weight = track.position - else - case track.instrument_id - when 'electric guitar' - weight = 100 - when 'acoustic guitar' - weight = 200 - when 'drums' - weight = 300 - when 'keys' - weight = 400 - when 'computer' - weight = 600 - else - weight = 500 - end - if track.track_type == 'Master' - weight = 1000 - end - end - - - weight - end - sorted_tracks = tracks.sort do |a, b| a_weight = set_custom_weight(a) b_weight = set_custom_weight(b) - a_weight <=> b_weight + if a_weight != b_weight + a_weight <=> b_weight + elsif a.instrument_id != b.instrument_id + a.instrument_id <=> b.instrument_id + else + a_part = a.part + b_part = b.part + a_part <=> b_part + end end # default to 1, but if there are any persisted tracks, this will get manipulated to be +1 the highest persisted track @@ -387,9 +817,10 @@ module JamRuby attempt_to_match_existing_tracks = true # find all wav files in the JamTracks s3 bucket - wav_files = fetch_wav_files(s3_path) + wav_files = fetch_important_files(s3_path) tracks = [] + addt_files = [] wav_files.each do |wav_file| @@ -419,27 +850,51 @@ module JamRuby @@log.debug("no existing track found; creating a new one") track = JamTrackTrack.new + track.original_filename = wav_file track.original_audio_s3_path = wav_file - parsed_wav = parse_wav(wav_file) + file = JamTrackFile.new + file.original_filename = wav_file + file.original_audio_s3_path = wav_file + parsed_wav = parse_file(wav_file) + + unknowns = 0 if parsed_wav[:master] track.track_type = 'Master' - track.part = 'Master' + track.part = 'Master Mix' + track.instrument_id = 'computer' + tracks << track @@log.debug("#{self.name} master! filename: #{parsed_wav[:filename]}") - else + elsif parsed_wav[:type] == :track + if !parsed_wav[:instrument] || !parsed_wav[:part] @@log.warn("#{self.name} track! instrument: #{parsed_wav[:instrument] ? parsed_wav[:instrument] : 'N/A'}, part: #{parsed_wav[:part] ? parsed_wav[:part] : 'N/A'}, filename: #{parsed_wav[:filename]} ") + unknowns += 1 else @@log.debug("#{self.name} track! instrument: #{parsed_wav[:instrument] ? parsed_wav[:instrument] : 'N/A'}, part: #{parsed_wav[:part] ? parsed_wav[:part] : 'N/A'}, filename: #{parsed_wav[:filename]} ") end + track.instrument_id = parsed_wav[:instrument] || 'other' track.track_type = 'Track' - track.part = parsed_wav[:part] || 'Other' + track.part = parsed_wav[:part] || "Other #{unknowns}" + tracks << track + elsif parsed_wav[:type] == :clicktxt + file.file_type = 'ClickTxt' + addt_files << file + elsif parsed_wav[:type] == :clickwav + file.file_type = 'ClickWav' + addt_files << file + elsif parsed_wav[:type] == :precount + file.file_type = 'Precount' + file.precount_num = parsed_wav[:precount_num] + addt_files << file + else + finish("unknown_file_type", "unknown file type #{wave_file}") + return false end - tracks << track end jam_track.jam_track_tracks.each do |jam_track_track| @@ -450,10 +905,18 @@ module JamRuby end end + jam_track.jam_track_files.each do |jam_track_file| + unless addt_files.include?(jam_track_file) + @@log.info("destroying removed JamTrackFile #{jam_track_file.inspect}") + jam_track_file.destroy # should also delete s3 files associated with this jamtrack + end + end + @@log.info("sorting tracks") tracks = sort_tracks(tracks) jam_track.jam_track_tracks = tracks + jam_track.jam_track_files = addt_files saved = jam_track.save @@ -505,7 +968,7 @@ module JamRuby wav_file = File.join(tmp_dir, basename) # bring the original wav file down from S3 to local file system - JamTrackImporter::s3_manager.download(track.original_audio_s3_path, wav_file) + JamTrackImporter::song_storage_manager.download(track.original_audio_s3_path, wav_file) sample_rate = `soxi -r "#{wav_file}"`.strip @@ -553,7 +1016,10 @@ module JamRuby if !preview_succeeded return false end + elsif track.track_type == 'Track' + synchronize_track_preview(track, tmp_dir, ogg_44100) end + end track.save! @@ -582,6 +1048,68 @@ module JamRuby true end + def synchronize_track_preview(track, tmp_dir, ogg_44100) + + out_wav = File.join(tmp_dir, 'stripped.wav') + + burp_gaps = ['0.3', '0.2', '0.1', '0.05'] + + total_time_command = "soxi -D \"#{ogg_44100}\"" + total_time = `#{total_time_command}`.to_f + + result_code = -20 + stripped_time = total_time # default to the case where we just start the preview at the beginning + + burp_gaps.each do |gap| + command_strip_lead_silence = "sox \"#{ogg_44100}\" \"#{out_wav}\" silence 1 #{gap} 1%" + + @@log.debug("stripping silence: " + command_strip_lead_silence) + + output = `#{command_strip_lead_silence}` + + result_code = $?.to_i + + if result_code == 0 + stripped_time_command = "soxi -D \"#{out_wav}\"" + stripped_time_test = `#{stripped_time_command}`.to_f + + if stripped_time_test < 1 # meaning a very short duration + @@log.warn("could not determine the start of non-silencea. assuming beginning") + stripped_time = total_time # default to the case where we just start the preview at the beginning + else + stripped_time = stripped_time_test # accept the measured time of the stripped file and move on by using break + break + end + else + @@log.warn("unable to determine silence for jam_track #{track.original_filename}, #{output}") + stripped_time = total_time # default to the case where we just start the preview at the beginning + end + + end + + preview_start_time = total_time - stripped_time + + # this is in seconds; convert to integer milliseconds + preview_start_time = (preview_start_time * 1000).to_i + + preview_start_time = nil if preview_start_time < 0 + + track.preview_start_time = preview_start_time + + if track.preview_start_time + @@log.debug("determined track start time to be #{track.preview_start_time}") + else + @@log.debug("determined track start time to be #{track.preview_start_time}") + end + + track.process_preview(ogg_44100, tmp_dir) if track.preview_start_time + + if track.preview_generate_error + @@log.warn(track.preview_generate_error) + end + + end + def synchronize_master_preview(track, tmp_dir, ogg_44100, ogg_digest) begin @@ -639,12 +1167,12 @@ module JamRuby end def fetch_all_files(s3_path) - JamTrackImporter::s3_manager.list_files(s3_path) + JamTrackImporter::song_storage_manager.list_files(s3_path) end - def fetch_wav_files(s3_path) + def fetch_important_files(s3_path) files = fetch_all_files(s3_path) - files.select { |file| file.end_with?('.wav') } + files.select { |file| file.end_with?('.wav') || file.end_with?('.txt') } end def synchronize(jam_track, metadata, metalocation, options) @@ -664,7 +1192,9 @@ module JamRuby return unless success - synchronized_audio = synchronize_audio(jam_track, metadata, "audio/#{original_artist}/#{name}", options[:skip_audio_upload]) + audio_path = metalocation[0...-"/meta.yml".length] + + synchronized_audio = synchronize_audio(jam_track, metadata, audio_path, options[:skip_audio_upload]) return unless synchronized_audio @@ -673,6 +1203,9 @@ module JamRuby finish("success", nil) end + # do a last check on any problems with the jamtrack + jam_track.sync_onboarding_exceptions + end def synchronize_recurly(jam_track) @@ -690,16 +1223,205 @@ module JamRuby class << self + attr_accessor :storage_format + attr_accessor :tency_mapping + attr_accessor :tency_metadata + attr_accessor :summaries + + def report_summaries + @@log.debug("SUMMARIES DUMP") + @@log.debug("--------------") + @summaries.each do |k, v| + + if k == :no_instrument_detail + @@log.debug("#{k}: #{v}") + elsif k == :no_precount_detail + v.each do |precount_detail| + @@log.debug("precount: #{precount_detail}") + end + elsif k == :unique_artists + v.each do |artist| + @@log.debug("artist: #{artist}") + end + else + @@log.debug("#{k}: #{v}") + end + end + end + + def song_storage_manager + if is_tency_storage? + tency_s3_manager + else + s3_manager + end + end + + def summaries + @summaries ||= {unknown_filetype: 0, no_instrument: 0, no_part: 0, total_tracks: 0, no_instrument_detail: {}, no_precount_num: 0, no_precount_detail: [], unique_artists: SortedSet.new} + end + + def tency_s3_manager + @tency_s3_manager ||= S3Manager.new('jamkazam-tency', APP_CONFIG.aws_access_key_id, APP_CONFIG.aws_secret_access_key) + end + def s3_manager @s3_manager ||= S3Manager.new(APP_CONFIG.aws_bucket_jamtracks, APP_CONFIG.aws_access_key_id, APP_CONFIG.aws_secret_access_key) end def private_s3_manager - @s3_manager ||= S3Manager.new(APP_CONFIG.aws_bucket, APP_CONFIG.aws_access_key_id, APP_CONFIG.aws_secret_access_key) + @private_s3_manager ||= S3Manager.new(APP_CONFIG.aws_bucket, APP_CONFIG.aws_access_key_id, APP_CONFIG.aws_secret_access_key) end + def extract_tency_song_id(metalocation) + # metalocation = mapped/4 Non Blondes - What's Up - 6475/meta.yml + + first_path = metalocation.index('/') + return nil unless first_path + metalocation = metalocation[(first_path + 1)..-1] + + suffix = '/meta.yml' + metalocation = metalocation[0...-suffix.length] + + last_dash = metalocation.rindex('-') + return nil if last_dash.nil? + + id = metalocation[(last_dash+1)..-1].strip + + return nil if id.to_i == 0 + + id + end + + def is_tency_storage? + assert_storage_set + @storage_format == 'Tency' + end + + def assert_storage_set + raise "no storage_format set" if @storage_format.nil? + end + + def iterate_tency_song_storage(&blk) + count = 0 + song_storage_manager.list_directories('mapped').each do |song| + @@log.debug("searching through song directory '#{song}'") + + metalocation = "#{song}meta.yml" + + metadata = load_metalocation(metalocation) + + blk.call(metadata, metalocation) + + count += 1 + #break if count > 100 + + end + end + + def iterate_default_song_storage(&blk) + song_storage_manager.list_directories('audio').each do |original_artist| + @@log.debug("searching through artist directory '#{original_artist}'") + + songs = song_storage_manager.list_directories(original_artist) + songs.each do |song| + @@log.debug("searching through song directory' #{song}'") + + metalocation = "#{song}meta.yml" + + metadata = load_metalocation(metalocation) + + blk.call(metadata, metalocation) + end + end + end + + def iterate_song_storage(&blk) + if is_tency_storage? + iterate_tency_song_storage do |metadata, metalocation| + blk.call(metadata, metalocation) + end + else + iterate_default_song_storage do |metadata, metalocation| + blk.call(metadata, metalocation) + end + end + end def dry_run + iterate_song_storage do |metadata, metalocation| + jam_track_importer = JamTrackImporter.new(@storage_format) + + jam_track_importer.dry_run(metadata, metalocation) + end + + report_summaries + end + + # figure out which songs are in S3 that do not exist in the 2k spreadsheet (mapping.csv), and which songs are in the 2k spreadsheet that are not in S3 + def tency_delta + in_s3 = {} + in_mapping = {} + + load_tency_mappings + + JamTrackImporter.tency_metadata.each do |song_id, metadata| + in_mapping[song_id] = {artist: metadata[:original_artist], song: metadata[:name]} + end + + iterate_song_storage do |metadata, metalocation| + + importer = JamTrackImporter.new(@storage_format) + song_id = JamTrackImporter.extract_tency_song_id(metalocation) + parsed_metalocation = importer.parse_metalocation(metalocation) + + next if song_id.nil? + next if parsed_metalocation.nil? + + original_artist = parsed_metalocation[1] + meta_name = parsed_metalocation[2] + + in_s3[song_id] = {artist: original_artist, song: meta_name} + end + + in_s3_keys = Set.new(in_s3.keys) + in_mapping_keys = Set.new(in_mapping.keys) + only_in_mapping = in_mapping_keys - in_s3_keys + only_in_s3 = in_s3_keys - in_mapping_keys + + CSV.open("only_in_s3.csv", "wb") do |csv| + only_in_s3.each do |song_id| + csv << [ song_id, in_s3[song_id][:artist], in_s3[song_id][:song] ] + end + end + + CSV.open("only_in_2k_selection.csv", "wb") do |csv| + only_in_mapping.each do |song_id| + csv << [ song_id, in_mapping[song_id][:artist], in_mapping[song_id][:song] ] + end + end + + end + def create_masters + iterate_song_storage do |metadata, metalocation| + next if metadata.nil? + jam_track_importer = JamTrackImporter.new(@storage_format) + + jam_track_importer.create_master(metadata, metalocation) + end + end + + def create_master(path) + metalocation = "#{path}/meta.yml" + + metadata = load_metalocation(metalocation) + + jam_track_importer = JamTrackImporter.new(@storage_format) + + jam_track_importer.create_master(metadata, metalocation) + end + + def dry_run_original s3_manager.list_directories('audio').each do |original_artist| @@log.debug("searching through artist directory '#{original_artist}'") @@ -906,23 +1628,35 @@ module JamRuby end end + def remove_s3_special_chars(filename) + filename.tr('/&@:,$=+?;\^`><{}[]#%~|', '') + end + def onboarding_exceptions + JamTrack.all.each do |jam_track| + jam_track.onboarding_exceptions + end + end def synchronize_all(options) importers = [] - s3_manager.list_directories('audio').each do |original_artist| - @@log.debug("searching through artist directory '#{original_artist}'") + count = 0 + iterate_song_storage do |metadata, metalocation| - songs = s3_manager.list_directories(original_artist) - songs.each do |song| - @@log.debug("searching through song directory' #{song}'") + next if metadata.nil? && is_tency_storage? - metalocation = "#{song}meta.yml" + importer = synchronize_from_meta(metalocation, options) + importers << importer - importer = synchronize_from_meta(metalocation, options) - importers << importer + if importer.reason != 'jam_track_exists' + count+=1 + end + + if count > 100 + break end end + @@log.info("SUMMARY") @@log.info("-------") importers.each do |importer| @@ -956,12 +1690,113 @@ module JamRuby end end + def genre_dump + load_tency_mappings + + genres = {} + @tency_metadata.each do |id, value| + + genre1 = value[:genre1] + genre2 = value[:genre2] + genre3 = value[:genre3] + genre4 = value[:genre4] + genre5 = value[:genre5] + + genres[genre1.downcase.strip] = genre1.downcase.strip if genre1 + genres[genre2.downcase.strip] = genre2.downcase.strip if genre2 + genres[genre3.downcase.strip] = genre3.downcase.strip if genre3 + genres[genre4.downcase.strip] = genre4.downcase.strip if genre4 + genres[genre5.downcase.strip] = genre5.downcase.strip if genre5 + end + + all_genres = Genre.select(:id).all.map(&:id) + + all_genres = Set.new(all_genres) + genres.each do |genre, value| + found = all_genres.include? genre + + puts "#{genre}" unless found + end + end + + def load_tency_mappings + Dir.mktmpdir do |tmp_dir| + mapping_file = File.join(tmp_dir, 'mapping.csv') + metadata_file = File.join(tmp_dir, 'metadata.csv') + + # this is a developer option to skip the download and look in the CWD to grab mapping.csv and metadata.csv + if ENV['TENCY_ALREADY_DOWNLOADED'] == '1' + mapping_file = 'mapping.csv' + metadata_file = 'metadata.csv' + else + tency_s3_manager.download('mapping/mapping.csv', mapping_file) + tency_s3_manager.download('mapping/metadata.csv', metadata_file) + end + + mapping_csv = CSV.read(mapping_file) + metadata_csv = CSV.read(metadata_file, headers: true, return_headers: false) + + @tency_mapping = {} + @tency_metadata = {} + # convert both to hashes + mapping_csv.each do |line| + @tency_mapping[line[0].strip] = {instrument: line[1], part: line[2], count: line[3], trust: line[4]} + end + + metadata_csv.each do |line| + @tency_metadata[line[0].strip] = {id: line[0].strip, original_artist: line[1], name: line[2], additional_info: line[3], year: line[4], language: line[5], isrc: line[10], genre1: line[11], genre2: line[12], genre3: line[13], genre4: line[14], genre5: line[15]} + end + + + @tency_metadata.each do |id, value| + + genres = [] + + genre1 = value[:genre1] + genre2 = value[:genre2] + genre3 = value[:genre3] + genre4 = value[:genre4] + genre5 = value[:genre5] + + genres << genre1.downcase.strip if genre1 + genres << genre2.downcase.strip if genre2 + genres << genre3.downcase.strip if genre3 + genres << genre4.downcase.strip if genre4 + genres << genre5.downcase.strip if genre5 + + value[:genres] = genres + end + + + end + end + def load_metalocation(metalocation) - begin - data = s3_manager.read_all(metalocation) - return YAML.load(data) - rescue AWS::S3::Errors::NoSuchKey - return nil + + if is_tency_storage? + load_tency_mappings if @tency_mapping.nil? + song_id = extract_tency_song_id(metalocation) + + if song_id.nil? + puts "missing_song_id #{metalocation}" + return nil + end + + + tency_data = @tency_metadata[song_id] + + if tency_data.nil? + @@log.warn("missing tency metadata '#{song_id}'") + end + + return tency_data + else + begin + data = s3_manager.read_all(metalocation) + return YAML.load(data) + rescue AWS::S3::Errors::NoSuchKey + return nil + end end end @@ -975,7 +1810,7 @@ module JamRuby end def sync_from_metadata(jam_track, meta, metalocation, options) - jam_track_importer = JamTrackImporter.new + jam_track_importer = JamTrackImporter.new(@storage_format) JamTrack.transaction do #begin @@ -998,6 +1833,9 @@ module JamRuby meta = load_metalocation(metalocation) + if meta.nil? && is_tency_storage? + raise "no tency song matching this metalocation #{metalocation}" + end jam_track_importer = nil if jam_track @@log.debug("jamtrack #{jam_track.name} located by metalocation") diff --git a/ruby/lib/jam_ruby/models/genre.rb b/ruby/lib/jam_ruby/models/genre.rb index 1b0cd9ada..91d80f755 100644 --- a/ruby/lib/jam_ruby/models/genre.rb +++ b/ruby/lib/jam_ruby/models/genre.rb @@ -16,7 +16,8 @@ module JamRuby has_and_belongs_to_many :recordings, :class_name => "JamRuby::Recording", :join_table => "recordings_genres" # jam tracks - has_many :jam_tracks, :class_name => "JamRuby::JamTrack" + has_many :genres_jam_tracks, :class_name => "JamRuby::GenreJamTrack", :foreign_key => "genre_id" + has_many :jam_tracks, :through => :genres_jam_tracks, :class_name => "JamRuby::JamTrack", :source => :genre def to_s description diff --git a/ruby/lib/jam_ruby/models/genre_jam_track.rb b/ruby/lib/jam_ruby/models/genre_jam_track.rb new file mode 100644 index 000000000..aa05e4fd8 --- /dev/null +++ b/ruby/lib/jam_ruby/models/genre_jam_track.rb @@ -0,0 +1,8 @@ +module JamRuby + class GenreJamTrack < ActiveRecord::Base + + self.table_name = 'genres_jam_tracks' + belongs_to :jam_track, class_name: 'JamRuby::JamTrack' + belongs_to :genre, class_name: 'JamRuby::Genre' + end +end diff --git a/ruby/lib/jam_ruby/models/jam_track.rb b/ruby/lib/jam_ruby/models/jam_track.rb index 8ab120c62..3338124be 100644 --- a/ruby/lib/jam_ruby/models/jam_track.rb +++ b/ruby/lib/jam_ruby/models/jam_track.rb @@ -14,7 +14,7 @@ module JamRuby attr_accessor :uploading_preview attr_accessible :name, :description, :bpm, :time_signature, :status, :recording_type, - :original_artist, :songwriter, :publisher, :licensor, :licensor_id, :pro, :genre, :genre_id, :sales_region, :price, + :original_artist, :songwriter, :publisher, :licensor, :licensor_id, :pro, :genres_jam_tracks_attributes, :sales_region, :price, :reproduction_royalty, :public_performance_royalty, :reproduction_royalty_amount, :licensor_royalty_amount, :pro_royalty_amount, :plan_code, :initial_play_silence, :jam_track_tracks_attributes, :jam_track_tap_ins_attributes, :version, :jmep_json, :jmep_text, :pro_ascap, :pro_bmi, :pro_sesac, :duration, as: :admin @@ -39,14 +39,17 @@ module JamRuby validates :public_performance_royalty, inclusion: {in: [nil, true, false]} validates :duration, numericality: {only_integer: true}, :allow_nil => true - validates_format_of :reproduction_royalty_amount, with: /^\d+\.*\d{0,3}$/ - validates_format_of :licensor_royalty_amount, with: /^\d+\.*\d{0,3}$/ + validates_format_of :reproduction_royalty_amount, with: /^\d+\.*\d{0,4}$/, :allow_blank => true + validates_format_of :licensor_royalty_amount, with: /^\d+\.*\d{0,4}$/, :allow_blank => true - belongs_to :genre, class_name: "JamRuby::Genre" belongs_to :licensor , class_name: 'JamRuby::JamTrackLicensor', foreign_key: 'licensor_id' + has_many :genres_jam_tracks, :class_name => "JamRuby::GenreJamTrack", :foreign_key => "jam_track_id" + has_many :genres, :through => :genres_jam_tracks, :class_name => "JamRuby::Genre", :source => :genre + has_many :jam_track_tracks, :class_name => "JamRuby::JamTrackTrack", order: 'track_type ASC, position ASC, part ASC, instrument_id ASC' has_many :jam_track_tap_ins, :class_name => "JamRuby::JamTrackTapIn", order: 'offset_time ASC' + has_many :jam_track_files, :class_name => "JamRuby::JamTrackFile" has_many :jam_track_rights, :class_name => "JamRuby::JamTrackRight" #, inverse_of: 'jam_track', :foreign_key => "jam_track_id" # ' @@ -67,6 +70,82 @@ module JamRuby accepts_nested_attributes_for :jam_track_tracks, allow_destroy: true accepts_nested_attributes_for :jam_track_tap_ins, allow_destroy: true + + # we can make sure a few things stay in sync here. + # 1) the reproduction_royalty_amount has to stay in sync based on duration + # 2) the onboarding_exceptions JSON column + after_save :sync_reproduction_royalty + after_save :sync_onboarding_exceptions + + + def sync_reproduction_royalty + + # reproduction royalty table based on duration + + # The statutory mechanical royalty rate for permanent digital downloads is: + # 9.10¢ per copy for songs 5 minutes or less, or + # 1.75¢ per minute or fraction thereof, per copy for songs over 5 minutes. + # So the base rate is 9.1 cents for anything up to 5 minutes. + # 5.01 to 6 minutes should be 10.5 cents. + # 6.01 to 7 minutes should be 12.25 cents. + # Etc. + + royalty = nil + if self.duration + minutes = (self.duration - 1) / 60 + extra_minutes = minutes - 4 + extra_minutes = 0 if extra_minutes < 0 + royalty = (0.091 + (0.0175 * extra_minutes)).round(5) + end + self.update_column(:reproduction_royalty_amount, royalty) + + true + end + + def sync_onboarding_exceptions + + exceptions = {} + if self.duration.nil? + exceptions[:no_duration] = true + end + + if self.genres.count == 0 + exceptions[:no_genres] = true + end + + if self.year.nil? + exceptions[:no_year] = true + end + + if self.licensor.nil? + exceptions[:no_licensor] = true + end + + if self.missing_instrument_info? + exceptions[:unknown_instrument] = true + end + + if self.master_track.nil? + exceptions[:no_master] = true + end + + if missing_previews? + exceptions[:missing_previews] = true + end + + if duplicate_positions? + exceptions[:duplicate_positions] = true + end + + if exceptions.keys.length == 0 + self.update_column(:onboarding_exceptions, nil) + else + self.update_column(:onboarding_exceptions, exceptions.to_json) + end + + true + end + def duplicate_positions? counter = {} jam_track_tracks.each do |track| @@ -87,6 +166,17 @@ module JamRuby duplicate end + def missing_instrument_info? + missing_instrument_info = false + self.jam_track_tracks.each do |track| + if track.instrument_id == 'other' && (track.part == nil || track.part.start_with?('Other')) + missing_instrument_info = true + break + end + end + missing_instrument_info + end + def missing_previews? missing_preview = false self.jam_track_tracks.each do |track| @@ -171,7 +261,7 @@ module JamRuby end if options[:group_artist] - query = query.select("original_artist, array_agg(jam_tracks.id) AS id, MIN(name) AS name, MIN(description) AS description, MIN(recording_type) AS recording_type, MIN(original_artist) AS original_artist, MIN(songwriter) AS songwriter, MIN(publisher) AS publisher, MIN(sales_region) AS sales_region, MIN(price) AS price, MIN(version) AS version, MIN(genre_id) AS genre_id") + query = query.select("original_artist, array_agg(jam_tracks.id) AS id, MIN(name) AS name, MIN(description) AS description, MIN(recording_type) AS recording_type, MIN(original_artist) AS original_artist, MIN(songwriter) AS songwriter, MIN(publisher) AS publisher, MIN(sales_region) AS sales_region, MIN(price) AS price, MIN(version) AS version") query = query.group("original_artist") query = query.order('jam_tracks.original_artist') else @@ -180,7 +270,12 @@ module JamRuby end query = query.where("jam_tracks.status = ?", 'Production') unless user.admin - query = query.where("jam_tracks.genre_id = '#{options[:genre]}'") unless options[:genre].blank? + + unless options[:genre].blank? + query = query.joins(:genres) + query = query.where('genre_id = ? ', options[:genre]) + end + query = query.where("jam_track_tracks.instrument_id = '#{options[:instrument]}' and jam_track_tracks.track_type != 'Master'") unless options[:instrument].blank? query = query.where("jam_tracks.sales_region = '#{options[:availability]}'") unless options[:availability].blank? @@ -231,7 +326,12 @@ module JamRuby query = query.order('jam_tracks.original_artist') query = query.where("jam_tracks.status = ?", 'Production') unless user.admin - query = query.where("jam_tracks.genre_id = '#{options[:genre]}'") unless options[:genre].blank? + + unless options[:genre].blank? + query = query.joins(:genres) + query = query.where('genre_id = ? ', options[:genre]) + end + query = query.where("jam_track_tracks.instrument_id = '#{options[:instrument]}'") unless options[:instrument].blank? query = query.where("jam_tracks.sales_region = '#{options[:availability]}'") unless options[:availability].blank? diff --git a/ruby/lib/jam_ruby/models/jam_track_file.rb b/ruby/lib/jam_ruby/models/jam_track_file.rb new file mode 100644 index 000000000..e7c880165 --- /dev/null +++ b/ruby/lib/jam_ruby/models/jam_track_file.rb @@ -0,0 +1,78 @@ +module JamRuby + + # holds a click track or precount file + class JamTrackFile < ActiveRecord::Base + include JamRuby::S3ManagerMixin + + # there should only be one Master per JamTrack, but there can be N Track per JamTrack + FILE_TYPE = %w{ClickWav ClickTxt Precount} + + @@log = Logging.logger[JamTrackFile] + + before_destroy :delete_s3_files + + attr_accessible :jam_track_id, :file_type, :filename, as: :admin + attr_accessible :url, :md5, :length, as: :admin + + attr_accessor :original_audio_s3_path, :skip_uploader, :preview_generate_error + + before_destroy :delete_s3_files + + validates :file_type, inclusion: {in: FILE_TYPE } + + belongs_to :jam_track, class_name: "JamRuby::JamTrack" + + # create storage directory that will house this jam_track, as well as + def store_dir + "jam_track_files" + end + + # create name of the file + def filename(original_name) + "#{store_dir}/#{jam_track.original_artist}/#{jam_track.name}/#{original_name}" + end + + def manually_uploaded_filename + if click_wav? + filename('click.wav') + elsif click_txt? + filename('click.txt') + elsif precount? + filename('precount.wav') + else + raise 'unknown file type: ' + file_type + end + + end + + def click_wav? + track_type == 'ClickWav' + end + + def click_txt? + track_type == 'ClickTxt' + end + + def precount? + track_type == 'Precount' + end + + # creates a short-lived URL that has access to the object. + # the idea is that this is used when a user who has the rights to this tries to download this JamTrack + # we would verify their rights (can_download?), and generates a URL in response to the click so that they can download + # but the url is short lived enough so that it wouldn't be easily shared + def sign_url(expiration_time = 120) + s3_manager.sign_url(self[url], {:expires => expiration_time, :response_content_type => 'audio/wav', :secure => true}) + end + + def can_download?(user) + # I think we have to make a special case for 'previews', but maybe that's just up to the controller to not check can_download? + jam_track.owners.include?(user) + end + + + def delete_s3_files + s3_manager.delete(self[:url]) if self[:url] && s3_manager.exists?(self[:url]) + end + end +end diff --git a/ruby/lib/jam_ruby/models/jam_track_track.rb b/ruby/lib/jam_ruby/models/jam_track_track.rb index e20469076..c99246876 100644 --- a/ruby/lib/jam_ruby/models/jam_track_track.rb +++ b/ruby/lib/jam_ruby/models/jam_track_track.rb @@ -131,81 +131,19 @@ module JamRuby end + def generate_preview begin Dir.mktmpdir do |tmp_dir| input = File.join(tmp_dir, 'in.ogg') - output = File.join(tmp_dir, 'out.ogg') - output_mp3 = File.join(tmp_dir, 'out.mp3') - - start = self.preview_start_time.to_f / 1000 - stop = start + 20 raise 'no track' unless self["url_44"] s3_manager.download(self.url_by_sample_rate(44), input) - command = "sox \"#{input}\" \"#{output}\" trim #{sprintf("%.3f", start)} =#{sprintf("%.3f", stop)}" - - @@log.debug("trimming using: " + command) - - sox_output = `#{command}` - - result_code = $?.to_i - - if result_code != 0 - @@log.debug("fail #{result_code}") - @preview_generate_error = "unable to execute cut command #{sox_output}" - else - # now create mp3 off of ogg preview - - convert_mp3_cmd = "#{APP_CONFIG.ffmpeg_path} -i \"#{output}\" -ab 192k \"#{output_mp3}\"" - - @@log.debug("converting to mp3 using: " + convert_mp3_cmd) - - convert_output = `#{convert_mp3_cmd}` - - result_code = $?.to_i - - if result_code != 0 - @@log.debug("fail #{result_code}") - @preview_generate_error = "unable to execute mp3 convert command #{convert_output}" - else - ogg_digest = ::Digest::MD5.file(output) - mp3_digest = ::Digest::MD5.file(output_mp3) - self["preview_md5"] = ogg_md5 = ogg_digest.hexdigest - self["preview_mp3_md5"] = mp3_md5 = mp3_digest.hexdigest - - @@log.debug("uploading ogg preview to #{self.preview_filename('ogg')}") - s3_public_manager.upload(self.preview_filename(ogg_md5, 'ogg'), output, content_type: 'audio/ogg', content_md5: ogg_digest.base64digest) - @@log.debug("uploading mp3 preview to #{self.preview_filename('mp3')}") - s3_public_manager.upload(self.preview_filename(mp3_md5, 'mp3'), output_mp3, content_type: 'audio/mpeg', content_md5: mp3_digest.base64digest) - - self.skip_uploader = true - - original_ogg_preview_url = self["preview_url"] - original_mp3_preview_url = self["preview_mp3_url"] - - # and finally update the JamTrackTrack with the new info - self["preview_url"] = self.preview_filename(ogg_md5, 'ogg') - self["preview_length"] = File.new(output).size - # and finally update the JamTrackTrack with the new info - self["preview_mp3_url"] = self.preview_filename(mp3_md5, 'mp3') - self["preview_mp3_length"] = File.new(output_mp3).size - self.save! - - # if all that worked, now delete old previews, if present - begin - s3_public_manager.delete(original_ogg_preview_url) if original_ogg_preview_url && original_ogg_preview_url != self["preview_url"] - s3_public_manager.delete(original_mp3_preview_url) if original_mp3_preview_url && original_mp3_preview_url != track["preview_mp3_url"] - rescue - puts "UNABLE TO CLEANUP OLD PREVIEW URL" - end - - end - end + process_preview(input, tmp_dir) end rescue Exception => e @@log.error("error in sox command #{e.to_s}") @@ -214,6 +152,76 @@ module JamRuby end + # input is the original ogg file for the track. tmp_dir is where this code can safely generate output stuff and have it cleaned up later + def process_preview(input, tmp_dir) + uuid = SecureRandom.uuid + output = File.join(tmp_dir, "#{uuid}.ogg") + output_mp3 = File.join(tmp_dir, "#{uuid}.mp3") + + start = self.preview_start_time.to_f / 1000 + stop = start + 20 + + command = "sox \"#{input}\" \"#{output}\" trim #{sprintf("%.3f", start)} =#{sprintf("%.3f", stop)}" + + @@log.debug("trimming using: " + command) + + sox_output = `#{command}` + + result_code = $?.to_i + + if result_code != 0 + @@log.debug("fail #{result_code}") + @preview_generate_error = "unable to execute cut command #{sox_output}" + else + # now create mp3 off of ogg preview + + convert_mp3_cmd = "#{APP_CONFIG.ffmpeg_path} -i \"#{output}\" -ab 192k \"#{output_mp3}\"" + + @@log.debug("converting to mp3 using: " + convert_mp3_cmd) + + convert_output = `#{convert_mp3_cmd}` + + result_code = $?.to_i + + if result_code != 0 + @@log.debug("fail #{result_code}") + @preview_generate_error = "unable to execute mp3 convert command #{convert_output}" + else + ogg_digest = ::Digest::MD5.file(output) + mp3_digest = ::Digest::MD5.file(output_mp3) + self["preview_md5"] = ogg_md5 = ogg_digest.hexdigest + self["preview_mp3_md5"] = mp3_md5 = mp3_digest.hexdigest + + @@log.debug("uploading ogg preview to #{self.preview_filename('ogg')}") + s3_public_manager.upload(self.preview_filename(ogg_md5, 'ogg'), output, content_type: 'audio/ogg', content_md5: ogg_digest.base64digest) + @@log.debug("uploading mp3 preview to #{self.preview_filename('mp3')}") + s3_public_manager.upload(self.preview_filename(mp3_md5, 'mp3'), output_mp3, content_type: 'audio/mpeg', content_md5: mp3_digest.base64digest) + + self.skip_uploader = true + + original_ogg_preview_url = self["preview_url"] + original_mp3_preview_url = self["preview_mp3_url"] + + # and finally update the JamTrackTrack with the new info + self["preview_url"] = self.preview_filename(ogg_md5, 'ogg') + self["preview_length"] = File.new(output).size + # and finally update the JamTrackTrack with the new info + self["preview_mp3_url"] = self.preview_filename(mp3_md5, 'mp3') + self["preview_mp3_length"] = File.new(output_mp3).size + self.save! + + # if all that worked, now delete old previews, if present + begin + s3_public_manager.delete(original_ogg_preview_url) if original_ogg_preview_url && original_ogg_preview_url != self["preview_url"] + s3_public_manager.delete(original_mp3_preview_url) if original_mp3_preview_url && original_mp3_preview_url != track["preview_mp3_url"] + rescue + puts "UNABLE TO CLEANUP OLD PREVIEW URL" + end + + end + end + end + private def normalize_position diff --git a/ruby/spec/factories.rb b/ruby/spec/factories.rb index 90cd48c44..531d3cc46 100644 --- a/ruby/spec/factories.rb +++ b/ruby/spec/factories.rb @@ -740,7 +740,7 @@ FactoryGirl.define do licensor_royalty_amount 0.999 sequence(:plan_code) { |n| "jamtrack-#{n}" } - genre JamRuby::Genre.first + genres [JamRuby::Genre.first] association :licensor, factory: :jam_track_licensor factory :jam_track_with_tracks do diff --git a/ruby/spec/jam_ruby/jam_track_importer_spec.rb b/ruby/spec/jam_ruby/jam_track_importer_spec.rb index a2cf96620..fbdc3d9cc 100644 --- a/ruby/spec/jam_ruby/jam_track_importer_spec.rb +++ b/ruby/spec/jam_ruby/jam_track_importer_spec.rb @@ -22,11 +22,13 @@ describe JamTrackImporter do in_directory_with_file(metafile) before(:each) do + JamTrackImporter.storage_format = 'default' content_for_file(YAML.dump(sample_yml)) end it "no meta" do s3_metalocation = 'audio/Artist 1/Bogus Place/meta.yml' + JamTrackImporter.storage_format = 'default' JamTrackImporter.load_metalocation(s3_metalocation).should be_nil end @@ -38,9 +40,105 @@ describe JamTrackImporter do end end + describe "sort_tracks" do + let(:jam_track) { FactoryGirl.create(:jam_track) } + let(:importer) { JamTrackImporter.new() } + let(:vocal) {Instrument.find('voice')} + let(:drums) {Instrument.find('drums')} + let(:bass_guitar) {Instrument.find('bass guitar')} + let(:piano) {Instrument.find('piano')} + let(:keyboard) {Instrument.find('keyboard')} + let(:acoustic_guitar) {Instrument.find('acoustic guitar')} + let(:electric_guitar) {Instrument.find('electric guitar')} + let(:other) {Instrument.find('other')} + + it "the big sort" do + # specified in https://jamkazam.atlassian.net/browse/VRFS-3296 + vocal_lead = FactoryGirl.build(:jam_track_track, jam_track: jam_track, instrument: vocal, part: 'Lead') + vocal_lead_female = FactoryGirl.build(:jam_track_track, jam_track: jam_track, instrument: vocal, part: 'Lead Female') + vocal_lead_male = FactoryGirl.build(:jam_track_track, jam_track: jam_track, instrument: vocal, part: 'Lead Male') + vocal_backing = FactoryGirl.build(:jam_track_track, jam_track: jam_track, instrument: vocal, part: 'Backing') + vocal_random = FactoryGirl.build(:jam_track_track, jam_track: jam_track, instrument: vocal, part: 'Random') + drums_drums = FactoryGirl.build(:jam_track_track, jam_track: jam_track, instrument: drums, part: 'Drums') + drums_percussion = FactoryGirl.build(:jam_track_track, jam_track: jam_track, instrument: drums, part: 'Percussion') + drums_random_1 = FactoryGirl.build(:jam_track_track, jam_track: jam_track, instrument: drums, part: 'A') + drums_random_2 = FactoryGirl.build(:jam_track_track, jam_track: jam_track, instrument: drums, part: 'C') + bass_guitar_bass = FactoryGirl.build(:jam_track_track, jam_track: jam_track, instrument: bass_guitar, part: 'Bass') + bass_guitar_random_1 = FactoryGirl.build(:jam_track_track, jam_track: jam_track, instrument: bass_guitar, part: 'some bass') + bass_guitar_random_2 = FactoryGirl.build(:jam_track_track, jam_track: jam_track, instrument: bass_guitar, part: 'zome bass') + piano_piano = FactoryGirl.build(:jam_track_track, jam_track: jam_track, instrument: piano, part: 'Piano') + keyboard_synth_1 = FactoryGirl.build(:jam_track_track, jam_track: jam_track, instrument: keyboard, part: 'Synth 1') + keyboard_synth_2 = FactoryGirl.build(:jam_track_track, jam_track: jam_track, instrument: keyboard, part: 'Synth 2') + keyboard_pads = FactoryGirl.build(:jam_track_track, jam_track: jam_track, instrument: keyboard, part: 'Pads') + keyboard_random_1 = FactoryGirl.build(:jam_track_track, jam_track: jam_track, instrument: keyboard, part: 'A') + keyboard_random_2 = FactoryGirl.build(:jam_track_track, jam_track: jam_track, instrument: keyboard, part: 'Z') + acoust_guitar_lead = FactoryGirl.build(:jam_track_track, jam_track: jam_track, instrument: acoustic_guitar, part: 'Lead') + acoust_guitar_lead_x = FactoryGirl.build(:jam_track_track, jam_track: jam_track, instrument: acoustic_guitar, part: 'Lead X') + acoust_guitar_solo_1 = FactoryGirl.build(:jam_track_track, jam_track: jam_track, instrument: acoustic_guitar, part: 'Solo 1') + acoust_guitar_solo_2 = FactoryGirl.build(:jam_track_track, jam_track: jam_track, instrument: acoustic_guitar, part: 'Solo 2') + acoust_guitar_rhythm = FactoryGirl.build(:jam_track_track, jam_track: jam_track, instrument: acoustic_guitar, part: 'Rhythm') + acoust_guitar_random_1 = FactoryGirl.build(:jam_track_track, jam_track: jam_track, instrument: acoustic_guitar, part: 'A') + acoust_guitar_random_2 = FactoryGirl.build(:jam_track_track, jam_track: jam_track, instrument: acoustic_guitar, part: 'Z') + elect_guitar_lead = FactoryGirl.build(:jam_track_track, jam_track: jam_track, instrument: electric_guitar, part: 'Lead') + elect_guitar_lead_x = FactoryGirl.build(:jam_track_track, jam_track: jam_track, instrument: electric_guitar, part: 'Lead X') + elect_guitar_solo_1 = FactoryGirl.build(:jam_track_track, jam_track: jam_track, instrument: electric_guitar, part: 'Solo 1') + elect_guitar_solo_2 = FactoryGirl.build(:jam_track_track, jam_track: jam_track, instrument: electric_guitar, part: 'Solo 2') + elect_guitar_rhythm = FactoryGirl.build(:jam_track_track, jam_track: jam_track, instrument: electric_guitar, part: 'Rhythm') + elect_guitar_random_1 = FactoryGirl.build(:jam_track_track, jam_track: jam_track, instrument: electric_guitar, part: 'A') + elect_guitar_random_2 = FactoryGirl.build(:jam_track_track, jam_track: jam_track, instrument: electric_guitar, part: 'Z') + other_1 = FactoryGirl.build(:jam_track_track, jam_track: jam_track, instrument: other, part: 'Other 1') + other_2 = FactoryGirl.build(:jam_track_track, jam_track: jam_track, instrument: other, part: 'Other 2') + + expected = [ + vocal_lead, + vocal_lead_female, + vocal_lead_male, + vocal_backing, + vocal_random, + drums_drums, + drums_percussion, + drums_random_1, + drums_random_2, + bass_guitar_bass, + piano_piano, + keyboard_synth_1, + keyboard_synth_2, + keyboard_pads, + keyboard_random_1, + keyboard_random_2, + acoust_guitar_lead, + acoust_guitar_lead_x, + acoust_guitar_rhythm, + acoust_guitar_random_1, + acoust_guitar_solo_1, + acoust_guitar_solo_2, + acoust_guitar_random_2, + elect_guitar_lead, + elect_guitar_lead_x, + elect_guitar_solo_1, + elect_guitar_solo_2, + elect_guitar_rhythm, + elect_guitar_random_1, + elect_guitar_random_2, + bass_guitar_random_1, + bass_guitar_random_2, + other_1, + other_2 + ] + shuffled = expected.shuffle + sorted_tracks = importer.sort_tracks(shuffled) + + importer.set_custom_weight(vocal_lead).should eq(100) + + expected.each_with_index do |expected_track, i| + sorted_tracks[i].should eq(expected_track) + end + end + end + describe "synchronize" do let(:jam_track) { JamTrack.new } - let(:importer) { JamTrackImporter.new } + let(:importer) { JamTrackImporter.new() } let(:minimum_meta) { nil } let(:metalocation) { 'audio/Artist 1/Song 1/meta.yml' } let(:options) {{ skip_audio_upload:true }} @@ -64,7 +162,7 @@ describe JamTrackImporter do describe "parse_wav" do it "Guitar" do - result = JamTrackImporter.new.parse_wav('blah/Ready for Love Stem - Guitar - Main.wav') + result = JamTrackImporter.new.parse_file('blah/Ready for Love Stem - Guitar - Main.wav') result[:instrument].should eq('electric guitar') result[:part].should eq('Main') end diff --git a/ruby/spec/jam_ruby/models/jam_track_spec.rb b/ruby/spec/jam_ruby/models/jam_track_spec.rb index d17019df7..c1aecf71d 100644 --- a/ruby/spec/jam_ruby/models/jam_track_spec.rb +++ b/ruby/spec/jam_ruby/models/jam_track_spec.rb @@ -12,6 +12,90 @@ describe JamTrack do jam_track = FactoryGirl.create(:jam_track) jam_track.licensor.should_not be_nil jam_track.licensor.jam_tracks.should == [jam_track] + jam_track.genres.length.should eq(1) + end + + describe 'sync_reproduction_royalty' do + it "all possible conditions" do + jam_track = FactoryGirl.create(:jam_track) + jam_track.reproduction_royalty_amount.should be_nil + + jam_track.duration = 0 + jam_track.save! + jam_track.reproduction_royalty_amount.to_f.should eq(0.091) + + jam_track.duration = 1 + jam_track.save! + jam_track.reproduction_royalty_amount.to_f.should eq(0.091) + + jam_track.duration = 5 * 60 - 1 # just under 5 minutes + jam_track.save! + jam_track.reproduction_royalty_amount.to_f.should eq(0.091) + + jam_track.duration = 5 * 60 + jam_track.save! + jam_track.reproduction_royalty_amount.to_f.should eq(0.091) + + jam_track.duration = 6 * 60 - 1 # just under 6 minutes + jam_track.save! + jam_track.reproduction_royalty_amount.should eq(0.091 + 0.0175) + + jam_track.duration = 6 * 60 + jam_track.save! + jam_track.reproduction_royalty_amount.should eq(0.091 + 0.0175) + + jam_track.duration = 7 * 60 - 1 + jam_track.save! + jam_track.reproduction_royalty_amount.should eq(0.091 + 0.0175 * 2) + + jam_track.duration = 7 * 60 + jam_track.save! + jam_track.reproduction_royalty_amount.should eq(0.091 + 0.0175 * 2) + + jam_track.duration = 8 * 60 - 1 + jam_track.save! + jam_track.reproduction_royalty_amount.should eq(0.091 + 0.0175 * 3) + + jam_track.duration = 8 * 60 + jam_track.save! + jam_track.reproduction_royalty_amount.should eq(0.091 + 0.0175 * 3) + + jam_track.duration = 9 * 60 - 1 + jam_track.save! + jam_track.reproduction_royalty_amount.should eq(0.091 + 0.0175 * 4) + + jam_track.duration = 9 * 60 + jam_track.save! + jam_track.reproduction_royalty_amount.should eq(0.091 + 0.0175 * 4) + + jam_track.duration = 10 * 60 - 1 + jam_track.save! + jam_track.reproduction_royalty_amount.should eq(0.091 + 0.0175 * 5) + + jam_track.duration = 10 * 60 + jam_track.save! + jam_track.reproduction_royalty_amount.should eq(0.091 + 0.0175 * 5) + + jam_track.duration = 11 * 60 - 1 + jam_track.save! + jam_track.reproduction_royalty_amount.should eq(0.091 + 0.0175 * 6) + + jam_track.duration = 11 * 60 + jam_track.save! + jam_track.reproduction_royalty_amount.should eq(0.091 + 0.0175 * 6) + + jam_track.duration = 12 * 60 - 1 + jam_track.save! + jam_track.reproduction_royalty_amount.should eq(0.091 + 0.0175 * 7) + + jam_track.duration = 12 * 60 + jam_track.save! + jam_track.reproduction_royalty_amount.should eq(0.091 + 0.0175 * 7) + + jam_track.duration = 13 * 60 + jam_track.save! + jam_track.reproduction_royalty_amount.should eq(0.091 + 0.0175 * 8) + end end describe 'plays' do @@ -98,6 +182,26 @@ describe JamTrack do query[1].should eq(jam_track1) end + it "queries on genre" do + jam_track1 = FactoryGirl.create(:jam_track_with_tracks, original_artist: 'artist', name: 'a') + jam_track2 = FactoryGirl.create(:jam_track_with_tracks, original_artist: 'artist', name: 'b') + jam_track1.genres = [Genre.find('rock')] + jam_track2.genres = [Genre.find('asian')] + jam_track1.save! + jam_track2.save! + + query, pager = JamTrack.index({genre: 'rock'}, user) + query.size.should == 1 + query[0].should eq(jam_track1) + + query, pager = JamTrack.index({genre: 'asian'}, user) + query.size.should == 1 + query[0].should eq(jam_track2) + + query, pager = JamTrack.index({genre: 'african'}, user) + query.size.should == 0 + end + it "supports showing purchased only" do jam_track1 = FactoryGirl.create(:jam_track_with_tracks, name: 'a') @@ -170,7 +274,7 @@ describe JamTrack do end it "100.1234" do - jam_track = FactoryGirl.build(:jam_track, reproduction_royalty_amount: 100.1234) + jam_track = FactoryGirl.build(:jam_track, reproduction_royalty_amount: 100.12345) jam_track.valid?.should be_false jam_track.errors[:reproduction_royalty_amount].should == ['is invalid'] end diff --git a/ruby/spec/jam_ruby/models/jam_track_track_spec.rb b/ruby/spec/jam_ruby/models/jam_track_track_spec.rb index 6fb4343f6..67a50ec22 100644 --- a/ruby/spec/jam_ruby/models/jam_track_track_spec.rb +++ b/ruby/spec/jam_ruby/models/jam_track_track_spec.rb @@ -7,6 +7,7 @@ describe JamTrackTrack do it "created" do jam_track_track = FactoryGirl.create(:jam_track_track) jam_track_track.jam_track.should_not be_nil + jam_track_track.jam_track.reload jam_track_track.jam_track.jam_track_tracks.should == [jam_track_track] end diff --git a/web/app/assets/javascripts/react-components/SessionMediaTracks.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionMediaTracks.js.jsx.coffee index d4d50a8ae..fdbaa40e9 100644 --- a/web/app/assets/javascripts/react-components/SessionMediaTracks.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/SessionMediaTracks.js.jsx.coffee @@ -263,10 +263,12 @@ ChannelGroupIds = context.JK.ChannelGroupIds # All the JamTracks mediaTracks.push(``) + if @state.metronome? @state.metronome.mode = MIX_MODES.PERSONAL mediaTracks.push(``) + for jamTrack in @state.jamTracks jamTrack.mode = MIX_MODES.PERSONAL mediaTracks.push(``) diff --git a/web/app/assets/javascripts/react-components/SessionMetronome.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionMetronome.js.jsx.coffee index aa7654c7e..afd9567da 100644 --- a/web/app/assets/javascripts/react-components/SessionMetronome.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/SessionMetronome.js.jsx.coffee @@ -32,6 +32,7 @@ MIX_MODES = context.JK.MIX_MODES componentClasses = classNames({ "session-track" : true "metronome" : true + "in-jam-track" : @props.location == 'jam-track' "no-mixer" : @props.mode == MIX_MODES.MASTER # show it as disabled if in master mode "in-jam-track" : @props.location == 'jam-track' }) diff --git a/web/app/assets/stylesheets/client/react-components/SessionTrack.css.scss b/web/app/assets/stylesheets/client/react-components/SessionTrack.css.scss index 57c2bbe03..82614b21e 100644 --- a/web/app/assets/stylesheets/client/react-components/SessionTrack.css.scss +++ b/web/app/assets/stylesheets/client/react-components/SessionTrack.css.scss @@ -175,8 +175,10 @@ .track-controls { margin-left:0; } + &.in-jam-track { min-height:56px; + .track-buttons { margin-top:2px; } diff --git a/web/app/controllers/api_music_sessions_controller.rb b/web/app/controllers/api_music_sessions_controller.rb index f1beaa5f9..c7433ea5a 100644 --- a/web/app/controllers/api_music_sessions_controller.rb +++ b/web/app/controllers/api_music_sessions_controller.rb @@ -482,7 +482,7 @@ class ApiMusicSessionsController < ApiController comment.save if comment.errors.any? - render :json => { :message => "Unexpected error occurred" }, :status => 500 + render :json => { :errors => comment.errors }, :status => 422 return else render :json => {}, :status => 201 @@ -508,7 +508,7 @@ class ApiMusicSessionsController < ApiController comment.save if comment.errors.any? - render :json => { :message => "Unexpected error occurred" }, :status => 500 + render :json => { :errors => comment.errors }, :status => 422 return else music_session = MusicSession.find(params[:id]) diff --git a/web/app/controllers/api_recordings_controller.rb b/web/app/controllers/api_recordings_controller.rb index b4012be5d..0767d9053 100644 --- a/web/app/controllers/api_recordings_controller.rb +++ b/web/app/controllers/api_recordings_controller.rb @@ -155,7 +155,7 @@ class ApiRecordingsController < ApiController comment.save if comment.errors.any? - render :json => { :message => "Unexpected error occurred" }, :status => 500 + render :json => { :errors => comment.errors }, :status => 422 return else render :json => {}, :status => 201 @@ -178,7 +178,7 @@ class ApiRecordingsController < ApiController liker.save if liker.errors.any? - render :json => { :message => "Unexpected error occurred" }, :status => 500 + render :json => { :errors => liker.errors }, :status => 422 return else render :json => {}, :status => 201 diff --git a/web/app/controllers/api_users_controller.rb b/web/app/controllers/api_users_controller.rb index 5c8f27377..18c2db8f8 100644 --- a/web/app/controllers/api_users_controller.rb +++ b/web/app/controllers/api_users_controller.rb @@ -796,7 +796,7 @@ class ApiUsersController < ApiController play.save if play.errors.any? - render :json => { :message => "Unexpected error occurred" }, :status => 500 + render :json => { :errors => play.errors }, :status => 422 else render :json => {}, :status => 201 end diff --git a/web/app/views/api_jam_tracks/show.rabl b/web/app/views/api_jam_tracks/show.rabl index 028180632..e05ca0cbf 100644 --- a/web/app/views/api_jam_tracks/show.rabl +++ b/web/app/views/api_jam_tracks/show.rabl @@ -3,7 +3,7 @@ object @jam_track attributes :id, :name, :description, :recording_type, :original_artist, :songwriter, :publisher, :sales_region, :price, :version, :duration node :genres do |item| - [item.genre.description] # XXX: need to return single genre; not array + item.genres.select(:description).map(&:description) end node :added_cart do |item| diff --git a/web/app/views/api_jam_tracks/show_for_client.rabl b/web/app/views/api_jam_tracks/show_for_client.rabl index bc212f46d..ce60ba00b 100644 --- a/web/app/views/api_jam_tracks/show_for_client.rabl +++ b/web/app/views/api_jam_tracks/show_for_client.rabl @@ -1,9 +1,9 @@ object @jam_track -attributes :id, :name, :description, :initial_play_silence, :original_artist, :version, :genre +attributes :id, :name, :description, :initial_play_silence, :original_artist, :version -node :genre do |jam_track| - jam_track.genre.present? ? jam_track.genre.id : nil +node :genres do |item| + item.genres.select(:description).map(&:description) end node :jmep do |jam_track| diff --git a/web/lib/tasks/jam_tracks.rake b/web/lib/tasks/jam_tracks.rake index 4d28e89a4..1774cfc64 100644 --- a/web/lib/tasks/jam_tracks.rake +++ b/web/lib/tasks/jam_tracks.rake @@ -4,15 +4,49 @@ namespace :jam_tracks do JamTrackImporter.dry_run end + task tency_dry_run: :environment do |task, args| + JamTrackImporter.storage_format = 'Tency' + JamTrackImporter.dry_run + end + + task tency_create_masters: :environment do |task, args| + JamTrackImporter.storage_format = 'Tency' + JamTrackImporter.create_masters + end + + + task tency_create_master: :environment do |task, args| + JamTrackImporter.storage_format = 'Tency' + + path = ENV['TRACK_PATH'] + + if !path + puts "TRACK_PATH must be set to something like audio/AC DC/Back in Black or mapped/50 Cent - In Da Club - 12401" + exit(1) + end + + JamTrackImporter.create_master(path) + end + + + task tency_delta: :environment do |task, args| + JamTrackImporter.storage_format = 'Tency' + JamTrackImporter.tency_delta + end + task sync: :environment do |task, args| path = ENV['TRACK_PATH'] if !path - puts "TRACK_PATH must be set to something like AD DC/Back in Black" + puts "TRACK_PATH must be set to something like audio/AC DC/Back in Black or mapped/50 Cent - In Da Club - 12401" exit(1) end - JamTrackImporter.synchronize_from_meta("audio/#{path}/meta.yml", skip_audio_upload:false) + if path.start_with?('mapped') + JamTrackImporter.storage_format = 'Tency' + end + + JamTrackImporter.synchronize_from_meta("#{path}/meta.yml", skip_audio_upload:false) end task resync_audio: :environment do |task, args| @@ -26,6 +60,20 @@ namespace :jam_tracks do JamTrackImporter.synchronize_from_meta("audio/#{path}/meta.yml", resync_audio:true, skip_audio_upload:false) end + task tency_genre_dump: :environment do |task, args| + JamTrackImporter.storage_format = 'Tency' + JamTrackImporter.genre_dump + end + + task sync_tency: :environment do |task, args| + JamTrackImporter.storage_format = 'Tency' + JamTrackImporter.synchronize_all(skip_audio_upload:false) + end + + task onboarding_exceptions: :environment do |task, args| + JamTrackImporter.onboarding_exceptions + end + task sync_all: :environment do |task, args| JamTrackImporter.synchronize_all(skip_audio_upload:false) end @@ -91,4 +139,10 @@ namespace :jam_tracks do task download_masters: :environment do |task, arg| JamTrackImporter.download_masters end + + + task tency: :environment do |task, arg| + mapper = TencyStemMapping.new + mapper.correlate + end end diff --git a/web/spec/controllers/api_jam_tracks_controller_spec.rb b/web/spec/controllers/api_jam_tracks_controller_spec.rb index 74bc1da94..25f971ec9 100644 --- a/web/spec/controllers/api_jam_tracks_controller_spec.rb +++ b/web/spec/controllers/api_jam_tracks_controller_spec.rb @@ -128,6 +128,7 @@ describe ApiJamTracksController do # of this process is checked in other tests: @ogg_path = File.join('spec', 'files', 'on.ogg') @jam_track = FactoryGirl.create(:jam_track) #jam_track_track.jam_track + @jam_track.reload jam_track_track = @jam_track.jam_track_tracks.first # 48 kHz: diff --git a/web/spec/factories.rb b/web/spec/factories.rb index 52edb7820..48f2ef3af 100644 --- a/web/spec/factories.rb +++ b/web/spec/factories.rb @@ -731,7 +731,7 @@ FactoryGirl.define do make_track true end - genre JamRuby::Genre.first + genres [JamRuby::Genre.first] association :licensor, factory: :jam_track_licensor after(:create) do |jam_track, evaluator| diff --git a/web/spec/features/jamtrack_shopping_spec.rb b/web/spec/features/jamtrack_shopping_spec.rb index 23da6a3f4..a3171a282 100644 --- a/web/spec/features/jamtrack_shopping_spec.rb +++ b/web/spec/features/jamtrack_shopping_spec.rb @@ -5,8 +5,8 @@ describe "JamTrack Shopping", :js => true, :type => :feature, :capybara_feature let(:user) { FactoryGirl.create(:user, has_redeemable_jamtrack: false) } let(:jt_us) { FactoryGirl.create(:jam_track, :name=>'jt_us', sales_region: 'Worldwide', make_track: true, original_artist: "foobar") } let(:jt_ww) { FactoryGirl.create(:jam_track, :name=>'jt_ww', sales_region: 'Worldwide', make_track: true, original_artist: "barfoo") } - let(:jt_rock) { FactoryGirl.create(:jam_track, :name=>'jt_rock', genre: JamRuby::Genre.find('rock'), make_track: true, original_artist: "badfood") } - let(:jt_blues) { FactoryGirl.create(:jam_track, :name=>'jt_blues', genre: JamRuby::Genre.find('blues'), make_track: true, original_artist: "foodbart") } + let(:jt_rock) { FactoryGirl.create(:jam_track, :name=>'jt_rock', genres: [JamRuby::Genre.find('rock')], make_track: true, original_artist: "badfood") } + let(:jt_blues) { FactoryGirl.create(:jam_track, :name=>'jt_blues', genres: [JamRuby::Genre.find('blues')], make_track: true, original_artist: "foodbart") } before(:all) do Capybara.javascript_driver = :poltergeist