# -*- coding: utf-8 -*- module JamRuby class JamTrack < ActiveRecord::Base include JamRuby::S3ManagerMixin TIME_SIGNATURES = %w{4/4 3/4 2/4 6/8 5/8'} STATUS = %w{Staging Production Retired} RECORDING_TYPE = %w{Cover Original} PRO = %w{ASCAP BMI SESAC} SALES_REGION = ['United States', 'Worldwide'] PRODUCT_TYPE = 'JamTrack' @@log = Logging.logger[JamTrack] attr_accessor :uploading_preview attr_accessible :name, :description, :bpm, :time_signature, :status, :recording_type, :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, :genre_ids, :version, :jmep_json, :jmep_text, :pro_ascap, :pro_bmi, :pro_sesac, :duration, :server_fixation_date, :hfa_license_status, :hfa_license_desired, :alternative_license_status, :hfa_license_number, :hfa_song_code, :album_title, :year, as: :admin validates :name, presence: true, length: {maximum: 200} validates :plan_code, presence: true, uniqueness: true, length: {maximum: 50 } validates :description, length: {maximum: 1000} validates :time_signature, inclusion: {in: [nil] + [''] + TIME_SIGNATURES} # the empty string is needed because of activeadmin validates :status, inclusion: {in: [nil] + STATUS} validates :recording_type, inclusion: {in: [nil] + RECORDING_TYPE} validates :original_artist, length: {maximum: 200} validates :songwriter, length: {maximum: 1000} validates :publisher, length: {maximum: 1000} validates :sales_region, inclusion: {in: [nil] + SALES_REGION} validates_format_of :price, with: /^\d+\.*\d{0,2}$/ validates :version, presence: true validates :pro_ascap, inclusion: {in: [true, false]} validates :pro_bmi, inclusion: {in: [true, false]} validates :pro_sesac, inclusion: {in: [true, false]} validates :public_performance_royalty, inclusion: {in: [nil, true, false]} validates :reproduction_royalty, inclusion: {in: [nil, true, false]} validates :public_performance_royalty, inclusion: {in: [nil, true, false]} validates :duration, numericality: {only_integer: true}, :allow_nil => true validates :hfa_license_status, inclusion: {in: [true, false]} validates :hfa_license_desired, inclusion: {in: [true, false]} validates :alternative_license_status, inclusion: {in: [true, false]} validates :hfa_license_number, numericality: {only_integer: true}, :allow_nil => true validates :hfa_song_code, length: {maximum: 200} validates :album_title, length: {maximum: 200} validates :slug, uniqueness: true 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 :licensor , class_name: 'JamRuby::JamTrackLicensor', foreign_key: 'licensor_id', :inverse_of => :jam_tracks has_many :genres_jam_tracks, :class_name => "JamRuby::GenreJamTrack", :foreign_key => "jam_track_id", inverse_of: :jam_track 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" # ' has_many :owners, :through => :jam_track_rights, :class_name => "JamRuby::User", :source => :user has_many :playing_sessions, :class_name => "JamRuby::ActiveMusicSession" has_many :recordings, :class_name => "JamRuby::Recording" # VRFS-2916 jam_tracks.id is varchar: REMOVE # has_many :plays, :class_name => "JamRuby::PlayablePlay", :foreign_key => :jam_track_id, :dependent => :destroy # VRFS-2916 jam_tracks.id is varchar: ADD has_many :plays, :class_name => "JamRuby::PlayablePlay", :as => :playable, :dependent => :destroy # when we know what JamTrack this refund is related to, these are associated belongs_to :recurly_transactions, class_name: 'JamRuby::RecurlyTransactionWebHook' 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 increment_version! self.version = version.to_i + 1 save! end 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 sale_display "JamTrack: " + name end def duplicate_positions? counter = {} jam_track_tracks.each do |track| count = counter[track.position] if count.nil? count = 0 end counter[track.position] = count + 1 end duplicate = false counter.each do|position, count| if count > 1 duplicate = true break end end 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| unless track.has_preview? missing_preview = true break end end missing_preview end def onboard_warnings warnings = [] warnings << 'POSITIONS' if duplicate_positions? warnings << 'PREVIEWS'if missing_previews? warnings << 'DURATION' if duration.nil? warnings << 'JMEP' if jmep_json.blank? warnings.join(',') end def band_jam_track_count JamTrack.where(original_artist: original_artist).count end class << self # @return array[artist_name(string)] def all_artists JamTrack.select("original_artist"). group("original_artist"). order('original_artist'). collect{|jam_track|jam_track.original_artist} end # @return array[JamTrack] for given artist_name def tracks_for_artist(artist_name) JamTrack.where("original_artist=?", artist_name).all end # special case of index def autocomplete(options, user) if options[:match].blank? return {artists: [], songs: []} end options[:show_purchased_only] = options[:show_purchased_only] options[:limit] = options[:limit] || 5 options[:artist_search] = options[:match] artists, pager = artist_index(options, user) options.delete(:artist_search) options[:song_search] = options[:match] options[:sort_by] = 'jamtrack' songs, pager = index(options, user) {artists: artists, songs:songs} end def index(options, user) if options[:page] page = options[:page].to_i per_page = options[:per_page].to_i if per_page == 0 # try and see if limit was specified limit = options[:limit] limit ||= 20 limit = limit.to_i per_page = limit else limit = per_page end start = (page -1 )* per_page else limit = options[:limit] limit ||= 20 limit = limit.to_i start = options[:start].presence start = start.to_i || 0 page = 1 + start/limit per_page = limit end query = JamTrack.joins(:jam_track_tracks) .paginate(page: page, per_page: per_page) if options[:show_purchased_only] query = query.joins(:jam_track_rights) query = query.where("jam_track_rights.user_id = ?", user.id) end if options[:search] tsquery = Search.create_tsquery(options[:search]) if tsquery query = query.where("(search_tsv @@ to_tsquery('jamenglish', ?))", tsquery) end end if options[:artist_search] tsquery = Search.create_tsquery(options[:artist_search]) if tsquery query = query.where("(artist_tsv @@ to_tsquery('jamenglish', ?))", tsquery) end end if options[:song_search] tsquery = Search.create_tsquery(options[:song_search]) if tsquery query = query.where("(name_tsv @@ to_tsquery('jamenglish', ?))", tsquery) end end if options[:artist].present? query = query.where("original_artist=?", options[:artist]) end if options[:song].present? query = query.where("name=?", options[:song]) end if options[:id].present? query = query.where("jam_tracks.id=?", options[:id]) 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") query = query.group("original_artist") query = query.order('jam_tracks.original_artist') else query = query.group("jam_tracks.id") if options[:sort_by] == 'jamtrack' query = query.order('jam_tracks.name') else query = query.order('jam_tracks.original_artist, jam_tracks.name') end end query = query.where("jam_tracks.status = ?", 'Production') unless user && user.admin 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 = 'Track'") unless options[:instrument].blank? query = query.where("jam_tracks.sales_region = '#{options[:availability]}'") unless options[:availability].blank? # FIXME: n+1 queries for rights and genres # query = query.includes([{ jam_track_tracks: :instrument }, # :jam_track_tap_ins, # :jam_track_rights, # :genres]) # { genres_jam_tracks: :genre }, # query = query.includes([{ jam_track_tracks: :instrument }, # { genres_jam_tracks: :genre }]) count = query.total_entries if count == 0 [query, nil, count] elsif query.length < limit [query, nil, count] else [query, start + limit, count] end end # provides artist names and how many jamtracks are available for each def artist_index(options, user) if options[:page] page = options[:page].to_i per_page = options[:per_page].to_i if per_page == 0 # try and see if limit was specified limit = options[:limit] limit ||= 100 limit = limit.to_i else limit = per_page end start = (page -1 )* per_page limit = per_page else limit = options[:limit] limit ||= 100 limit = limit.to_i start = options[:start].presence start = start.to_i || 0 page = 1 + start/limit per_page = limit end query = JamTrack.paginate(page: page, per_page: per_page) query = query.select("original_artist, count(original_artist) AS song_count") query = query.group("original_artist") query = query.order('jam_tracks.original_artist') query = query.where("jam_tracks.status = ?", 'Production') unless user.admin if options[:show_purchased_only] query = query.joins(:jam_track_rights) query = query.where("jam_track_rights.user_id = ?", user.id) end if options[:artist_search] tsquery = Search.create_tsquery(options[:artist_search]) if tsquery query = query.where("(artist_tsv @@ to_tsquery('jamenglish', ?))", tsquery) end end 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? if query.length == 0 [query, nil] elsif query.length < limit [query, nil] else [query, start + limit] end end end def click_track_file JamTrackFile.where(jam_track_id: self.id).where(file_type: 'ClickWav').first end def click_track JamTrackTrack.where(jam_track_id: self.id).where(track_type: 'Click').first end def has_count_in? has_count_in = false if jmep_json jmep = JSON.parse(jmep_json) if jmep["Events"] events = jmep["Events"] metronome = nil events.each do |event| if event.has_key?("metronome") metronome = event["metronome"] break end end if metronome has_count_in = true end end end has_count_in end def master_track JamTrackTrack.where(jam_track_id: self.id).where(track_type: 'Master').first end def stem_tracks JamTrackTrack.where(jam_track_id: self.id).where("track_type = 'Track' or track_type = 'Click'") end def can_download?(user) owners.include?(user) end def right_for_user(user) jam_track_rights.where("user_id=?", user).first end def mixdowns_for_user(user) JamTrackMixdown.where(user_id: user.id).where(jam_track_id: self.id) end def short_plan_code prefix = 'jamtrack-' plan_code[prefix.length..-1] end # http://stackoverflow.com/questions/4308377/ruby-post-title-to-slug def sluggarize(field) field.downcase.strip.gsub(' ', '-').gsub(/[^\w-]/, '') end def generate_slug self.slug = sluggarize(original_artist) + '-' + sluggarize(name) if licensor && licensor.slug.present? #raise "no slug on licensor #{licensor.id}" if licensor.slug.nil? self.slug << "-" + licensor.slug end end def gen_plan_code # remove all non-alphanumeric chars from artist as well as name artist_code = original_artist.gsub(/[^0-9a-z]/i, '').downcase name_code = name.gsub(/[^0-9a-z]/i, '').downcase self.plan_code = "jamtrack-#{artist_code[0...20]}-#{name_code}" if licensor && licensor.slug raise "no slug on licensor #{licensor.id}" if licensor.slug.nil? self.plan_code << "-" + licensor.slug end self.plan_code = self.plan_code[0...50] # make sure it's a max of 50 long end def to_s "#{self.name} (#{self.original_artist})" end def self.latestPurchase(user_id) jtx_created = JamTrackRight .select('created_at') .where(user_id: user_id) .order('created_at DESC') .limit(1) jtx_created.first.created_at.to_i end attr_accessor :preview_generate_error before_save :jmep_json_generate validate :jmep_text_validate def jmep_text_validate begin JmepManager.execute(self.jmep_text) rescue ArgumentError => err errors.add(:jmep_text, err.to_s) end end def jmep_json_generate self.licensor_id = nil if self.licensor_id == '' self.jmep_json = nil if self.jmep_json == '' self.time_signature = nil if self.time_signature == '' begin self[:jmep_json] = JmepManager.execute(self.jmep_text) rescue ArgumentError => err #errors.add(:jmep_text, err.to_s) end end # used in mobile simulate purchase def self.forsale(user) sql =<