module JamRuby # describes what users have rights to which tracks class JamTrackMixdownPackage < ActiveRecord::Base include JamRuby::S3ManagerMixin @@log = Logging.logger[JamTrackMixdownPackage] # these are used as extensions for the files stored in s3 FILE_TYPE_MP3 = 'mp3' FILE_TYPE_OGG = 'ogg' FILE_TYPE_AAC = 'aac' FILE_TYPES = [FILE_TYPE_MP3, FILE_TYPE_OGG, FILE_TYPE_AAC] SAMPLE_RATE_44 = 44 SAMPLE_RATE_48 = 48 SAMPLE_RATES = [SAMPLE_RATE_44, SAMPLE_RATE_48] ENCRYPT_TYPE_JKZ = 'jkz' ENCRYPT_TYPES = [ENCRYPT_TYPE_JKZ, nil] default_scope { order('created_at desc') } belongs_to :jam_track_mixdown, class_name: "JamRuby::JamTrackMixdown", dependent: :destroy validates :jam_track_mixdown, presence: true validates :file_type, inclusion: {in: FILE_TYPES} validates :sample_rate, inclusion: {in: SAMPLE_RATES} validates :encrypt_type, inclusion: {in: ENCRYPT_TYPES} validates_uniqueness_of :file_type, scope: [:sample_rate, :encrypt_type, :jam_track_mixdown_id] validates :signing, inclusion: {in: [true, false]} validates :signed, inclusion: {in: [true, false]} validate :verify_download_count before_destroy :delete_s3_files after_save :after_save MAX_JAM_TRACK_DOWNLOADS = 1000 def self.estimated_queue_time jam_track_signing_count = JamTrackRight.where(queued: true).count #mixdowns = JamTrackMixdownPackage.unscoped.select('count(CASE WHEN queued THEN 1 ELSE NULL END) as queue_count, count(CASE WHEN speed_pitched THEN 1 ELSE NULL END) as speed_pitch_count').where(queued: true).limit(1) mixdowns = ActiveRecord::Base.connection.execute("select count(CASE WHEN queued THEN 1 ELSE NULL END) as queue_count, count(CASE WHEN speed_pitched THEN 1 ELSE NULL END) as speed_pitch_count FROM jam_track_mixdown_packages WHERE queued = true")[0] total_mixdowns = mixdowns['queue_count'].to_i slow_mixdowns = mixdowns['speed_pitch_count'].to_i fast_mixdowns = total_mixdowns - slow_mixdowns guess = APP_CONFIG.estimated_jam_track_time * jam_track_signing_count + APP_CONFIG.estimated_fast_mixdown_time * fast_mixdowns + APP_CONFIG.estimated_slow_mixdown_time * slow_mixdowns Stats.write('web.jam_track.queue_time', {value: guess / 60.0, jam_tracks: jam_track_signing_count, slow_mixdowns: slow_mixdowns, fast_mixdowns: fast_mixdowns}) guess end def after_save # try to catch major transitions: # if just queue time changes, start time changes, or signed time changes, send out a notice if signing_queued_at_was != signing_queued_at || signing_started_at_was != signing_started_at || last_signed_at_was != last_signed_at || current_packaging_step != current_packaging_step_was || packaging_steps != packaging_steps_was SubscriptionMessage.mixdown_signing_job_change(self) end end def self.create(mixdown, file_type, sample_rate, encrypt_type) package = JamTrackMixdownPackage.new package.speed_pitched = mixdown.will_pitch_shift? package.jam_track_mixdown = mixdown package.file_type = file_type package.sample_rate = sample_rate package.signed = false package.signing = false package.encrypt_type = encrypt_type package.save package end def verify_download_count if (self.download_count < 0 || self.download_count > MAX_JAM_TRACK_DOWNLOADS) && !@current_user.admin errors.add(:download_count, "must be less than or equal to #{MAX_JAM_TRACK_DOWNLOADS}") end end def is_pitch_speed_shifted? mix_settings = JSON.parse(self.settings) mix_settings["speed"] || mix_settings["pitch"] end def finish_errored(error_reason, error_detail) self.last_errored_at = Time.now self.last_signed_at = Time.now self.error_count = self.error_count + 1 self.error_reason = error_reason self.error_detail = error_detail self.should_retry = self.error_count < 5 self.signing = false self.signing_queued_at = nil # if left set, throws off signing_state on subsequent signing attempts if save Notification.send_mixdown_sign_failed(self) else raise "Error sending notification #{self.errors}" end end def finish_sign(url, private_key, length, md5) self.url = url self.private_key = private_key self.signing_queued_at = nil # if left set, throws off signing_state on subsequent signing attempts self.downloaded_since_sign = false self.last_signed_at = Time.now self.length = length self.md5 = md5 self.signed = true self.signing = false self.error_count = 0 self.error_reason = nil self.error_detail = nil self.should_retry = false save! end def store_dir "jam_track_mixdowns/#{created_at.strftime('%m-%d-%Y')}/#{self.jam_track_mixdown.user_id}" end def filename if encrypt_type "#{id}.#{encrypt_type}" else "#{id}.#{file_type}" end 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, content_type = nil, response_content_disposition = nil) options = {:expires => expiration_time, :secure => true} options[:response_content_type] = content_type if content_type options[:response_content_disposition] = response_content_disposition if response_content_disposition s3_manager.sign_url(self['url'], options) end def enqueue begin self.signing_queued_at = Time.now self.signing_started_at = nil self.last_signed_at = nil self.queued = true self.save queue_time = JamTrackMixdownPackage.estimated_queue_time # is_pitch_speed_shifted? Resque.enqueue(JamTrackMixdownPackager, self.id) return queue_time rescue Exception => e puts "e: #{e}" # implies redis is down. we don't update started_at by bailing out here false end end # if the job is already signed, just queued up for signing, or currently signing, then don't enqueue... otherwise fire it off def enqueue_if_needed state = signing_state if state == 'SIGNED' || state == 'SIGNING' || state == 'QUEUED' false else return enqueue end end def ready? self.signed && self.url.present? end # returns easy to digest state field # SIGNED - the package is ready to be downloaded # ERROR - the package was built unsuccessfully # SIGNING_TIMEOUT - the package was kicked off to be signed, but it seems to have hung # SIGNING - the package is currently signing # QUEUED_TIMEOUT - the package signing job (JamTrackBuilder) was queued, but never executed # QUEUED - the package is queued to sign # QUIET - the jam_track_right exists, but no job has been kicked off; a job needs to be enqueued def signing_state state = nil if signed state = 'SIGNED' elsif signing_started_at && signing # the maximum amount of time the packaging job can take is 10 seconds * num steps. For a 10 track song, this will be 110 seconds. It's a bit long. if Time.now - signing_started_at > APP_CONFIG.signing_job_signing_max_time state = 'SIGNING_TIMEOUT' elsif Time.now - last_step_at > APP_CONFIG.mixdown_step_max_time state = 'SIGNING_TIMEOUT' else state = 'SIGNING' end elsif signing_queued_at if Time.now - signing_queued_at > APP_CONFIG.mixdown_job_queue_max_time state = 'QUEUED_TIMEOUT' else state = 'QUEUED' end elsif error_count > 0 state = 'ERROR' else if Time.now - created_at > 60 # it should not take more than a minute to get QUIET out state = 'QUIET_TIMEOUT' else state = 'QUIET' # needs to be poked to go build end end state end def signed? signed end def update_download_count(count=1) self.download_count = self.download_count + count self.last_downloaded_at = Time.now if self.signed self.downloaded_since_sign = true end end def self.stats stats = {} result = JamTrackMixdownPackage.unscoped.select('count(id) as total, count(CASE WHEN signing THEN 1 ELSE NULL END) as signing_count') stats['count'] = result[0]['total'].to_i stats['signing_count'] = result[0]['signing_count'].to_i stats end def delete_s3_files s3_manager.delete(self.url) if self.url && s3_manager.exists?(self.url) end end end