246 lines
7.8 KiB
Ruby
246 lines
7.8 KiB
Ruby
module JamRuby
|
|
class Mix < ActiveRecord::Base
|
|
include S3ManagerMixin
|
|
|
|
MAX_MIX_TIME = 7200 # 2 hours
|
|
|
|
before_destroy :delete_s3_files
|
|
|
|
self.primary_key = 'id'
|
|
|
|
attr_accessible :ogg_url, :should_retry, as: :admin
|
|
attr_accessor :is_skip_mount_uploader
|
|
attr_writer :current_user
|
|
|
|
belongs_to :recording, :class_name => "JamRuby::Recording", :inverse_of => :mixes, :foreign_key => 'recording_id'
|
|
|
|
validates :download_count, presence: true
|
|
validate :verify_download_count
|
|
|
|
skip_callback :save, :before, :store_picture!, if: :is_skip_mount_uploader
|
|
|
|
mount_uploader :ogg_url, MixUploader
|
|
|
|
|
|
def verify_download_count
|
|
if (self.download_count < 0 || self.download_count > APP_CONFIG.max_audio_downloads) && !@current_user.admin
|
|
errors.add(:download_count, "must be less than or equal to 100")
|
|
end
|
|
end
|
|
|
|
before_validation do
|
|
# this should be an activeadmin only path, because it's using the mount_uploader (whereas the client does something completely different)
|
|
if !is_skip_mount_uploader && ogg_url.present? && ogg_url.respond_to?(:file) && ogg_url_changed?
|
|
self.ogg_length = ogg_url.file.size
|
|
self.ogg_md5 = ogg_url.md5
|
|
self.completed = true
|
|
self.started_at = Time.now
|
|
self.completed_at = Time.now
|
|
# do not set marking_complete = true; use of marking_complete is a client-centric design,
|
|
# and setting to true causes client-centric validations
|
|
end
|
|
end
|
|
|
|
|
|
def self.schedule(recording)
|
|
raise if recording.nil?
|
|
|
|
mix = Mix.new
|
|
mix.is_skip_mount_uploader = true
|
|
mix.recording = recording
|
|
mix.save
|
|
mix[:ogg_url] = construct_filename(recording.created_at, recording.id, mix.id, type='ogg')
|
|
mix[:mp3_url] = construct_filename(recording.created_at, recording.id, mix.id, type='mp3')
|
|
if mix.save
|
|
mix.enqueue
|
|
end
|
|
mix.is_skip_mount_uploader = false
|
|
|
|
mix
|
|
end
|
|
|
|
def enqueue
|
|
begin
|
|
Resque.enqueue(AudioMixer, self.id, self.sign_put(3600 * 24, 'ogg'), self.sign_put(3600 * 24, 'mp3'))
|
|
rescue
|
|
# implies redis is down. we don't update started_at
|
|
false
|
|
end
|
|
|
|
# avoid db validations
|
|
Mix.where(:id => self.id).update_all(:started_at => Time.now, :should_retry => false)
|
|
|
|
true
|
|
end
|
|
|
|
def can_download?(some_user)
|
|
claimed_recording = ClaimedRecording.find_by_user_id_and_recording_id(some_user.id, recording.id)
|
|
|
|
if claimed_recording
|
|
!claimed_recording.discarded
|
|
else
|
|
false
|
|
end
|
|
end
|
|
|
|
def mix_timeout?
|
|
Time.now - started_at > 60 * 30 # 30 minutes to mix is more than enough
|
|
end
|
|
|
|
def state
|
|
return 'mixed' if completed
|
|
return 'stream-mix' if recording.has_stream_mix
|
|
return 'waiting-to-mix' if started_at.nil?
|
|
return 'error' if error_count > 0 || mix_timeout?
|
|
return 'mixing'
|
|
end
|
|
|
|
def error
|
|
return nil if state != 'error'
|
|
return {error_count: error_count, error_reason: error_reason, error_detail: error_detail} if error_count > 0
|
|
return {error_count: 1, error_reason: 'mix-timeout', error_detail: started_at} if mix_timeout?
|
|
return {error_count: 1, error_reason: 'unknown', error_detail: 'unknown'}
|
|
end
|
|
|
|
def too_many_downloads?
|
|
(self.download_count < 0 || self.download_count > APP_CONFIG.max_audio_downloads) && !@current_user.admin
|
|
end
|
|
|
|
def errored(reason, detail)
|
|
self.started_at = nil
|
|
self.error_reason = reason
|
|
self.error_detail = detail
|
|
self.error_count = self.error_count + 1
|
|
if self.error_count <= 3
|
|
self.should_retry = true
|
|
end
|
|
|
|
save
|
|
end
|
|
|
|
def finish(ogg_length, ogg_md5, mp3_length, mp3_md5)
|
|
self.completed_at = Time.now
|
|
self.ogg_length = ogg_length
|
|
self.ogg_md5 = ogg_md5
|
|
self.mp3_length = mp3_length
|
|
self.mp3_md5 = mp3_md5
|
|
self.completed = true
|
|
if save
|
|
Notification.send_recording_master_mix_complete(recording)
|
|
end
|
|
|
|
Recording.where(:id => self.recording.id).update_all(:has_final_mix => true)
|
|
end
|
|
|
|
# valid for 1 day; because the s3 urls eventually expire
|
|
def manifest
|
|
one_day = 60 * 60 * 24
|
|
|
|
manifest = { "files" => [], "timeline" => [] }
|
|
mix_params = []
|
|
|
|
recording.recorded_tracks.each do |recorded_track|
|
|
manifest["files"] << { "filename" => recorded_track.sign_url(one_day), "codec" => "vorbis", "offset" => 0 }
|
|
mix_params << { "level" => 1.0, "balance" => 0 }
|
|
end
|
|
|
|
recording.recorded_backing_tracks.each do |recorded_backing_track|
|
|
manifest["files"] << { "filename" => recorded_backing_track.sign_url(one_day), "codec" => "vorbis", "offset" => 0 }
|
|
mix_params << { "level" => 1.0, "balance" => 0 }
|
|
end
|
|
|
|
recording.recorded_jam_track_tracks.each do |recorded_jam_track_track|
|
|
manifest["files"] << { "filename" => recorded_jam_track_track.jam_track_track.sign_url(one_day), "codec" => "vorbis", "offset" => 0 }
|
|
# let's look for level info from the client
|
|
level = 1.0 # default value - means no effect
|
|
if recorded_jam_track_track.timeline
|
|
|
|
timeline_data = JSON.parse(recorded_jam_track_track.timeline)
|
|
|
|
# always take the 1st entry for now
|
|
first = timeline_data[0]
|
|
|
|
if first["mute"]
|
|
# mute equates to no noise
|
|
level = 0.0
|
|
else
|
|
# otherwise grab the left channel...
|
|
level = first["vol_l"]
|
|
end
|
|
end
|
|
|
|
mix_params << { "level" => level, "balance" => 0 }
|
|
end
|
|
|
|
manifest["timeline"] << { "timestamp" => 0, "mix" => mix_params }
|
|
manifest["output"] = { "codec" => "vorbis" }
|
|
manifest["recording_id"] = self.recording.id
|
|
manifest
|
|
end
|
|
|
|
def s3_url(type='ogg')
|
|
if type == 'ogg'
|
|
s3_manager.s3_url(self[:ogg_url])
|
|
else
|
|
s3_manager.s3_url(self[:mp3_url])
|
|
end
|
|
|
|
|
|
end
|
|
|
|
def is_completed
|
|
completed
|
|
end
|
|
|
|
# if the url starts with http, just return it because it's in some other store. Otherwise it's a relative path in s3 and needs be signed
|
|
def resolve_url(url_field, mime_type, expiration_time)
|
|
self[url_field].start_with?('http') ? self[url_field] : s3_manager.sign_url(self[url_field], {:expires => expiration_time, :response_content_type => mime_type, :secure => false})
|
|
end
|
|
|
|
def sign_url(expiration_time = 120, type='ogg')
|
|
type ||= 'ogg'
|
|
# expire link in 1 minute--the expectation is that a client is immediately following this link
|
|
if type == 'ogg'
|
|
resolve_url(:ogg_url, 'audio/ogg', expiration_time)
|
|
else
|
|
resolve_url(:mp3_url, 'audio/mpeg', expiration_time)
|
|
end
|
|
end
|
|
|
|
def sign_put(expiration_time = 3600 * 24, type='ogg')
|
|
type ||= 'ogg'
|
|
if type == 'ogg'
|
|
s3_manager.sign_url(self[:ogg_url], {:expires => expiration_time, :content_type => 'audio/ogg', :secure => false}, :put)
|
|
else
|
|
s3_manager.sign_url(self[:mp3_url], {:expires => expiration_time, :content_type => 'audio/mpeg', :secure => false}, :put)
|
|
end
|
|
end
|
|
|
|
|
|
def filename(type='ogg')
|
|
# construct a path for s3
|
|
Mix.construct_filename(recording.created_at, self.recording_id, self.id, type)
|
|
end
|
|
|
|
def update_download_count(count=1)
|
|
self.download_count = self.download_count + count
|
|
self.last_downloaded_at = Time.now
|
|
end
|
|
|
|
|
|
def delete_s3_files
|
|
s3_manager.delete(filename(type='ogg')) if self[:ogg_url] && s3_manager.exists?(filename(type='ogg'))
|
|
s3_manager.delete(filename(type='mp3')) if self[:mp3_url] && s3_manager.exists?(filename(type='mp3'))
|
|
end
|
|
|
|
private
|
|
|
|
|
|
|
|
def self.construct_filename(created_at, recording_id, id, type='ogg')
|
|
raise "unknown ID" unless id
|
|
"recordings/#{created_at.strftime('%m-%d-%Y')}/#{recording_id}/mix-#{id}.#{type}"
|
|
end
|
|
end
|
|
end
|