diff --git a/db/manifest b/db/manifest index 92a320cd7..9b209f892 100755 --- a/db/manifest +++ b/db/manifest @@ -302,4 +302,5 @@ jam_track_onboarding_enhancements.sql jam_track_name_drop_unique.sql jam_track_searchability.sql harry_fox_agency.sql -jam_track_slug.sql \ No newline at end of file +jam_track_slug.sql +mixdown.sql \ No newline at end of file diff --git a/db/up/mixdown.sql b/db/up/mixdown.sql new file mode 100644 index 000000000..9a71ec7c2 --- /dev/null +++ b/db/up/mixdown.sql @@ -0,0 +1,41 @@ +CREATE TABLE jam_track_mixdowns ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + jam_track_id VARCHAR(64) NOT NULL REFERENCES jam_tracks(id) ON DELETE CASCADE, + user_id VARCHAR(64) NOT NULL REFERENCES users(id) ON DELETE CASCADE, + settings JSON NOT NULL, + name VARCHAR(1000) NOT NULL, + description VARCHAR(1000), + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE jam_track_mixdown_packages ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + jam_track_mixdown_id VARCHAR(64) NOT NULL REFERENCES jam_track_mixdowns(id) ON DELETE SET NULL, + file_type VARCHAR NOT NULL , + sample_rate INTEGER NOT NULL, + url VARCHAR(2048), + md5 VARCHAR, + length INTEGER, + downloaded_since_sign BOOLEAN NOT NULL DEFAULT FALSE, + last_signed_at TIMESTAMP, + download_count INTEGER NOT NULL DEFAULT 0, + signed_at TIMESTAMP, + downloaded_at TIMESTAMP, + signing_queued_at TIMESTAMP, + error_count INTEGER NOT NULL DEFAULT 0, + error_reason VARCHAR, + error_detail VARCHAR, + should_retry BOOLEAN NOT NULL DEFAULT FALSE, + packaging_steps INTEGER, + current_packaging_step INTEGER, + private_key VARCHAR, + signed BOOLEAN, + signing_started_at TIMESTAMP, + first_downloaded TIMESTAMP, + signing BOOLEAN NOT NULL DEFAULT FALSE, + encrypt_type VARCHAR, + version VARCHAR NOT NULL DEFAULT '1', + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); \ No newline at end of file diff --git a/pb/src/client_container.proto b/pb/src/client_container.proto index 5d5ecf100..14a8215c4 100644 --- a/pb/src/client_container.proto +++ b/pb/src/client_container.proto @@ -82,6 +82,10 @@ message ClientMessage { JAM_TRACK_SIGN_COMPLETE = 260; JAM_TRACK_SIGN_FAILED = 261; + // jamtracks mixdown notifications + MIXDOWN_SIGN_COMPLETE = 270; + MIXDOWN_SIGN_FAILED = 271; + TEST_SESSION_MESSAGE = 295; PING_REQUEST = 300; @@ -188,6 +192,10 @@ message ClientMessage { optional JamTrackSignComplete jam_track_sign_complete = 260; optional JamTrackSignFailed jam_track_sign_failed = 261; + // jamtrack mixdown notification + optional MixdownSignComplete mixdown_sign_complete = 270; + optional MixdownSignFailed mixdown_sign_failed = 271; + // Client-Session messages (to/from) optional TestSessionMessage test_session_message = 295; @@ -612,6 +620,15 @@ message JamTrackSignFailed { required int32 jam_track_right_id = 1; // jam track right id } +message MixdownSignComplete { + required int32 mixdown_package_id = 1; // jam track mixdown package id +} + +message MixdownSignFailed { + required int32 mixdown_package_id = 1; // jam track mixdown package id +} + + message SubscriptionMessage { optional string type = 1; // the type of the subscription optional string id = 2; // data about what to subscribe to, specifically diff --git a/ruby/lib/jam_ruby.rb b/ruby/lib/jam_ruby.rb index 16890a9d9..8f62cecf9 100755 --- a/ruby/lib/jam_ruby.rb +++ b/ruby/lib/jam_ruby.rb @@ -64,6 +64,7 @@ require "jam_ruby/resque/scheduled/jam_tracks_cleaner" require "jam_ruby/resque/scheduled/stats_maker" require "jam_ruby/resque/scheduled/tally_affiliates" require "jam_ruby/resque/jam_tracks_builder" +require "jam_ruby/resque/jam_track_mixdown_packager" require "jam_ruby/resque/google_analytics_event" require "jam_ruby/resque/batch_email_job" require "jam_ruby/resque/long_running" @@ -209,6 +210,8 @@ 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/jam_track_mixdown" +require "jam_ruby/models/jam_track_mixdown_package" require "jam_ruby/models/genre_jam_track" require "jam_ruby/app/mailers/async_mailer" require "jam_ruby/app/mailers/batch_mailer" diff --git a/ruby/lib/jam_ruby/constants/notification_types.rb b/ruby/lib/jam_ruby/constants/notification_types.rb index e05c8e00e..60dfe9f1b 100644 --- a/ruby/lib/jam_ruby/constants/notification_types.rb +++ b/ruby/lib/jam_ruby/constants/notification_types.rb @@ -51,4 +51,7 @@ module NotificationTypes JAM_TRACK_SIGN_COMPLETE = "JAM_TRACK_SIGN_COMPLETE" JAM_TRACK_SIGN_FAILED = "JAM_TRACK_SIGN_FAILED" + MIXDOWN_SIGN_COMPLETE = "MIXDOWN_SIGN_COMPLETE" + MIXDOWN_SIGN_FAILED = "MIXDOWN_SIGN_FAILED" + end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/lib/subscription_message.rb b/ruby/lib/jam_ruby/lib/subscription_message.rb index 6be9f7d16..dc1636b5e 100644 --- a/ruby/lib/jam_ruby/lib/subscription_message.rb +++ b/ruby/lib/jam_ruby/lib/subscription_message.rb @@ -24,6 +24,14 @@ module JamRuby def self.jam_track_signing_job_change(jam_track_right) Notification.send_subscription_message('jam_track_right', jam_track_right.id.to_s, {signing_state: jam_track_right.signing_state, current_packaging_step: jam_track_right.current_packaging_step, packaging_steps: jam_track_right.packaging_steps}.to_json ) end + + def self.mixdown_signing_job_change(jam_track_mixdown_package) + Notification.send_subscription_message('mixdown', jam_track_mixdown_package.id.to_s, {signing_state: jam_track_mixdown_package.signing_state, current_packaging_step: jam_track_mixdown_package.current_packaging_step, packaging_steps: jam_track_right.packaging_steps}.to_json ) + end + + def self.test + Notification.send_subscription_message('some_key', '1', {field1: 'field1', field2: 'field2'}.to_json) + end end end diff --git a/ruby/lib/jam_ruby/message_factory.rb b/ruby/lib/jam_ruby/message_factory.rb index e8cf40b1b..6b7e98034 100644 --- a/ruby/lib/jam_ruby/message_factory.rb +++ b/ruby/lib/jam_ruby/message_factory.rb @@ -736,6 +736,30 @@ module JamRuby ) end + def mixdown_sign_complete(receiver_id, mixdown_package_id) + signed = Jampb::MixdownSignComplete.new( + :mixdown_package_id => mixdown_package_id + ) + + Jampb::ClientMessage.new( + :type => ClientMessage::Type::MIXDOWN_SIGN_COMPLETE, + :route_to => USER_TARGET_PREFIX + receiver_id, #:route_to => CLIENT_TARGET, + :mixdown_sign_complete => signed + ) + end + + def mixdown_sign_failed(receiver_id, mixdown_package_id) + signed = Jampb::MixdownSignFailed.new( + :mixdown_package_id => mixdown_package_id + ) + + Jampb::ClientMessage.new( + :type => ClientMessage::Type::MIXDOWN_SIGN_FAILED, + :route_to => USER_TARGET_PREFIX + receiver_id, #:route_to => CLIENT_TARGET, + :mixdown_sign_failed=> signed + ) + end + def recording_master_mix_complete(receiver_id, recording_id, claimed_recording_id, band_id, msg, notification_id, created_at) recording_master_mix_complete = Jampb::RecordingMasterMixComplete.new( diff --git a/ruby/lib/jam_ruby/models/jam_track_mixdown.rb b/ruby/lib/jam_ruby/models/jam_track_mixdown.rb new file mode 100644 index 000000000..645cf96fa --- /dev/null +++ b/ruby/lib/jam_ruby/models/jam_track_mixdown.rb @@ -0,0 +1,44 @@ +module JamRuby + + # describes what users have rights to which tracks + class JamTrackMixdown < ActiveRecord::Base + + @@log = Logging.logger[JamTrackMixdown] + + belongs_to :user, class_name: "JamRuby::User" # the owner, or purchaser of the jam_track + belongs_to :jam_track, class_name: "JamRuby::JamTrack" + + validates :name, presence: true, length: {maximum: 100} + validates :description, length: {maximum: 1000} + validates :user, presence: true + validates :jam_track, presence: true + validates :settings, presence: true + + validates_uniqueness_of :user_id, scope: :jam_track_id + + validate :verify_settings + + def verify_settings + # TODO: validate settings + if false + errors.add(:settings, 'invalid settings') + end + end + + def self.create(name, user, jam_track, settings) + mixdown = JamTrackMixdown.new + mixdown.name = name + mixdown.user = user + mixdown.jam_track = jam_track + mixdown.settings = settings.to_json # RAILS 4 CAN REMOVE .to_json + mixdown.save + mixdown + end + + def will_pitch_shift? + self.settings["pitch"] != 0 || self.settings["speed"] != 0 + end + + end +end + diff --git a/ruby/lib/jam_ruby/models/jam_track_mixdown_package.rb b/ruby/lib/jam_ruby/models/jam_track_mixdown_package.rb new file mode 100644 index 000000000..00d1287a2 --- /dev/null +++ b/ruby/lib/jam_ruby/models/jam_track_mixdown_package.rb @@ -0,0 +1,217 @@ +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_OGG = 'ogg' + FILE_TYPE_AAC = 'aac' + FILE_TYPES = [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] + + 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 + validates :signing, presence: true + validates :signed, presence: true + + validate :verify_download_count + before_destroy :delete_s3_files + + + MAX_JAM_TRACK_DOWNLOADS = 1000 + + + + 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) + + package = JamTrackMixdownPackage.new + package.jam_track_mixdown = mixdown + package.file_type = file_type + package.sample_rate = sample_rate + 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 finish_errored(error_reason, error_detail) + 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 + + 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) + s3_manager.sign_url(self['url'], {:expires => expiration_time, :secure => true}) + end + + + def enqueue + begin + JamTrackMixdownPackager.where(:id => self.id).update_all(:signing_queued_at => Time.now, :signing_started_at => nil, :last_signed_at => nil) + Resque.enqueue(JamTrackMixdownPackager, self.id) + true + 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 + enqueue + true + 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 + # 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. + # TODO: base this on the settings of the mix + signing_job_run_max_time = 100 # packaging_steps * 10 + if Time.now - signing_started_at > signing_job_run_max_time + state = 'SIGNING_TIMEOUT' + elsif Time.now - last_step_at > APP_CONFIG.signing_step_max_time + state = 'SIGNING_TIMEOUT' + else + state = 'SIGNING' + end + elsif signing_queued_at + if Time.now - signing_queued_at > APP_CONFIG.signing_job_queue_max_time + state = 'QUEUED_TIMEOUT' + else + state = 'QUEUED' + end + elsif error_count > 0 + state = 'ERROR' + else + state = 'QUIET' # needs to be poked to go build + 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.select('count(id) as total, count(CASE WHEN signing THEN 1 ELSE NULL END) as signing_count').first + + stats['count'] = result['total'].to_i + stats['signing_count'] = result['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 + diff --git a/ruby/lib/jam_ruby/models/notification.rb b/ruby/lib/jam_ruby/models/notification.rb index 5b008a55f..0bfc4da4b 100644 --- a/ruby/lib/jam_ruby/models/notification.rb +++ b/ruby/lib/jam_ruby/models/notification.rb @@ -1265,6 +1265,30 @@ module JamRuby #@@mq_router.publish_to_all_clients(msg) end + def send_mixdown_sign_failed(jam_track_mixdown_package) + + notification = Notification.new + notification.jam_track_right_id = jam_track_mixdown_package.id + notification.description = NotificationTypes::MIXDOWN_SIGN_FAILED + notification.target_user_id = jam_track_mixdown_package.jam_track_mixdown.user_id + notification.save! + + msg = @@message_factory.mixdown_sign_failed(jam_track_mixdown_package.jam_track_mixdown.user_id, jam_track_mixdown_package.id) + @@mq_router.publish_to_user(jam_track_mixdown_package.jam_track_mixdown.user_id, msg) + end + + def send_mixdown_sign_complete(jam_track_mixdown_package) + + notification = Notification.new + notification.mixdown_package_id = jam_track_mixdown_package.id + notification.description = NotificationTypes::MIXDOWN_SIGN_COMPLETE + notification.target_user_id = jam_track_mixdown_package.jam_track_mixdown.user_id + notification.save! + + msg = @@message_factory.mixdown_sign_complete(jam_track_mixdown_package.jam_track_mixdown.user_id, jam_track_mixdown_package.id) + @@mq_router.publish_to_user(jam_track_mixdown_package.jam_track_mixdown.user_id, msg) + end + def send_client_update(product, version, uri, size) msg = @@message_factory.client_update( product, version, uri, size) diff --git a/ruby/lib/jam_ruby/resque/jam_track_mixdown_packager.rb b/ruby/lib/jam_ruby/resque/jam_track_mixdown_packager.rb new file mode 100644 index 000000000..3afe04816 --- /dev/null +++ b/ruby/lib/jam_ruby/resque/jam_track_mixdown_packager.rb @@ -0,0 +1,395 @@ +require 'json' +require 'resque' +require 'resque-retry' +require 'net/http' +require 'digest/md5' + +module JamRuby + class JamTracksMixdownPackager + extend JamRuby::ResqueStats + + include JamRuby::S3ManagerMixin + + + MAX_PAN = 90 + MIN_PAN = -90 + + attr_accessor :mixdown_package_id, :settings, :mixdown_package, :mixdown, :steps + @queue = :jam_track_mixdown_packager + + def log + @log || Logging.logger[JamTracksMixdownPackager] + end + + def self.perform(mixdown_package_id, bitrate=48) + jam_track_builder = JamTracksMixdownPackager.new() + jam_track_builder.mixdown_package_id = mixdown_package_id + jam_track_builder.run + end + + def compute_steps + @steps = 0 + number_downloads = @track_settings.length + number_volume_adjustments = @track_settings.select { |track| should_alter_volume? track } + + pitch_shift_steps = @mixdown.will_pitch_shift? ? 1 : 0 + mix_step = 1 + package_steps = 1 + + number_downloads + number_volume_adjustments + pitch_shift_steps + mix_steps + package_steps + end + + def run + begin + log.info("Mixdown job starting. mixdown_packager_id #{mixdown_package_id}") + begin + @mixdown_package = JamTrackMixdownPackage.find(mixdown_package_id) + + + # bailout check + if @mixdown_package.signed? + log.debug("package is already signed. bailing") + return + end + + @mixdown = @mixdown_package.jam_track_mixdown + @settings = @mixdown.settings + + track_settings + + # compute the step count + total_steps = compute_steps + + # track that it's started ( and avoid db validations ) + signing_started_at = Time.now + last_step_at = Time.now + JamTrackMixdownPackage.where(:id => @mixdown_package.id).update_all(:signing_started_at => signing_started_at, :should_retry => false, packaging_steps: total_steps, current_packaging_step: 0, last_step_at: last_step_at, :signing => true) + + # because we are skipping 'after_save', we have to keep the model current for the notification. A bit ugly... + + @mixdown_package.current_packaging_step = 0 + @mixdown_package.packaging_steps = total_steps + @mixdown_package.signing_started_at = signing_started_at + @mixdown_package.signing = true + @mixdown_package.should_retry = false + @mixdown_package.last_step_at = Time.now + + SubscriptionMessage.mixdown_signing_job_change(@mixdown_package) + + package + + log.info "Signed mixdown package to #{@mixdown_package[:url]}" + + rescue Exception => e + # record the error in the database + post_error(e) + # and let the job fail, alerting ops too + raise + end + end + end + + def should_alter_volume? track + + # short cut is possible if vol = 1.0 and pan = 0 + vol = track[:vol] + pan = track[:pan] + + vol == 1.0 && pan == 0 + end + + # creates a list of tracks to actually mix + def track_settings + altered_tracks = @settings["tracks"] || [] + + @track_settings = [] + + #void slider2Pan(int i, float *f); + + stems = @mixdown.jam_track.stem_tracks + @track_count = stems.length + + stems.each do |stem| + + # is this stem in the altered_tracks list? + altered_tracks.each do |alteration| + vol = 1.0 + pan = 0 + pitch = 0 + speed = 0 + if alteration["id"] == stem.id + if alteration["mute"] || alteration["vol"] == 0 + next + else + vol = alteration["vol"] || vol + pan = alteration["pan"] || pan + end + end + + @track_settings << {stem: stem, vol: vol, pan: pan} + end + @track_settings + end + end + + def slider_to_pan(pan) + # transpose MIN_PAN to MAX_PAN to + # 0-1.0 range + #assumes abs(MIN_PAN) == abs(MAX_PAN) + # k = f(i) = (i)/(2*MAX_PAN) + 0.5 + # so f(MIN_PAN) = -0.5 + 0.5 = 0 + + k = ((i * (1.0))/ (2.0 * MAX_PAN )) + 0.5 + l, r = 0 + + if k == 0 + l = 0.0 + r = 1.0 + else + l = Math.sqrt(k) + r = Math.sqrt(1-k) + end + + [l, r] + end + + def package + + puts @settings.inspect + puts @track_count + puts @track_settings + + Dir.mktmpdir do |tmp_dir| + + # download all files + @track_settings.each do |track| + jam_track_track = track[:stem] + + file = File.join(tmp_dir, jam_track_track.id + '.ogg') + + bump_step(@mixdown_package) + + # download each track needed + s3_manager.download(jam_track_track.url_by_sample_rate(@mixdown_package.sample_rate), file) + + + track[:file] = file + end + + audio_process tmp_dir + end + end + + def audio_process(tmp_dir) + # use sox remix to apply mute, volume, pan settings + + + # step 1: apply pan and volume per track. mute and vol of 0 has already been handled, by virtue of those tracks not being present in @track_settings + # step 2: mix all tracks into single track, dividing by constant number of jam tracks, which is same as done by client backend + # step 3: apply pitch and speed (if applicable) + # step 4: encrypt with jkz (if applicable) + + apply_vol_and_pan tmp_dir + + mix tmp_dir + + pitch_speed tmp_dir + + final_packaging tmp_dir + end + + # output is :volumed_file in each track in @track_settings + def apply_vol_and_pan(tmp_dir) + @track_settings.each do |track| + + jam_track_track = track[:stem] + file = track[:file] + + if should_alter_volume? track + track[:volumed_file] = file + else + pan_l, pan_r = slider_to_pan(track[:pan]) + + # short + channel_l = pan_l * vol + channel_r = pan_r * vol + + bump_step(@mixdown_package) + + # sox claps.wav claps-remixed.wav remix 1v1.0 2v1.0 + + volumed_file = File.join(tmp_dir, jam_track_track.id + '-volumed.ogg') + + cmd("sox \"#{file}\" \"#{volumed_file}\" remix 1v#{channel_l} 2v#{channel_r}") + + track[:volumed_file] = volumed_file + end + end + end + + # output is @mix_file + def mix(tmp_dir) + + bump_step(@mixdown_package) + + # sox -m will divide by number of inputs by default. But we purposefully leave out tracks that are mute/no volume (to save downloading/processing time in this job) + # so we need to tell sox to divide by how many tracks there are as a constant, because this is how the client works today + #sox -m -v 1/n file1 -v 1/n file2 out + cmd = "sox -m" + mix_divide = 1.0/@track_count + @track_settings.each do |track| + volumed_file = track[:volumed_file] + cmd << " -v #{mix_divide} \"#{volumed_file}\"" + end + + + @mix_file = File.join(tmp_dir, "mix.ogg") + + cmd << " \"#{@mix_file}\"" + cmd(cmd) + end + + + # output is @speed_mix_file + def pitch_speed tmp_dir + + # # usage + # This app will take an ogg, wav, or mp3 file (for the uploads) as its input and output an ogg file. + # Usage: + # sbsms path-to-input.ogg path-to-output.ogg TimeStrech PitchShift + + # input is @mix_file, created by mix() + # output is @speed_mix_file + + pitch = @settings['pitch'] || 0 + speed = @settings['speed'] || 0 + + # if pitch and speed are 0, we do nothing here + if pitch == 0 && speed == 0 + @speed_mix_file = @mix_file + else + bump_step(@mixdown_package) + + @speed_mix_file = File.join(tmp_dir, "speed_mix_file.ogg") + + cmd "sbms \"#{@mix_file}\" \"#{@speed_mix_file}\" #{speed} #{pitch}" + end + end + + def final_packaging tmp_dir + + bump_step(@mixdown_package) + + url = null + private_key = nil + md5 = nil + length = 0 + output = null + + if encrypted_file + output, private_key = encrypt_jkz tmp_dir + else + # create output file to correct output format + output = convert tmp_dir + end + + # upload output to S3 + s3_url = "#{@mixdown_package.store_dir}/#{@mixdown_package.filename}" + s3_manager.upload(s3_url, output) + + length = File.size(output) + computed_md5 = Digest::MD5.new + File.open(output, 'rb').each {|line| computed_md5.update(line)} + md5 = computed_md5.to_s + + @mixdown_package.finish_sign(s3_url, private_key, length, md5.to_s) + end + + # returns output destination, converting if necessary + def convert(tmp_dir) + # if the file already ends with the desired file type, call it a win + if @speed_mix_file.end_with?(@mixdown_package.file_type) + @speed_mix_file + else + # otherwise we need to convert from lastly created file to correct + output = File.join(tmp_dir, "output.#{@mixdown_pacakge.file_type}") + cmd("sox \"#{@speed_mix_file}\" \"#{output}\"") + output + end + end + + def encrypt_jkz(tmp_dir) + py_root = APP_CONFIG.jamtracks_dir + step = 0 + + jam_file_opts = "" + jam_file_opts << " -i #{Shellwords.escape("#{track_filename}+#{jam_track_track.part}")}" + + sku = @mixdown_package.id + title = @mixdown.name + output = File.join(tmp_dir, "#{title.parameterize}.jkz") + py_file = File.join(py_root, "jkcreate.py") + version = @mixdown_package.version + + @@log.info "Executing python source in #{py_file}, outputting to #{tmp_dir} (#{output})" + + cli = "python #{py_file} -D -k #{sku} -p #{Shellwords.escape(tmp_dir)}/pkey.pem -s #{Shellwords.escape(tmp_dir)}/skey.pem #{jam_file_opts} -o #{Shellwords.escape(output)} -t #{Shellwords.escape(title)} -V #{Shellwords.escape(version)}" + + Open3.popen3(cli) do |stdin, stdout, stderr, wait_thr| + pid = wait_thr.pid + exit_status = wait_thr.value + err = stderr.read(1000) + out = stdout.read(1000) + #puts "stdout: #{out}, stderr: #{err}" + raise ArgumentError, "Error calling python script: #{err}" if err.present? + raise ArgumentError, "Error calling python script: #{out}" if out && (out.index("No track files specified") || out.index("Cannot find file")) + + private_key = File.read("#{tmp_dir}/skey.pem") + end + return output, private_key + end + + def cmd(cmd) + + log.debug("executing #{cmd}") + + output = `#{cmd}` + + result_code = $?.to_i + + if result_code == 0 + output + else + raise "command `#{cmd}` failed." + end + end + + # increment the step, which causes a notification to be sent to the client so it can keep the UI fresh as the packaging step goes on + def bump_step(mixdown_package) + step = @step + last_step_at = Time.now + mixdown_package.current_packaging_step = step + mixdown_package.last_step_at = Time.now + JamTrackMixdownPackage.where(:id => mixdown_package.id).update_all(last_step_at: last_step_at, current_packaging_step: step) + SubscriptionMessage.mixdown_signing_job_change(mixdown_package) + + @step = step + 1 + end + + # set @error_reason before you raise an exception, and it will be sent back as the error reason + # otherwise, the error_reason will be unhandled-job-exception + def post_error(e) + begin + # if error_reason is null, assume this is an unhandled error + unless @error_reason + @error_reason = "unhandled-job-exception" + @error_detail = e.to_s + end + @mixdown_package.finish_errored(@error_reason, @error_detail) + + rescue Exception => e + log.error "unable to post back to the database the error #{e}" + end + end + end +end diff --git a/ruby/lib/jam_ruby/resque/scheduled/stats_maker.rb b/ruby/lib/jam_ruby/resque/scheduled/stats_maker.rb index 6bff4192e..78a40aa6c 100644 --- a/ruby/lib/jam_ruby/resque/scheduled/stats_maker.rb +++ b/ruby/lib/jam_ruby/resque/scheduled/stats_maker.rb @@ -31,6 +31,7 @@ module JamRuby Stats.write('users', User.stats) Stats.write('sessions', ActiveMusicSession.stats) Stats.write('jam_track_rights', JamTrackRight.stats) + Stats.write('jam_track_mixdown_packages', JamTrackMixdownPackage.stats) end end diff --git a/ruby/spec/factories.rb b/ruby/spec/factories.rb index f181e00da..3ab351952 100644 --- a/ruby/spec/factories.rb +++ b/ruby/spec/factories.rb @@ -723,6 +723,18 @@ FactoryGirl.define do sequence(:phone) { |n| "phone-#{n}" } end + factory :jam_track_mixdown, :class => JamRuby::JamTrackMixdown do + association :user, factory: :user + association :jam_track, factory: :jam_track + sequence(:name) { |n| "mixdown-#{n}"} + settings '{}' + end + + factory :jam_track_mixdown_pakage, :class => JamRuby::JamTrackMixdownPackage do + association :jam_track_mixdown, factory: :jam_track_mixdown + end + + factory :jam_track, :class => JamRuby::JamTrack do sequence(:name) { |n| "jam-track-#{n}" } sequence(:description) { |n| "description-#{n}" } diff --git a/ruby/spec/jam_ruby/models/jam_track_mixdown_package_spec.rb b/ruby/spec/jam_ruby/models/jam_track_mixdown_package_spec.rb new file mode 100644 index 000000000..a152e994c --- /dev/null +++ b/ruby/spec/jam_ruby/models/jam_track_mixdown_package_spec.rb @@ -0,0 +1,18 @@ +require 'spec_helper' + +describe JamTrackMixdownPackage do + include UsesTempFiles + + it "can be created (factory girl)" do + package = FactoryGirl.create(:jam_track_mixdown_package) + end + + it "can be created" do + mixdown= FactoryGirl.create(:jam_track_mixdown_package) + + package = JamTrackMixdownPackage.create(mixdown, 'ogg', 48, true) + + package.errors.any?.should == false + end +end + diff --git a/ruby/spec/jam_ruby/models/jam_track_mixdown_spec.rb b/ruby/spec/jam_ruby/models/jam_track_mixdown_spec.rb new file mode 100644 index 000000000..d56ff8fe3 --- /dev/null +++ b/ruby/spec/jam_ruby/models/jam_track_mixdown_spec.rb @@ -0,0 +1,20 @@ +require 'spec_helper' + +describe JamTrackMixdown do + + let(:user) {FactoryGirl.create(:user)} + let(:jam_track) {FactoryGirl.create(:jam_track)} + + it "can be created (factory girl)" do + mixdown = FactoryGirl.create(:jam_track_mixdown) + + mixdown = JamTrackMixdown.find(mixdown.id) + mixdown.settings.should eq('{}') + end + + it "can be created" do + mixdown = JamTrackMixdown.create('abc', user, jam_track, {}) + mixdown.errors.any?.should == false + end +end +