* VRFS-3519 - client and server jamtrack mixdown support

This commit is contained in:
Seth Call 2015-09-21 20:01:39 -05:00
parent 30965c6351
commit 4a647b8bd1
69 changed files with 3289 additions and 132 deletions

View File

@ -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
jam_track_slug.sql
mixdown.sql

61
db/up/mixdown.sql Normal file
View File

@ -0,0 +1,61 @@
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 CASCADE,
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_step_at TIMESTAMP,
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,
first_downloaded_at TIMESTAMP,
last_downloaded_at TIMESTAMP,
version VARCHAR NOT NULL DEFAULT '1',
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
ALTER TABLE jam_track_rights ADD COLUMN last_mixdown_id VARCHAR(64) REFERENCES jam_track_mixdowns(id) ON DELETE SET NULL;
ALTER TABLE notifications ADD COLUMN jam_track_mixdown_package_id VARCHAR(64) REFERENCES jam_track_mixdown_packages(id) ON DELETE CASCADE;
ALTER TABLE jam_track_mixdown_packages ADD COLUMN last_errored_at TIMESTAMP;
ALTER TABLE jam_track_mixdown_packages ADD COLUMN queued BOOLEAN DEFAULT FALSE;
ALTER TABLE jam_track_mixdown_packages ADD COLUMN speed_pitched BOOLEAN DEFAULT FALSE;
ALTER TABLE jam_track_rights ADD COLUMN queued BOOLEAN DEFAULT FALSE;
CREATE INDEX jam_track_rights_queued ON jam_track_rights(queued);
CREATE INDEX jam_track_rights_signing_queued ON jam_track_rights(signing_queued_at);
CREATE INDEX jam_track_rights_updated ON jam_track_rights(updated_at);
CREATE INDEX jam_track_mixdown_packages_queued ON jam_track_mixdown_packages(queued);
CREATE INDEX jam_track_mixdown_packages_signing_queued ON jam_track_mixdown_packages(signing_queued_at);
CREATE INDEX jam_track_mixdown_packages_updated ON jam_track_mixdown_packages(updated_at);

View File

@ -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 string mixdown_package_id = 1; // jam track mixdown package id
}
message MixdownSignFailed {
required string 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

View File

@ -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"

View File

@ -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

View File

@ -444,7 +444,10 @@ module JamRuby
jam_track.alternative_license_status = false
jam_track.hfa_license_desired = true
jam_track.server_fixation_date = Time.now
jam_track.slug = metadata['slug'] || jam_track.generate_slug
jam_track.slug = metadata['slug']
unless jam_track.slug
jam_track.generate_slug
end
if is_tency_storage?
jam_track.vendor_id = metadata[:id]
@ -1758,7 +1761,7 @@ module JamRuby
end
end
def synchronize_all(options)
def synchronize_all(options)
importers = []
count = 0

View File

@ -14,15 +14,29 @@ module JamRuby
end
def self.mount_source_up_requested(mount)
Notification.send_subscription_message('mount', mount.id, {change_type: IcecastSourceChange::CHANGE_TYPE_MOUNT_UP_REQUEST}.to_json )
Notification.send_subscription_message('mount', mount.id, {change_type: IcecastSourceChange::CHANGE_TYPE_MOUNT_UP_REQUEST}.to_json)
end
def self.mount_source_down_requested(mount)
Notification.send_subscription_message('mount', mount.id, {change_type: IcecastSourceChange::CHANGE_TYPE_MOUNT_DOWN_REQUEST}.to_json )
Notification.send_subscription_message('mount', mount.id, {change_type: IcecastSourceChange::CHANGE_TYPE_MOUNT_DOWN_REQUEST}.to_json)
end
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 )
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_mixdown_package.packaging_steps}.to_json)
end
def self.test
Notification.send_subscription_message('some_key', '1', {field1: 'field1', field2: 'field2'}.to_json)
end
end
end

View File

@ -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(

View File

@ -15,7 +15,7 @@ module JamRuby
before_validation(:on => :create) do
self.created_at ||= Time.now
self.id = SecureRandom.uuid
self.uri = "dump/#{self.id}-#{self.created_at.to_i}"
self.uri = "dump/#{created_at.strftime('%Y-%m-%d')}/#{self.id}"
end
def user_email

View File

@ -438,6 +438,10 @@ module JamRuby
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]
@ -450,7 +454,6 @@ module JamRuby
def generate_slug
self.slug = sluggarize(original_artist) + '-' + sluggarize(name)
puts "Self.slug #{self.slug}"
end
end

View File

@ -0,0 +1,119 @@
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"
has_many :jam_track_mixdown_packages, class_name: "JamRuby::JamTrackMixdownPackage", order: 'created_at DESC'
has_one :jam_track_right, class_name: 'JamRuby::JamTrackRight', foreign_key: 'last_mixdown_id', inverse_of: :last_mixdown
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 :name, scope: :user_id
validate :verify_settings
validate :verify_max_mixdowns
def self.index(params, user)
jam_track_id = params[:id]
limit = 20
query = JamTrackMixdown.where('jam_track_id = ?', jam_track_id).where('user_id = ?', user.id).order('created_at').paginate(page: 1, per_page: limit)
count = query.total_entries
if count == 0
[query, nil, count]
elsif query.length < limit
[query, nil, count]
else
[query, start + limit, count]
end
end
def verify_max_mixdowns
if self.jam_track && self.user && self.jam_track.mixdowns_for_user(self.user).length >= 5
errors.add(:jam_track, 'allowed 5 mixes')
end
end
def verify_settings
# the user has to specify at least at least one tweak to volume, speed, pitch, pan. otherwise there is nothing to do
parsed = JSON.parse(self.settings)
specified_track_count = parsed["tracks"] ? parsed["tracks"].length : 0
tweaked = false
all_quiet = jam_track.stem_tracks.length == 0 ? false : jam_track.stem_tracks.length == specified_track_count # we already say 'all_quiet is false' if the user did not specify as many tracks as there are on the JamTrack, because omission implies 'include this track'
if parsed["speed"]
tweaked = true
end
if parsed["pitch"]
tweaked = true
end
if parsed["tracks"]
parsed["tracks"].each do |track|
if track["mute"]
tweaked = true
end
if track["vol"] && track["vol"] != 0
tweaked = true
end
if track["pan"] && track["pan"] != 0
tweaked = true
end
# there is at least one track with volume specified.
if !track["mute"] && track["vol"] != 0
all_quiet = false
end
end
end
if all_quiet
errors.add(:settings, 'are all muted')
end
if !tweaked
errors.add(:settings, 'have nothing specified')
end
if parsed["speed"] && !parsed["speed"].is_a?(Integer)
errors.add(:settings, 'has non-integer speed')
end
if parsed["pitch"] && !parsed["pitch"].is_a?(Integer)
errors.add(:settings, 'has non-integer pitch')
end
end
def self.create(name, description, user, jam_track, settings)
mixdown = JamTrackMixdown.new
mixdown.name = name
mixdown.description = description
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

View File

@ -0,0 +1,249 @@
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]
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).first
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)
s3_manager.sign_url(self['url'], {:expires => expiration_time, :secure => true})
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

View File

@ -11,6 +11,7 @@ module JamRuby
attr_accessible :url_48, :md5_48, :length_48, :url_44, :md5_44, :length_44
belongs_to :user, class_name: "JamRuby::User" # the owner, or purchaser of the jam_track
belongs_to :jam_track, class_name: "JamRuby::JamTrack"
belongs_to :last_mixdown, class_name: 'JamRuby::JamTrackMixdown', foreign_key: 'last_mixdown_id', inverse_of: :jam_track_right
validates :user, presence: true
validates :jam_track, presence: true
@ -25,9 +26,16 @@ module JamRuby
mount_uploader :url_48, JamTrackRightUploader
mount_uploader :url_44, JamTrackRightUploader
before_destroy :delete_s3_files
before_create :create_private_keys
MAX_JAM_TRACK_DOWNLOADS = 1000
def create_private_keys
rsa_key = OpenSSL::PKey::RSA.new(1024)
key = rsa_key.to_pem()
self.private_key_44 = key
self.private_key_48 = key
end
def after_save
# try to catch major transitions:
@ -58,6 +66,7 @@ module JamRuby
def finish_errored(error_reason, error_detail, sample_rate)
self.last_signed_at = Time.now
self.queued = false
self.error_count = self.error_count + 1
self.error_reason = error_reason
self.error_detail = error_detail
@ -77,6 +86,7 @@ module JamRuby
def finish_sign(length, md5, bitrate)
self.last_signed_at = Time.now
self.queued = false
if bitrate==48
self.length_48 = length
self.md5_48 = md5
@ -112,7 +122,7 @@ module JamRuby
def enqueue(sample_rate=48)
begin
JamTrackRight.where(:id => self.id).update_all(:signing_queued_at => Time.now, :signing_started_at_44 => nil, :signing_started_at_48 => nil, :last_signed_at => nil)
JamTrackRight.where(:id => self.id).update_all(:signing_queued_at => Time.now, :signing_started_at_44 => nil, :signing_started_at_48 => nil, :last_signed_at => nil, :queued => true)
Resque.enqueue(JamTracksBuilder, self.id, sample_rate)
true
rescue Exception => e

View File

@ -14,6 +14,7 @@ module JamRuby
belongs_to :music_session, :class_name => "JamRuby::MusicSession", :foreign_key => "music_session_id"
belongs_to :recording, :class_name => "JamRuby::Recording", :foreign_key => "recording_id"
belongs_to :jam_track_right, :class_name => "JamRuby::JamTrackRight", :foreign_key => "jam_track_right_id"
belongs_to :jam_track_mixdown_package, :class_name => "JamRuby::JamTrackMixdownPackage", :foreign_key => "jam_track_mixdown_package_id"
validates :target_user, :presence => true
validates :message, length: {minimum: 1, maximum: 400}, no_profanity: true, if: :text_message?
@ -1255,7 +1256,7 @@ module JamRuby
def send_jam_track_sign_complete(jam_track_right)
notification = Notification.new
notification.jam_track_right_id = jam_track_right.id
notification.jam_track_mixdown_package = jam_track_right.id
notification.description = NotificationTypes::JAM_TRACK_SIGN_COMPLETE
notification.target_user_id = jam_track_right.user_id
notification.save!
@ -1265,6 +1266,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_mixdown_package_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.jam_track_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)

View File

@ -0,0 +1,461 @@
require 'json'
require 'resque'
require 'resque-retry'
require 'net/http'
require 'digest/md5'
module JamRuby
class JamTrackMixdownPackager
extend JamRuby::ResqueStats
include JamRuby::S3ManagerMixin
MAX_PAN = 90
MIN_PAN = -90
attr_accessor :mixdown_package_id, :settings, :mixdown_package, :mixdown, :step
@queue = :jam_track_mixdown_packager
def log
@log || Logging.logger[JamTrackMixdownPackager]
end
def self.perform(mixdown_package_id, bitrate=48)
jam_track_builder = JamTrackMixdownPackager.new()
jam_track_builder.mixdown_package_id = mixdown_package_id
jam_track_builder.run
end
def compute_steps
@step = 0
number_downloads = @track_settings.length
number_volume_adjustments = (@track_settings.select { |track| should_alter_volume? track }).length
pitch_shift_steps = @mixdown.will_pitch_shift? ? 1 : 0
mix_steps = 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 = JSON.parse(@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 = last_step_at
@mixdown_package.queued = false
@mixdown_package.save
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)
#SubscriptionMessage.mixdown_signing_job_change(@mixdown_package)
# 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|
vol = 1.0
pan = 0
match = false
skipped = false
# is this stem in the altered_tracks list?
altered_tracks.each do |alteration|
if alteration["id"] == stem.id
if alteration["mute"] || alteration["vol"] == 0
log.debug("leaving out track because muted or 0 volume #{alteration.inspect}")
skipped = true
next
else
vol = alteration["vol"] || vol
pan = alteration["pan"] || pan
end
@track_settings << {stem: stem, vol: vol, pan: pan}
match = true
break
end
end
# if we didn't deliberately skip this one, and if there was no 'match' (meaning user did not specify), then we leave this in unchanged
if !skipped && !match
@track_settings << {stem:stem, vol:vol, pan:pan}
end
end
@track_settings
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 = ((pan * (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
puts @track_settings.count
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]
unless should_alter_volume? track
track[:volumed_file] = file
else
pan_l, pan_r = slider_to_pan(track[:pan])
vol = track[:vol]
# 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_r} 2v#{channel_l}", 'vol_pan')
track[:volumed_file] = volumed_file
end
end
end
# output is @mix_file
def mix(tmp_dir)
bump_step(@mixdown_package)
@mix_file = File.join(tmp_dir, "mix.ogg")
# if there is only one track to mix, we need to skip mixing (sox will barf if you try to mix one file), but still divide by number of tracks
if @track_settings.count == 1
mix_divide = 1.0/@track_count
cmd = "sox -v #{mix_divide} \"#{@track_settings[0][:volumed_file]}\" \"#{@mix_file}\""
cmd(cmd, 'volume_adjust')
else
# 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
cmd << " \"#{@mix_file}\""
cmd(cmd, 'mix_adjust')
end
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")
# usage: sbsms infile<.wav|.aif|.mp3|.ogg> outfile<.ogg> rate[0.01:100] halfsteps[-48:48] outSampleRateInHz
sample_rate = 48000
if @mixdown_package.sample_rate != 48
sample_rate = 44100
end
# rate comes in as a percent (like 5, -5 for 5%, -5%). We need to change that to 1.05/
sbsms_speed = speed/100.0
sbsms_speed = 1.0 + sbsms_speed
sbsms_pitch = pitch
cmd( "sbsms \"#{@mix_file}\" \"#{@speed_mix_file}\" #{sbsms_speed} #{sbsms_pitch} #{sample_rate}", 'speed_pitch_shift')
end
end
def final_packaging tmp_dir
bump_step(@mixdown_package)
url = nil
private_key = nil
md5 = nil
length = 0
output = nil
if @mixdown_package.encrypt_type
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_package.file_type}")
raise 'unknown file_type' if @mixdown_package.file_type != JamTrackMixdownPackage::FILE_TYPE_AAC
cmd("ffmpeg -i \"#{@speed_mix_file}\" -c:a libfdk_aac -b:a 128k \"#{output}\"", 'convert_aac')
output
end
end
def encrypt_jkz(tmp_dir)
py_root = APP_CONFIG.jamtracks_dir
step = 0
private_key = nil
# we need to make the id of the custom mix be the name of the file (ID.ogg)
custom_mix_name = File.join(tmp_dir, "#{@mixdown.id}.ogg")
FileUtils.mv(@speed_mix_file, custom_mix_name)
jam_file_opts = ""
jam_file_opts << " -i #{Shellwords.escape("#{custom_mix_name}+mixdown")}"
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
right = @mixdown.jam_track.right_for_user(@mixdown.user)
if @mixdown_package.sample_rate == 48
private_key = right.private_key_48
else
private_key = right.private_key_44
end
unless private_key
@error_reason = 'no_private_key'
@error_detail = 'user needs to generate JamTrack for given sample rate'
raise @error_reason
end
private_key_file = File.join(tmp_dir, 'skey.pem')
File.open(private_key_file, 'w') {|f| f.write(private_key) }
log.debug("PRIVATE KEY")
log.debug(private_key)
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(private_key_file)
end
return output, private_key
end
def cmd(cmd, type)
log.debug("executing #{cmd}")
output = `#{cmd}`
result_code = $?.to_i
if result_code == 0
output
else
@error_reason = type + "_fail"
@error_detail = "#{cmd}, #{output}"
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 = last_step_at
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

View File

@ -42,7 +42,7 @@ module JamRuby
signing_started_model_symbol = bitrate == 48 ? :signing_started_at_48 : :signing_started_at_44
signing_state_symbol = bitrate == 48 ? :signing_48 : :signing_44
last_step_at = Time.now
JamTrackRight.where(:id => @jam_track_right.id).update_all(signing_started_model_symbol => signing_started_at, :should_retry => false, packaging_steps: total_steps, current_packaging_step: 0, last_step_at: last_step_at, signing_state_symbol => true)
JamTrackRight.where(:id => @jam_track_right.id).update_all(signing_started_model_symbol => signing_started_at, :should_retry => false, packaging_steps: total_steps, current_packaging_step: 0, last_step_at: last_step_at, signing_state_symbol => true, queued: false)
# because we are skipping 'after_save', we have to keep the model current for the notification. A bit ugly...
@jam_track_right.current_packaging_step = 0
@jam_track_right.packaging_steps = total_steps
@ -50,6 +50,7 @@ module JamRuby
@jam_track_right[signing_state_symbol] = true
@jam_track_right.should_retry = false
@jam_track_right.last_step_at = Time.now
@jam_track_right.queued = false
SubscriptionMessage.jam_track_signing_job_change(@jam_track_right)
JamRuby::JamTracksManager.save_jam_track_right_jkz(@jam_track_right, self.bitrate)

View File

@ -24,6 +24,11 @@ module JamRuby
def perform
# this needs more testing
# let's make sure jobs don't stay falsely queued for too long. 1 hour seems more than enough
JamTrackRight.where("queued = true AND (NOW() - signing_queued_at > '1 hour'::INTERVAL OR NOW() - updated_at > '1 hour'::INTERVAL").update_all(queued:false)
JamTrackRightMixdown.unscoped.where("queued = true AND (NOW() - signing_queued_at > '1 hour'::INTERVAL OR NOW() - updated_at > '1 hour'::INTERVAL").update_all(queued:false)
return
#JamTrackRight.ready_to_clean.each do |jam_track_right|
# log.debug("deleting files for jam_track_right #{jam_track_right.id}")

View File

@ -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

View File

@ -723,6 +723,23 @@ 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 '{"speed":5}'
end
factory :jam_track_mixdown_package, :class => JamRuby::JamTrackMixdownPackage do
file_type JamRuby::JamTrackMixdownPackage::FILE_TYPE_OGG
sample_rate 48
signing false
signed false
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}" }

View File

@ -0,0 +1,103 @@
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 = JamTrackMixdownPackage.create(mixdown, JamTrackMixdownPackage::FILE_TYPE_OGG, 48, 'jkz')
package.errors.any?.should == false
end
describe "signing_state" do
it "quiet" do
package = FactoryGirl.create(:jam_track_mixdown_package)
package.signing_state.should eq('QUIET')
end
it "signed" do
package = FactoryGirl.create(:jam_track_mixdown_package, signed: true, signing_started_at: Time.now)
package.signing_state.should eq('SIGNED')
end
it "error" do
package = FactoryGirl.create(:jam_track_mixdown_package, error_count: 1)
package.signing_state.should eq('ERROR')
end
it "signing" do
package = FactoryGirl.create(:jam_track_mixdown_package, signing:true, signing_started_at: Time.now, packaging_steps: 3, current_packaging_step:0, last_step_at:Time.now)
package.signing_state.should eq('SIGNING')
end
it "signing timeout" do
package = FactoryGirl.create(:jam_track_mixdown_package, signing: true, signing_started_at: Time.now - (APP_CONFIG.signing_job_signing_max_time + 1), packaging_steps: 3, current_packaging_step:0, last_step_at:Time.now)
package.signing_state.should eq('SIGNING_TIMEOUT')
end
it "queued" do
package = FactoryGirl.create(:jam_track_mixdown_package, signing_queued_at: Time.now)
package.signing_state.should eq('QUEUED')
end
it "signing timeout" do
package = FactoryGirl.create(:jam_track_mixdown_package, signing_queued_at: Time.now - (APP_CONFIG.mixdown_job_queue_max_time + 1))
package.signing_state.should eq('QUEUED_TIMEOUT')
end
end
describe "stats" do
it "empty" do
JamTrackMixdownPackage.stats['count'].should eq(0)
end
it "signing" do
package = FactoryGirl.create(:jam_track_mixdown_package)
JamTrackMixdownPackage.stats.should eq('count' => 1,
'signing_count' => 0)
package.signing = true
package.save!
JamTrackMixdownPackage.stats.should eq('count' => 1,
'signing_count' => 1)
end
end
describe "estimated_queue_time" do
it "succeeds with no data" do
JamTrackMixdownPackage.estimated_queue_time.should eq(0)
end
it "mixdown packages of different sorts" do
package = FactoryGirl.create(:jam_track_mixdown_package, speed_pitched: true)
JamTrackMixdownPackage.estimated_queue_time.should eq(0)
package.queued = true
package.save!
JamTrackMixdownPackage.estimated_queue_time.should eq(APP_CONFIG.estimated_slow_mixdown_time * 1)
package.speed_pitched = false
package.save!
JamTrackMixdownPackage.estimated_queue_time.should eq(APP_CONFIG.estimated_fast_mixdown_time * 1)
right = FactoryGirl.create(:jam_track_right)
JamTrackMixdownPackage.estimated_queue_time.should eq(APP_CONFIG.estimated_fast_mixdown_time * 1)
right.queued = true
right.save!
JamTrackMixdownPackage.estimated_queue_time.should eq(APP_CONFIG.estimated_fast_mixdown_time * 1 + APP_CONFIG.estimated_jam_track_time * 1)
end
end
end

View File

@ -0,0 +1,75 @@
require 'spec_helper'
describe JamTrackMixdown do
let(:user) {FactoryGirl.create(:user)}
let(:jam_track) {FactoryGirl.create(:jam_track)}
let(:settings) { {speed:5} }
it "can be created (factory girl)" do
mixdown = FactoryGirl.create(:jam_track_mixdown)
mixdown = JamTrackMixdown.find(mixdown.id)
mixdown.settings.should eq('{"speed":5}')
end
it "can be created" do
mixdown = JamTrackMixdown.create('abc', 'description', user, jam_track, settings)
mixdown.errors.any?.should == false
end
it "index" do
query, start, count = JamTrackMixdown.index({id: jam_track}, user)
query.length.should eq(0)
start.should be_nil
count.should eq(0)
mixdown = FactoryGirl.create(:jam_track_mixdown, user: user, jam_track: jam_track)
query, start, count = JamTrackMixdown.index({id: jam_track}, user)
query[0].should eq(mixdown)
start.should be_nil
count.should eq(1)
end
describe "settings" do
it "validates empty settings" do
invalid = FactoryGirl.build(:jam_track_mixdown, settings: {}.to_json)
invalid.save
invalid.errors.any?.should be_true
invalid.errors["settings"].should eq(["have nothing specified"])
end
it "validates speed numeric" do
invalid = FactoryGirl.build(:jam_track_mixdown, settings: {"speed": "5"}.to_json)
invalid.save
invalid.errors.any?.should be_true
invalid.errors["settings"].should eq(["has non-integer speed"])
end
it "validates pitch numeric" do
invalid = FactoryGirl.build(:jam_track_mixdown, settings: {"pitch": "5"}.to_json)
invalid.save
invalid.errors.any?.should be_true
invalid.errors["settings"].should eq(["has non-integer pitch"])
end
it "validates speed not-float" do
invalid = FactoryGirl.build(:jam_track_mixdown, settings: {"speed": 5.5}.to_json)
invalid.save
invalid.errors.any?.should be_true
invalid.errors["settings"].should eq(["has non-integer speed"])
end
it "validates pitch not-float" do
invalid = FactoryGirl.build(:jam_track_mixdown, settings: {"pitch": 10.5}.to_json)
invalid.save
invalid.errors.any?.should be_true
invalid.errors["settings"].should eq(["has non-integer pitch"])
end
end
end

View File

@ -29,6 +29,15 @@ describe JamTrackRight do
end
end
describe "private keys automatically created" do
it "created automatically" do
jam_track_right = FactoryGirl.create(:jam_track_right)
jam_track_right.private_key_44.should_not be_nil
jam_track_right.private_key_48.should_not be_nil
jam_track_right.private_key_44.should eq(jam_track_right.private_key_48)
end
end
describe "JKZ" do
before(:all) do
original_storage = JamTrackTrackUploader.storage = :fog
@ -109,12 +118,14 @@ describe JamTrackRight do
end
it "valid track with rights to it by querying user" do
jam_track_right = FactoryGirl.create(:jam_track_right, private_key_44: 'keyabc')
jam_track_right = FactoryGirl.create(:jam_track_right)
keys = JamTrackRight.list_keys(jam_track_right.user, [jam_track_right.jam_track.id])
keys.should have(1).items
keys[0].id.should == jam_track_right.jam_track.id
keys[0]['private_key_44'].should eq('keyabc')
keys[0]['private_key_48'].should be_nil
keys[0]['private_key_44'].should_not be_nil
keys[0]['private_key_48'].should_not be_nil
keys[0]['private_key_44'].should eq(jam_track_right.private_key_44)
keys[0]['private_key_48'].should eq(jam_track_right.private_key_48)
end
end

View File

@ -179,7 +179,7 @@ def app_config
end
def signing_job_queue_max_time
20 # 20 seconds
600 # 20 seconds
end
def one_free_jamtrack_per_user
@ -210,6 +210,34 @@ def app_config
"AIzaSyCPTPq5PEcl4XWcm7NZ2IGClZlbsiE8JNo"
end
def estimated_jam_track_time
40
end
def estimated_fast_mixdown_time
30
end
def estimated_slow_mixdown_time
80
end
def num_packaging_nodes
2
end
def signing_job_signing_max_time
300
end
def mixdown_job_queue_max_time
600
end
def mixdown_step_max_time
300
end
private
def audiomixer_workspace_path

View File

@ -94,6 +94,8 @@ gem 'bower-rails', "~> 0.9.2"
gem 'react-rails', '~> 1.0'
#gem "browserify-rails", "~> 0.7"
gem 'react-rails-img'
source 'https://rails-assets.org' do
gem 'rails-assets-reflux'
gem 'rails-assets-classnames'

View File

@ -103,9 +103,8 @@
sampleRate = context.jamClient.GetSampleRate()
sampleRateForFilename = sampleRate == 48 ? '48' : '44';
doSearch();
}
function afterHide() {
showing = false;
}

View File

@ -190,7 +190,7 @@ context.JK.DownloadJamTrack = class DownloadJamTrack
showDownloading: () =>
@logger.debug("showing #{@state.name}")
# while downloading, we don't run the transition timer, because the download API is guaranteed to call success, or failure, eventually
context.jamClient.JamTrackDownload(@jamTrack.id, context.JK.currentUserId,
context.jamClient.JamTrackDownload(@jamTrack.id, null, context.JK.currentUserId,
this.makeDownloadProgressCallback(),
this.makeDownloadSuccessCallback(),
this.makeDownloadFailureCallback())

View File

@ -121,6 +121,9 @@
return 30;
}
function GetSampleRate() {
return 48;
}
function FTUESetVideoShareEnable(){
}
@ -502,6 +505,9 @@
return 0;
}
function GetJamTrackSettings() {
return {tracks:[]}
}
function SessionGetJamTracksPlayDurationMs() {
return 60000;
}
@ -1211,6 +1217,7 @@
this.TrackGetChatUsesMusic = TrackGetChatUsesMusic;
this.TrackSetChatUsesMusic = TrackSetChatUsesMusic;
this.GetJamTrackSettings = GetJamTrackSettings;
this.JamTrackStopPlay = JamTrackStopPlay;
this.JamTrackPlay = JamTrackPlay;
this.JamTrackIsPlayable = JamTrackIsPlayable;
@ -1275,6 +1282,7 @@
this.FTUESetSendFrameRates = FTUESetSendFrameRates;
this.GetCurrentVideoResolution = GetCurrentVideoResolution;
this.GetCurrentVideoFrameRate = GetCurrentVideoFrameRate;
this.GetSampleRate = GetSampleRate;
this.FTUESetVideoShareEnable = FTUESetVideoShareEnable;
this.FTUEGetVideoShareEnable = FTUEGetVideoShareEnable;
this.isSessVideoShared = isSessVideoShared;

View File

@ -1322,6 +1322,85 @@
})
}
function markMixdownActive(options) {
var id = options["id"];
return $.ajax({
type: "POST",
dataType: "json",
contentType: 'application/json',
url: "/api/jamtracks/" + id + "/mixdowns/active",
data: JSON.stringify(options)
})
}
function createMixdown(options) {
return $.ajax({
type: "POST",
dataType: "json",
contentType: 'application/json',
url: "/api/mixdowns/",
data: JSON.stringify(options)
})
}
function editMixdown(options) {
var id = options["id"];
return $.ajax({
type: "POST",
dataType: "json",
contentType: 'application/json',
url: "/api/mixdowns/" + id ,
data: JSON.stringify(options)
})
}
function deleteMixdown(options) {
var id = options["id"];
return $.ajax({
type: "DELETE",
dataType: "json",
contentType: 'application/json',
url: "/api/mixdowns/" + id
})
}
function getMixdown(options) {
var id = options["id"];
return $.ajax({
type: "GET",
dataType: "json",
contentType: 'application/json',
url: "/api/mixdowns/" + id
})
}
function getMixdownPackage(options) {
var id = options["id"];
return $.ajax({
type: "GET",
dataType: "json",
contentType: 'application/json',
url: "/api/mixdown_packages/" + id
})
}
function enqueueMixdown(options) {
var id = options["id"];
return $.ajax({
type: "POST",
dataType: "json",
contentType: 'application/json',
url: "/api/mixdowns/" + id + '/enqueue' ,
data: JSON.stringify(options)
})
}
function openJamTrack(options) {
var musicSessionId = options["id"];
var jamTrackId = options["jam_track_id"];
@ -1951,6 +2030,13 @@
this.claimRecording = claimRecording;
this.startPlayClaimedRecording = startPlayClaimedRecording;
this.stopPlayClaimedRecording = stopPlayClaimedRecording;
this.markMixdownActive = markMixdownActive;
this.createMixdown = createMixdown;
this.editMixdown = editMixdown;
this.deleteMixdown = deleteMixdown;
this.enqueueMixdown = enqueueMixdown;
this.getMixdown = getMixdown;
this.getMixdownPackage = getMixdownPackage;
this.openJamTrack = openJamTrack
this.openBackingTrack = openBackingTrack
this.closeBackingTrack = closeBackingTrack

View File

@ -1,5 +1,6 @@
//= require react-input-autosize
//= require react-select
// //= require react_rails_img
//= require_directory ./react-components/helpers
//= require_directory ./react-components/actions
//= require ./react-components/stores/AppStore

View File

@ -38,9 +38,11 @@ mixins.push(Reflux.listenTo(MediaPlaybackStore, 'onMediaStateChanged'))
@state.controls.onPlayStopEvent()
else if changes.playbackState == 'play_pause'
@state.controls.onPlayPauseEvent();
else if changes.positionUpdateChanged
if changes.positionUpdateChanged
if @state.controls?
@state.controls.executeMonitor(changes.positionMs, changes.durationMs, changes.isPlaying)
if changes.currentTimeChanged
@setState({time: changes.time})
onInputsChanged: (sessionMixers) ->
@ -69,8 +71,8 @@ mixins.push(Reflux.listenTo(MediaPlaybackStore, 'onMediaStateChanged'))
monitorControls: (controls, mediaSummary) ->
if mediaSummary.mediaOpen
if mediaSummary.jamTrackOpen
if mediaSummary.mediaOpen || mediaSummary.jamTrack?
if mediaSummary.jamTrack?
controls.startMonitor(PLAYBACK_MONITOR_MODE.JAMTRACK)
else if mediaSummary.backingTrackOpen
controls.startMonitor(PLAYBACK_MONITOR_MODE.MEDIA_FILE)
@ -163,7 +165,7 @@ mixins.push(Reflux.listenTo(MediaPlaybackStore, 'onMediaStateChanged'))
</div>
</div>
<div className="recording-time start-time">0:00</div>
<div className="recording-time start-time">{this.state.time}</div>
<div className="recording-playback">
<div className="recording-slider"><img src="/assets/content/slider_playcontrols.png" height="16" width="5" /></div>
</div>
@ -179,7 +181,7 @@ mixins.push(Reflux.listenTo(MediaPlaybackStore, 'onMediaStateChanged'))
getInitialState: () ->
{controls: null, mediaSummary: {}, initializedMetronomeControls: false}
{controls: null, mediaSummary: {}, initializedMetronomeControls: false, time: '0:00'}
tryPrepareMetronome: (metro) ->
if @state.mediaSummary.metronomeOpen && !@state.initializedMetronomeControls

View File

@ -1,5 +1,6 @@
context = window
logger = context.JK.logger
ReactCSSTransitionGroup = React.addons.CSSTransitionGroup;
mixins = []
@ -16,30 +17,72 @@ if window.opener?
if accessOpener
SessionActions = window.opener.SessionActions
MediaPlaybackStore = window.opener.MediaPlaybackStore
MixerActions = window.opener.MixerActions
JamTrackActions = window.opener.JamTrackActions
JamTrackMixdownActions = window.opener.JamTrackMixdownActions
#JamTrackMixdownStore = window.opener.JamTrackMixdownStore
JamTrackMixdown = window.opener.JamTrackMixdown
JamTrackStore = window.opener.JamTrackStore
MixerStore = window.opener.MixerStore
SessionStore = window.opener.SessionStore
mixins.push(Reflux.listenTo(MediaPlaybackStore, 'onMediaStateChanged'))
mixins.push(Reflux.listenTo(MixerStore, 'onMixersChanged'))
mixins.push(Reflux.listenTo(JamTrackStore, 'onJamTrackChanged'))
@PopupMediaControls = React.createClass({
mixins: mixins
onMixersChanged: (sessionMixers) ->
session = sessionMixers.session
mixers = sessionMixers.mixers
# the backend delete/adds the metronome rapidly when the user hits play. this is custom code to deal with that
state =
isRecording: session.isRecording
mediaSummary: mixers.mediaSummary
backingTracks: mixers.backingTracks
jamTracks: mixers.jamTracks
recordedTracks: mixers.recordedTracks
metronome: mixers.metronome
recordingName: mixers.recordingName()
jamTrackName: mixers.jamTrackName()
@setState(media: state, downloadingJamTrack: session.downloadingJamTrack)
onMediaStateChanged: (changes) ->
if changes.currentTimeChanged && @root?
@setState({time: changes.time})
onJamTrackMixdownChanged: (changes) ->
@setState({mixdown: changes})
onJamTrackChanged: (changes) ->
logger.debug("PopupMediaControls: jamtrack changed", changes)
@setState({jamTrackState: changes})
showMetronome: (e) ->
e.preventDefault()
SessionActions.showNativeMetronomeGui()
getInitialState: () ->
{time: '0:00'}
{
media: @props.media,
mixdown: @props.mixdown,
jamTrackState: @props.jamTrackState,
creatingMixdown: false,
createMixdownErrors: null,
editingMixdownId: null,
downloadingJamTrack: @props.downloadingJamTrack
}
close: () ->
window.close()
render: () ->
closeLinkText = null
@ -47,21 +90,245 @@ mixins.push(Reflux.listenTo(MediaPlaybackStore, 'onMediaStateChanged'))
extraControls = null
# give the users options to close it
if @props.mediaSummary.recordingOpen
if @state.media.mediaSummary.recordingOpen
mediaType = "Recording"
mediaName = @props.recordedTracks[0].recordingName
mediaName = @state.media.recordedTracks[0].recordingName
closeLinkText = 'close recording'
header = `<h3>{mediaType}: {mediaName} ({this.state.time})</h3>`
else if @props.mediaSummary.jamTrackOpen
header = `<h3>{mediaType}: {mediaName}</h3>`
else if @state.jamTrackState.jamTrack?
jamTrack = @state.jamTrackState.jamTrack
mediaType = "JamTrack"
mediaName = @props.jamTracks[0].name
closeLinkText = 'close JamTrack'
header = `<h3>{mediaType}: {mediaName} ({this.state.time})</h3>`
else if @props.mediaSummary.backingTrackOpen
mediaName = jamTrack.name
closeLinkText = 'CLOSE JAMTRACK'
selectedMixdown = jamTrack.activeMixdown
if selectedMixdown?
jamTrackTypeHeader = 'Custom Mix'
disabled = true
if selectedMixdown.client_state?
switch selectedMixdown.client_state
when 'cant_open'
customMixName = `<h5>{selectedMixdown.name}<img src="/assets/content/icon-mix-fail@2X.png" /></h5>`
when 'keying_timeout'
customMixName = `<h5>{selectedMixdown.name}<img src="/assets/content/icon-mix-fail@2X.png" /></h5>`
when 'download_fail'
customMixName = `<h5>{selectedMixdown.name}<img src="/assets/content/icon-mix-fail@2X.png" /></h5>`
when 'keying'
customMixName = `<h5>Loading selected mix... <img src="/assets/shared/spinner.gif" /></h5>`
when 'downloading'
customMixName = `<h5>Loading selected mix... <img src="/assets/shared/spinner.gif" /></h5>`
when 'ready'
customMixName = `<h5>{selectedMixdown.name}</h5>`
disabled = false
else
customMixName = `<h5>Creating mixdown... <img src="/assets/shared/spinner.gif" /></h5>`
else
if SessionStore.downloadingJamTrack
downloader = `<img src="/assets/shared/spinner.gif" />`
jamTrackTypeHeader = `<span>Full JamTrack {downloader}</span>`
header = `
<div className="header">
<h3>{mediaType}: {mediaName}</h3>
<h4>{jamTrackTypeHeader}</h4>
{customMixName}
</div>`
myMixes = null
if @state.showMyMixes
myMixdowns = []
boundPlayClick = this.jamTrackPlay.bind(this, jamTrack);
active = jamTrack.last_mixdown_id == null
myMixdowns.push `
<div key="full-track" className={classNames({'full-track': true, 'mixdown-display': true, 'active' : active})}>
<div className="mixdown-name">
Full JamTrack
</div>
<div className="mixdown-actions">
<img src="/assets/content/icon_open@2X.png" className="mixdown-play" onClick={boundPlayClick}/>
</div>
</div>`
for mixdown in jamTrack.mixdowns
boundPlayClick = this.mixdownPlay.bind(this, mixdown);
boundEditClick = this.mixdownEdit.bind(this, mixdown);
boundSaveClick = this.mixdownSave.bind(this, mixdown);
boundDeleteClick = this.mixdownDelete.bind(this, mixdown);
boundErrorClick = this.mixdownError.bind(this, mixdown);
boundEditKeydown = this.onEditKeydown.bind(this, mixdown);
mixdown_package = mixdown.myPackage
active = mixdown.id == jamTrack.last_mixdown_id
editing = mixdown.id == @state.editingMixdownId
# if there is a package, check it's state; otherwise let the user enqueue it
if mixdown_package
switch mixdown_package.signing_state
when 'QUIET_TIMEOUT'
action = `<img src="/assets/content/icon-mix-fail@2X.png" className="mixdown-play" onClick={boundErrorClick}/>`
when 'QUIET'
action = `<img src="/assets/shared/spinner.gif" className="mixdown-play"/>`
when 'QUEUED'
action = `<img src="/assets/shared/spinner.gif" className="mixdown-play"/>`
when 'QUEUED_TIMEOUT'
action = `<img src="/assets/content/icon-mix-fail@2X.png" className="mixdown-play" onClick={boundErrorClick}/>`
when 'SIGNING'
action = `<img src="/assets/shared/spinner.gif" className="mixdown-play"/>`
when 'SIGNING_TIMEOUT'
action = `<img src="/assets/content/icon-mix-fail@2X.png" className="mixdown-play" onClick={boundErrorClick}/>`
when 'SIGNED'
action = `<img src="/assets/content/icon_open@2X.png" className="mixdown-play" onClick={boundPlayClick}/>`
when 'ERROR'
action = `<img src="/assets/content/icon-mix-fail@2X.png" className="mixdown-play" onClick={boundErrorClick}/>`
else
action = `<img src="/assets/content/icon-mix-fail@2X.png" className="mixdown-play" onClick={boundErrorClick}/>`
if editing
mixdownName = `<input className="edit-name" type="text" defaultValue={mixdown.name} onKeyDown={boundEditKeydown} />`
editIcon = `<img src="/assets/content/icon-save@2X.png" className="mixdown-edit" onClick={boundSaveClick}/>`
else
mixdownName = mixdown.name
editIcon = `<img src="/assets/content/icon-edit@2X.png" className="mixdown-edit" onClick={boundEditClick}/>`
myMixdowns.push `
<div key={mixdown.id} className={classNames({'mixdown-display': true, 'active' : active})}>
<div className="mixdown-name">
{mixdownName}
</div>
<div className="mixdown-actions">
{action}
{editIcon}
<img src ="/assets/content/icon-delete@2X.png" className="mixdown-delete" onClick={boundDeleteClick} />
</div>
</div>`
myMixes = `<div key="my-mixes" className="my-mixes">{myMixdowns}</div>`
mixControls = null
if @state.showCustomMixes
nameClassData = {field: true}
if @state.createMixdownErrors?
errorHtml = context.JK.reactErrors(@state.createMixdownErrors, {name: 'Mix Name', settings: 'Settings', jam_track: 'JamTrack'})
createMixClasses = classNames({'button-orange' : true, 'create-mix-btn' : true, 'disabled' : @state.creatingMixdown})
mixControls = `
<div key="create-mix" className="create-mix">
<p>Use the JamTrack controls on the session screen to set levels, mute/unmute, or pan any of the parts of the JamTrack as you like. You can also use the controls below to adjust the tempo or pitch of the JamTrack. Then give your custom mix a name, and click the Create Mix button. Please note that changing the tempo or pitch of the JamTrack may take a long time, and won't be ready right away.</p>
<div className="field">
<label>Change Tempo:</label>
<select name="mix-speed">
<option value="">No change</option>
<option value="separator-1">------------</option>
<option value="-5">Slower by 5%</option>
<option value="-10">Slower by 10%</option>
<option value="-15">Slower by 15%</option>
<option value="-20">Slower by 20%</option>
<option value="-25">Slower by 25%</option>
<option value="-30">Slower by 30%</option>
<option value="-35">Slower by 35%</option>
<option value="-40">Slower by 40%</option>
<option value="-45">Slower by 45%</option>
<option value="-50">Slower by 50%</option>
<option value="-60">Slower by 60%</option>
<option value="-70">Slower by 70%</option>
<option value="-80">Slower by 80%</option>
<option value="separator-2">------------</option>
<option value="5">Faster by 5%</option>
<option value="10">Faster by 10%</option>
<option value="15">Faster by 15%</option>
<option value="20">Faster by 20%</option>
<option value="30">Faster by 30%</option>
<option value="40">Faster by 40%</option>
<option value="50">Faster by 50%</option>
</select>
</div>
<div className="field">
<label>Change Pitch:</label>
<select name="mix-pitch">
<option value="">No change</option>
<option value="separator-1">---------------</option>
<option value="-1">Down 1 Semitone</option>
<option value="-2">Down 2 Semitones</option>
<option value="-3">Down 3 Semitones</option>
<option value="-4">Down 4 Semitones</option>
<option value="-5">Down 5 Semitones</option>
<option value="-6">Down 6 Semitones</option>
<option value="-7">Down 7 Semitones</option>
<option value="-8">Down 8 Semitones</option>
<option value="-9">Down 9 Semitones</option>
<option value="-10">Down 10 Semitones</option>
<option value="-11">Down 11 Semitones</option>
<option value="-12">Down 12 Semitones</option>
<option value="separator-2">---------------</option>
<option value="1">Up 1 Semitone</option>
<option value="2">Up 2 Semitones</option>
<option value="3">Up 3 Semitones</option>
<option value="4">Up 4 Semitones</option>
<option value="5">Up 5 Semitones</option>
<option value="6">Up 6 Semitones</option>
<option value="7">Up 7 Semitones</option>
<option value="8">Up 8 Semitones</option>
<option value="9">Up 9 Semitones</option>
<option value="10">Up 10 Semitones</option>
<option value="11">Up 11 Semitones</option>
<option value="12">Up 12 Semitones</option>
</select>
</div>
<div className={classNames(nameClassData)}>
<label>Mix Name:</label>
<input type="text" name="mix-name"/>
</div>
<div className="field">
<a className={createMixClasses} onClick={this.createMix}>CREATE MIX</a>
{errorHtml}
</div>
<div className="clearall"/>
</div>`
if @state.showMyMixes
showMyMixesText = `<a onClick={this.toggleMyMixes}>hide my mixes <div className="details-arrow arrow-up" /></a>`
else
showMyMixesText = `<a onClick={this.toggleMyMixes}>show my mixes <div className="details-arrow arrow-down" /></a>`
if @state.showCustomMixes
showMixControlsText = `<a onClick={this.toggleCustomMixes}>hide mix controls <div className="details-arrow arrow-up" /></a>`
else
showMixControlsText = `<a onClick={this.toggleCustomMixes}>show mix controls <div className="details-arrow arrow-down" /></a>`
extraControls = `
<div className="extra-controls">
<h4>My Mixes {showMyMixesText}</h4>
<ReactCSSTransitionGroup transitionName="session-track-list" transitionAppear={true}>
{myMixes}
</ReactCSSTransitionGroup>
<h4 className="custom-mix-header">Create Custom Mix {showMixControlsText}</h4>
<ReactCSSTransitionGroup transitionName="session-track-list" transitionAppear={true}>
{mixControls}
</ReactCSSTransitionGroup>
</div>`
else if @state.media.mediaSummary.backingTrackOpen
mediaType = "Audio File"
mediaName = context.JK.getNameOfFile(@props.backingTracks[0].shortFilename)
closeLinkText = 'close audio file'
header = `<h3>{mediaType}: {mediaName} ({this.state.time})</h3>`
mediaName = context.JK.getNameOfFile(@state.media.backingTracks[0].shortFilename)
closeLinkText = 'CLOSE AUDIO FILE'
header = `<h3>{mediaType}: {mediaName}</h3>`
extraControls =
`<div>
<div className="field">
@ -69,9 +336,9 @@ mixins.push(Reflux.listenTo(MediaPlaybackStore, 'onMediaStateChanged'))
</div>
<br className="clearall"/>
</div>`
else if @props.mediaSummary.metronomeOpen
else if @state.media.mediaSummary.metronomeOpen
mediaType = "Metronome"
closeLinkText = 'close metronome'
closeLinkText = 'CLOSE METRONOME'
header = `<h3>Metronome</h3>`
extraControls =
`<div>
@ -84,12 +351,161 @@ mixins.push(Reflux.listenTo(MediaPlaybackStore, 'onMediaStateChanged'))
{header}
<MediaControls />
{extraControls}
<a className="close-link" onClick={this.close}>{closeLinkText}</a>
<a className="close-link button-orange" onClick={this.close}>{closeLinkText}</a>
</div>`
windowUnloaded: () ->
SessionActions.closeMedia(false) unless window.DontAutoCloseMedia
toggleMyMixes: (e) ->
e.preventDefault()
@setState({showMyMixes: !@state.showMyMixes})
toggleCustomMixes: (e) ->
e.preventDefault()
@setState({showCustomMixes: !@state.showCustomMixes})
mixdownPlay: (mixdown, e) ->
@setState({editingMixdownId: null})
e.preventDefault()
if @disableLoading
alert('Certain actions are disabled while a track is being loaded.')
return
# make this package the active one
JamTrackMixdownActions.openMixdown(mixdown)
jamTrackPlay: (jamtrack, e) ->
e.preventDefault()
# user wants to select the full track
if @disableLoading
alert('Certain actions are disabled while a track is being loaded.')
return
JamTrackActions.activateNoMixdown(jamtrack)
onEditKeydown: (mixdown, e) ->
logger.debug("on edit keydown", e)
if e.keyCode == 13 # enter
@mixdownSave(mixdown, e)
else if e.keyCode == 27 # esc
@setState({editingMixdownId: null})
mixdownEdit: (mixdown) ->
@setState({editingMixdownId: mixdown.id})
mixdownSave: (mixdown, e) ->
e.preventDefault()
$input = $(this.getDOMNode()).find('input.edit-name')
newValue = $input.val()
logger.debug("editing mixdown name to be: " + newValue)
JamTrackMixdownActions.editMixdown({id: mixdown.id, name: newValue})
@setState({editingMixdownId: null})
mixdownDelete: (mixdown) ->
if @state.editingMixdownId?
@setState({editingMixdownId:null})
return
if confirm("Delete this custom mix?")
JamTrackMixdownActions.deleteMixdown(mixdown)
mixdownError: (mixdown) ->
myPackage = mixdown.myPackage
if myPackage?
switch myPackage.signing_state
when 'QUIET_TIMEOUT'
action = 'Custom mix never got created. Retry?'
when 'QUEUED_TIMEOUT'
action = 'Custom mix was never built. Retry?'
when 'SIGNING_TIMEOUT'
action = 'Custom mix took took long to build. Retry?'
when 'ERROR'
action = 'Custom mix failed to build. Retry?'
else
action = 'Custom mix never got created. Retry?'
return unless action?
if confirm(action)
JamTrackMixdownActions.enqueueMixdown(mixdown, @enqueueDone)
enqueueDone: (enqueued) ->
@promptEstimate(enqueued)
promptEstimate: (enqueued) ->
time = enqueued.queue_time
if time == 0
alert("It will take approximately 1 minute to create your custom mix.")
else
guess = Math.ceil(time / 60.0)
if guess == 1
msg = '1 minute'
else
msg = "#{guess} minutes"
alert("Your custom mix will take approximately #{msg} to be created.")
createMix: (e) ->
e.preventDefault()
return if @state.creatingMix
$root = $(@getDOMNode())
name = $root.find('input[name="mix-name"]').val()
speed = $root.find('select[name="mix-speed"]').val()
pitch = $root.find('select[name="mix-pitch"]').val()
if @state.jamTrackState.jamTrack?.activeMixdown?
@setState({createMixdownErrors: {errors: {'Full JamTrack': ['must be selected']}}})
return
if name == null || name == ''
@setState({createMixdownErrors: {errors: {'Mix Name': ["can't be blank"]}}})
return
# sanitize junk out of speed/pitch
if speed == '' || speed.indexOf('separator') > -1
speed = undefined
else
speed = parseInt(speed)
if pitch == '' || pitch.indexOf('separator') > -1
pitch = undefined
else
pitch = parseInt(pitch)
mixdown = {jamTrackID: @state.jamTrackState.jamTrack.id, name: name, settings: {speed:speed, pitch: pitch}}
JamTrackMixdownActions.createMixdown(mixdown, @createMixdownDone, @createMixdownFail)
@setState({creatingMixdown: true, createMixdownErrors: null})
createMixdownDone: (created) ->
logger.debug("created (within PopupMediaControls)", created)
# automatically close the create custom mix area
@setState({creatingMixdown: false, showCustomMixes: false, showMyMixes: true})
@promptEstimate(created)
createMixdownFail: (jqXHR) ->
logger.debug("create mixdown fail (within PopupMediaControls)", jqXHR.status)
@setState({creatingMixdown: false})
if jqXHR.status == 422
response = JSON.parse(jqXHR.responseText)
logger.warn("failed to create mixdown", response, jqXHR.responseText)
@setState({createMixdownErrors: response})
componentDidMount: () ->
$(window).unload(@windowUnloaded)
@ -100,13 +516,12 @@ mixins.push(Reflux.listenTo(MediaPlaybackStore, 'onMediaStateChanged'))
context.JK.checkbox($loop)
$loop.on('ifChecked', () =>
logger.debug("@props", @props)
# it doesn't matter if you do personal or master, because backend just syncs both
MixerActions.loopChanged(@props.backingTracks[0].mixers.personal.mixer, true)
MixerActions.loopChanged(@state.media.backingTracks[0].mixers.personal.mixer, true)
)
$loop.on('ifUnchecked', () =>
# it doesn't matter if you do personal or master, because backend just syncs both
MixerActions.loopChanged(@props.backingTracks[0].mixers.personal.mixer, false)
MixerActions.loopChanged(@state.media.backingTracks[0].mixers.personal.mixer, false)
)
@resizeWindow()
@ -116,6 +531,7 @@ mixins.push(Reflux.listenTo(MediaPlaybackStore, 'onMediaStateChanged'))
componentDidUpdate: () ->
@resizeWindow()
setTimeout(@resizeWindow, 1000)
resizeWindow: () =>
$container = $('#minimal-container')
@ -134,4 +550,25 @@ mixins.push(Reflux.listenTo(MediaPlaybackStore, 'onMediaStateChanged'))
#offset += 25
window.resizeTo(width, height + offset)
componentWillUpdate: (nextProps, nextState) ->
@disableLoading = false
return unless nextState?
selectedMixdown = nextState?.jamTrackState?.jamTrack?.activeMixdown
mixdownDownloading = false
if selectedMixdown?
switch selectedMixdown.client_state
when 'keying'
mixdownDownloading = true
when 'downloading'
mixdownDownloading = true
@disableLoading = SessionStore.downloadingJamTrack || mixdownDownloading
})

View File

@ -14,10 +14,19 @@ ChannelGroupIds = context.JK.ChannelGroupIds
Reflux.listenTo(@AppStore,"onAppInit"),
Reflux.listenTo(@JamTrackStore, "onJamTrackStateChanged")]
onJamTrackStateChanged: (jamTrack) ->
if jamTrack?
@loadJamTrack(jamTrack)
else
onJamTrackStateChanged: (jamTrackState) ->
if jamTrackState.fullTrackActivated || jamTrackState.opened && jamTrackState.jamTrack.activeMixdown == null
@loadJamTrack(jamTrackState.jamTrack)
else if jamTrackState.closed
logger.debug("SessionMediaTracks: jamtrack has been closed")
if @state.downloadJamTrack?
logger.debug("closing DownloadJamTrack widget")
@state.downloadJamTrack.root.remove()
@state.downloadJamTrack.destroy()
SessionActions.downloadingJamTrack(false)
@setState({downloadJamTrack: null})
SessionActions.closeMedia(true)
#inputsChangedProcessed: (state) ->
@ -264,8 +273,8 @@ ChannelGroupIds = context.JK.ChannelGroupIds
# All the JamTracks
mediaTracks.push(`<SessionJamTrackCategory key="JamTrackCategory" jamTrackName={this.state.jamTrackName} mixers={this.state.mediaCategoryMixer} mode={MIX_MODES.PERSONAL} />`)
if @state.metronome?
# show metronome only if it's a full jamtrack
if @state.metronome? && @state.jamTrackMixdown.id == null
@state.metronome.mode = MIX_MODES.PERSONAL
mediaTracks.push(`<SessionMetronome key="JamTrackMetronome" {...this.state.metronome} location="jam-track" />`)
@ -334,11 +343,11 @@ ChannelGroupIds = context.JK.ChannelGroupIds
@handlePopup()
handlePopup: () ->
if @state.mediaSummary.mediaOpen
if @state.mediaSummary.userNeedsMediaControls
unless @childWindow?
logger.debug("opening media control window")
@childWindow = window.open("/popups/media-controls", 'Media Controls', 'scrollbars=yes,toolbar=no,status=no,height=155,width=350')
@childWindow.PopupProps = @state
@childWindow.PopupProps = {media: @state, jamTrackState: context.JamTrackStore.getState(), downloadingJamTrack: context.SessionStore.downloadingJamTrack }
else
if @childWindow?
@childWindow.DontAutoCloseMedia = true

View File

@ -113,9 +113,10 @@ ptrCount = 0
context.JK.checkbox($checkbox)
$checkbox.on('ifChanged', this.handleMuteCheckbox);
# using iCheck causes a 'ifChanged' event, so we need to swallow this up
@iCheckMaint = true
if muteMixer.mute
if muteMixer?.mute
$checkbox.iCheck('check').attr('checked', true)
else
$checkbox.iCheck('uncheck').attr('checked', false)
@ -139,7 +140,7 @@ ptrCount = 0
# using iCheck causes a 'ifChanged' event, so we need to swallow this up
@iCheckMaint = true
if muteMixer.mute
if muteMixer?.mute
$checkbox.iCheck('check').attr('checked', true)
else
$checkbox.iCheck('uncheck').attr('checked', false)

View File

@ -3,6 +3,7 @@ context = window
@JamTrackActions = Reflux.createActions({
open: {}
close: {}
activateNoMixdown: {}
requestSearch: {}
requestFilter: {}
})

View File

@ -0,0 +1,13 @@
context = window
@JamTrackMixdownActions = Reflux.createActions({
createMixdown: {}
editMixdown: {}
refreshMixdown: {}
deleteMixdown: {}
openMixdown: {}
closeMixdown: {}
enqueueMixdown: {}
downloadMixdown: {}
})

View File

@ -19,4 +19,5 @@ context = window
broadcastFailure: {}
broadcastSuccess: {}
broadcastStopped: {}
mixdownActive: {}
})

View File

@ -193,6 +193,11 @@ MIX_MODES = context.JK.MIX_MODES;
@mediaSummary.mediaOpen = mediaOpenSummary
# the user needs media controls if any media is open, or, if the user has indicated they want to open a JamTrack
@mediaSummary.userNeedsMediaControls = @mediaSummary.mediaOpen || window.JamTrackStore.jamTrack?
# this defines what the user wants to be open, not what actually is open in the backend and/or session
@mediaSummary.jamTrack = window.JamTrackStore.jamTrack
# figure out if we opened any media
isOpener = false
@ -294,6 +299,7 @@ MIX_MODES = context.JK.MIX_MODES;
jamTrackMixers = @jamTrackMixers.slice();
jamTracks = []
jamTrackName = null;
jamTrackMixdown = {id: null}
if @session.isPlayingRecording()
# only return managed mixers for recorded backing tracks
@ -303,6 +309,7 @@ MIX_MODES = context.JK.MIX_MODES;
# only return un-managed (ad-hoc) mixers for normal backing tracks
jamTracks = @session.jamTracks()
jamTrackName = @session.jamTrackName()
jamTrackMixdown = @session.jamTrackMixdown()
# pluck the 1st mixer, and assume that all other mixers in this group are of the same type (between JamTrack vs Peer)
# if it's a locally opened track (JamTrackGroup), then we can say this person is the opener
@ -310,55 +317,90 @@ MIX_MODES = context.JK.MIX_MODES;
if jamTracks
noCorrespondingTracks = false
for jamTrack in jamTracks
mixer = null
preMasteredClass = ""
# find the track or tracks that correspond to the mixer
correspondingTracks = []
for matchMixer in @jamTrackMixers
if matchMixer.id == jamTrack.id
correspondingTracks.push(jamTrack)
mixer = matchMixer
if correspondingTracks.length == 0
# Are we opening a mixdown, or a full track?
if jamTrackMixdown.id?
logger.debug("MixerHelper: mixdown is active. id: #{jamTrackMixdown.id}")
if jamTrackMixers.length == 0
noCorrespondingTracks = true
logger.error("could not correlate jam tracks", jamTrackMixers, jamTracks)
logger.error("could not correlate mixdown tracks", jamTrackMixers, jamTrackMixdown)
@app.notify({
title: "Unable to Open JamTrack",
title: "Unable to Open Custom Mix",
text: "Could not correlate server and client tracks",
icon_url: "/assets/content/icon_alert_big.png"})
return _jamTracks
#jamTracks = $.grep(jamTracks, (value) =>
# $.inArray(value, correspondingTracks) < 0
#)
# prune found mixers
jamTrackMixers.splice(mixer);
oneOfTheTracks = correspondingTracks[0];
instrumentIcon = context.JK.getInstrumentIcon24(oneOfTheTracks.instrument.id);
part = oneOfTheTracks.part
instrumentName = oneOfTheTracks.instrument.description
if part?
trackName = "#{instrumentName}: #{part}"
else if jamTrackMixers.length > 1
logger.warn("ignoring wrong amount of mixers for JamTrack in mixdown mode")
return _jamTracks
else
trackName = instrumentName
data =
name: jamTrackName
trackName: trackName
part: part
isOpener: isOpener
instrumentIcon: instrumentIcon
track: oneOfTheTracks
mixers: @mediaMixers(mixer, isOpener)
instrumentIcon = context.JK.getInstrumentIcon24('other')
part = null
instrumentName = 'Custom Mix'
trackName = 'Custom Mix'
_jamTracks.push(data)
data =
name: jamTrackName
trackName: trackName
part: part
isOpener: isOpener
instrumentIcon: instrumentIcon
track: jamTrackMixdown
mixers: @mediaMixers(jamTrackMixers[0], isOpener)
_jamTracks.push(data)
else
logger.debug("MixerHelper: full jamtrack is active")
for jamTrack in jamTracks
mixer = null
preMasteredClass = ""
# find the track or tracks that correspond to the mixer
correspondingTracks = []
for matchMixer in @jamTrackMixers
if matchMixer.id == jamTrack.id
correspondingTracks.push(jamTrack)
mixer = matchMixer
if correspondingTracks.length == 0
noCorrespondingTracks = true
logger.error("could not correlate jam tracks", jamTrackMixers, jamTracks)
@app.notify({
title: "Unable to Open JamTrack",
text: "Could not correlate server and client tracks",
icon_url: "/assets/content/icon_alert_big.png"})
return _jamTracks
#jamTracks = $.grep(jamTracks, (value) =>
# $.inArray(value, correspondingTracks) < 0
#)
# prune found mixers
jamTrackMixers.splice(mixer);
oneOfTheTracks = correspondingTracks[0];
instrumentIcon = context.JK.getInstrumentIcon24(oneOfTheTracks.instrument.id);
part = oneOfTheTracks.part
instrumentName = oneOfTheTracks.instrument.description
if part?
trackName = "#{instrumentName}: #{part}"
else
trackName = instrumentName
data =
name: jamTrackName
trackName: trackName
part: part
isOpener: isOpener
instrumentIcon: instrumentIcon
track: oneOfTheTracks
mixers: @mediaMixers(mixer, isOpener)
_jamTracks.push(data)
_jamTracks

View File

@ -75,6 +75,9 @@ context = window
else
null
jamTrackMixdown: () ->
{ id: @session?.jam_track?.mixdown.id }
jamTrackName: () ->
@session?.jam_track?.name

View File

@ -31,6 +31,7 @@ logger = context.JK.logger
mediaCategoryMixer: mediaCategoryMixer
recordingName: mixers.recordingName()
jamTrackName: mixers.jamTrackName()
jamTrackMixdown: session.jamTrackMixdown()
@inputsChangedProcessed(state) if @inputsChangedProcessed?

View File

@ -0,0 +1,90 @@
$ = jQuery
context = window
logger = context.JK.logger
rest = context.JK.Rest()
EVENTS = context.JK.EVENTS
JamTrackActions = @JamTrackActions
@JamTrackMixdownStore = Reflux.createStore(
{
# listenables: JamTrackMixdownActions
# the jamtrack that contains the mixdowns in question
jamTrack: null
# what mixdowns are being built right now
building: []
# a currently open (loaded) mixdown
current: null
init: () ->
this.listenTo(context.AppStore, this.onAppInit);
this.listenTo(context.JamTrackStore, this.onJamTrackChanged);
@changed()
onAppInit: (@app) ->
getState: () ->
@state
changed: () ->
@state = {jamTrack: @jamTrack, building:@building, current: @current}
this.trigger(@state)
onJamTrackChanged: (@jamTrack) ->
# TODO: close out building? current?
onCreateMixdown: (mixdown, package_settings, done, fail) ->
logger.debug("creating mixdown", mixdown, package_settings)
rest.createMixdown(mixdown)
.done((created) =>
logger.debug("created mixdown", created)
package_settings.id = created.id
# we have to determine sample rate here, in the store, because child windows don't have access to jamClient
sampleRate = context.jamClient.GetSampleRate()
sampleRate = if sampleRate == 48 then 48 else 44
package_settings.sample_rate = sampleRate
rest.enqueueMixdown(package_settings)
.done((enqueued) =>
logger.debug("enqueued mixdown package", package_settings)
done(enqueued)
)
.fail((jqxhr) =>
@app.layout.notify({title:'Unable to Package Mixdown', text: 'You can push the RETRY button.'})
fail(jqxhr)
)
)
.fail((jqxhr) =>
fail(jqxhr)
)
onEditMixdown: (mixdown) ->
logger.debug("editing mixdown", mixdown)
onDeleteMixdown: (mixdown) ->
logger.debug("deleting mixdown", mixdown)
onOpenMixdown: (mixdown) ->
logger.debug("opening mixdown", mixdown)
onCloseMixdown: (mixdown) ->
logger.debug("closing mixdown", mixdown)
onEnqueueMixdown: (mixdown) ->
logger.debug("enqueuing mixdown", mixdown)
onDownloadMixdown: (mixdown) ->
logger.debug("download mixdown", mixdown)
onRefreshMixdown: (mixdown) ->
logger.debug("refresh mixdown", mixdown)
}
)

View File

@ -9,10 +9,13 @@ JamTrackActions = @JamTrackActions
@JamTrackStore = Reflux.createStore(
{
listenables: JamTrackActions
listenables: [JamTrackActions, JamTrackMixdownActions]
jamTrack: null
previous: null
requestedSearch: null
requestedFilter: null
subscriptions: {}
enqueuedMixdowns: {}
init: ->
# Register with the app store to get @app
@ -21,17 +24,245 @@ JamTrackActions = @JamTrackActions
onAppInit: (app) ->
@app = app
getState: () ->
@state
pickMyPackage: () ->
return unless @jamTrack?
for mixdown in @jamTrack.mixdowns
myPackage = null
for mixdown_package in mixdown.packages
if mixdown_package.file_type == 'ogg' && mixdown_package.encrypt_type == 'jkz' && mixdown_package.sample_rate == @sampleRate
myPackage = mixdown_package
break
mixdown.myPackage = myPackage
subscriptionKey: (mixdown_package) ->
"mixdown-#{mixdown_package.id}"
subscribe: (mixdown_package) ->
key = @subscriptionKey(mixdown_package)
if !@watchedMixdowns[key]?
# we need to register
context.JK.SubscriptionUtils.subscribe('mixdown', mixdown_package.id).on(context.JK.EVENTS.SUBSCRIBE_NOTIFICATION, this.onMixdownSubscriptionEvent)
@watchedMixdowns[key] = {type:'mixdown', id: mixdown_package.id}
unsubscribe: (mixdown_package) ->
key = @subscriptionKey(mixdown_package)
if @watchedMixdowns[key]?
context.JK.SubscriptionUtils.unsubscribe('mixdown', mixdown_package.id)
delete @watchedMixdowns[key]
manageWatchedMixdowns: () ->
if @jamTrack?
for mixdown in @jamTrack.mixdowns
if mixdown.myPackage
if mixdown.myPackage.signing_state == 'SIGNED'
@unsubscribe(mixdown.myPackage)
else
@subscribe(mixdown.myPackage)
else
for key, subscription of @watchedMixdowns
logger.debug("unsubscribing bulk", key, subscription)
context.JK.SubscriptionUtils.unsubscribe(subscription.type, subscription.id)
# we cleared them all out; clear out storage
@watchedMixdowns = {}
onMixdownSubscriptionEvent: (e, data) ->
logger.debug("JamTrackStore: subscription notification received: type:" + data.type, data)
return unless @jamTrack?
mixdown_package_id = data.id
for mixdown in @jamTrack.mixdowns
for mixdown_package in mixdown.packages
if mixdown_package.id == mixdown_package_id
mixdown_package.signing_state = data.body.signing_state
mixdown_package.packaging_steps = data.body.packaging_steps
mixdown_package.current_packaging_step = data.body.current_packaging_step
logger.debug("updated package with subscription notification event")
if mixdown_package.signing_state == 'SIGNING_TIMEOUT' || mixdown_package.signing_state == 'QUEUED_TIMEOUT' || mixdown_package.signing_state == 'QUIET_TIMEOUT' || mixdown_package.signing_state == 'ERROR'
@reportError(mixdown)
@changed()
break
# this drives the state engine required to get a Mixdown from 'available on the server' to
manageMixdownSynchronization: () ->
@jamTrack.activeMixdown = null if @jamTrack
# let's see if we have a mixdown active?
if !@jamTrack?.last_mixdown_id?
logger.debug("JamTrackStore: no mixdown active")
@clearMixdownTimers()
return
for mixdown in @jamTrack.mixdowns
if mixdown.id == @jamTrack.last_mixdown_id
@jamTrack.activeMixdown = mixdown
logger.debug("JamTrackStore: mixdown active:", mixdown)
break
if @jamTrack.activeMixdown?
# if we don't have this on the server yet, don't engage the rest of this logic...
return if @jamTrack.activeMixdown?.myPackage?.signing_state != 'SIGNED'
fqId = "#{@jamTrack.id}_#{@jamTrack.activeMixdown.id}-#{@sampleRate}"
@trackDetail = context.jamClient.JamTrackGetTrackDetail (fqId)
logger.debug("JamTrackStore: JamTrackGetTrackDetail(#{fqId}).key_state: " + @trackDetail.key_state, @trackDetail)
# first check if the version is not the same; if so, invalidate.
if @trackDetail.version? && @jamTrack.activeMixdown.myPackage?
if @jamTrack.activeMixdown.myPackage.version != @trackDetail.version
logger.info("JamTrackStore: JamTrack Mixdown on disk is different version (stored: #{@trackDetail.version}, server: #{@jamTrack.activeMixdown.myPackage.version}. Invalidating")
context.jamClient.InvalidateJamTrack(fqId)
@trackDetail = context.jamClient.JamTrackGetTrackDetail (fqId)
if @trackDetail.version?
logger.error("after invalidating package, the version is still wrong!")
throw "after invalidating package, the version is still wrong!"
if @jamTrack.activeMixdown.client_state == 'cant_open'
logger.debug(" skipping state check because of earlier 'cant_open'. user should hit retry. ")
return
if @jamTrack.activeMixdown.client_state == 'download_fail'
logger.debug("skipping state check because of earlier 'download_fail'. user should hit retry. ")
return
if @jamTrack.activeMixdown.client_state == 'downloading'
logger.debug("skipping state check because we are downloading")
switch @trackDetail.key_state
when 'pending'
@attemptKeying()
when 'not authorized'
# TODO: if not authorized, do we need to re-initiate a keying attempt?
@attemptKeying()
when 'ready'
if @jamTrack.activeMixdown.client_state != 'ready'
@clearMixdownTimers()
@jamTrack.activeMixdown.client_state = 'ready'
# now load it:
# JamTrackPlay means 'load'
logger.debug("JamTrackStore: loading mixdown")
context.jamClient.JamTrackStopPlay();
result = context.jamClient.JamTrackPlay(fqId);
if !result
@jamTrack.activeMixdown.client_state = 'cant_open'
@reportError(@jamTrack.activeMixdown)
@app.notify(
{
title: "Mixdown Can Not Open",
text: "Unable to open your JamTrack Mixdown. Please contact support@jamkazam.com"
}
, null, true)
when 'unknown'
if @jamTrack.activeMixdown.client_state != 'downloading'
@jamTrack.activeMixdown.client_state = 'downloading'
logger.debug("JamTrackStore: initiating download of mixdown")
context.jamClient.JamTrackDownload(@jamTrack.id, @jamTrack.activeMixdown.id, context.JK.currentUserId,
this.makeDownloadProgressCallback(),
this.makeDownloadSuccessCallback(),
this.makeDownloadFailureCallback())
else
logger.debug("JamTrackStore: already downloading")
attemptKeying: () ->
if @keyCheckTimeout?
logger.debug("JamTrackStore: attemptKeying: skipping because already keying")
return
else if @jamTrack.activeMixdown.client_state == 'keying_timeout'
# if we have timed out keying, we shouldn't automatically retry
logger.debug("JamTrackStore: attempKeying: skipping because we have timed out before and user hasn't requested RETRY")
return
else
@keyCheckTimeout = setTimeout(@onKeyCheckTimeout, 10000)
@keyCheckoutInterval = setInterval(@checkOnKeying, 1000)
@jamTrack.activeMixdown.client_state = 'keying'
logger.debug("JamTrackStore: initiating keying requested")
context.jamClient.JamTrackKeysRequest()
onKeyCheckTimeout: () ->
@keyCheckTimeout = null
clearInterval(@keyCheckoutInterval)
@keyCheckoutInterval = null
if @jamTrack?.activeMixdown?
@jamTrack.activeMixdown.client_state = 'keying_timeout'
@reportError(@jamTrack.activeMixdown)
@changed()
checkOnKeying: () ->
@manageMixdownSynchronization()
# if we exit keying state, we can clear our timers and poke state
if @jamTrack.activeMixdown.client_state != 'keying'
@clearMixdownTimers()
@changed()
# clear out any timer/watcher stuff
clearMixdownTimers: () ->
logger.debug("JamTrackStore: clearing mixdown timers", @keyCheckTimeout, @keyCheckoutInterval)
clearTimeout(@keyCheckTimeout) if @keyCheckTimeout?
clearInterval(@keyCheckoutInterval) if @keyCheckoutInterval?
@keyCheckTimeout = null
@keyCheckoutInterval = null
changed: () ->
@pickMyPackage()
@manageWatchedMixdowns()
@manageMixdownSynchronization()
@state = {
jamTrack: @jamTrack,
opened: @previous == null && @jamTrack != null,
closed: @previous != null && @jamTrack == null,
fullTrackActivated: @previousMixdown != null && @jamTrack?.activeMixdown == null}
@previous = @jamTrack
@previousMixdown = @jamTrack?.activeMixdown
this.trigger(@state)
onOpen: (jamTrack) ->
if @jamTrack?
@app.notify({text: 'Unable to open JamTrack because another one is already open.'})
return
@enqueuedMixdowns = {}
@jamTrack = jamTrack
this.trigger(@jamTrack)
# we can cache this because you can't switch gear while in a session (and possible change sample rate!)
sampleRate = context.jamClient.GetSampleRate()
@sampleRate = if sampleRate == 48 then 48 else 44
@changed()
onClose: () ->
@jamTrack = null
this.trigger(@jamTrack)
@changed()
onRequestSearch:(searchType, searchData) ->
@requestedSearch = {searchType: searchType, searchData: searchData}
@ -53,5 +284,261 @@ JamTrackActions = @JamTrackActions
@requestedFilter = null
requested
onCreateMixdown: (mixdown, done, fail) ->
volumeSettings = context.jamClient.GetJamTrackSettings();
track_settings = []
for track in volumeSettings.tracks
track_settings.push({id: track.id, pan: track.pan, vol: track.vol_l, mute: track.mute})
mixdown.settings.tracks = track_settings
logger.debug("creating mixdown", mixdown)
rest.createMixdown(mixdown)
.done((created) =>
@addMixdown(created)
logger.debug("created mixdown", created)
@onEnqueueMixdown({id: created.id}, done, fail)
)
.fail((jqxhr) =>
fail(jqxhr)
)
onEditMixdown: (mixdown) ->
logger.debug("editing mixdown", mixdown)
rest.editMixdown(mixdown)
.done((updatedMixdown) =>
logger.debug("edited mixdown")
@updateMixdown(updatedMixdown)
).fail((jqxhr) =>
@app.layout.notify({title:'Unable to Edit Custom Mix', text: 'The server was unable to edit this mix.'})
)
onDeleteMixdown: (mixdown) ->
logger.debug("deleting mixdown", mixdown)
rest.deleteMixdown(mixdown)
.done(() =>
logger.debug("deleted mixdown")
@deleteMixdown(mixdown)
)
.fail((jqxhr) =>
@app.layout.notify({title:'Unable to Deleted Custom Mix', text: 'The server was unable to delete this mix.'})
)
onOpenMixdown: (mixdown) ->
logger.debug("opening mixdown", mixdown)
# check if it's already available in the backend or not
rest.markMixdownActive({id: @jamTrack.id, mixdown_id: mixdown.id})
.done((edited) =>
logger.debug("marked mixdown as active")
@jamTrack = edited
# unload any currently loaded JamTrack
context.jamClient.JamTrackStopPlay();
@changed()
SessionActions.mixdownActive(mixdown)
)
.fail((jqxhr) =>
@app.layout.notify({title:'Unable to Edit Mixdown', text: 'Unable to mark this mixdown as active.'})
)
onActivateNoMixdown: (jamTrack) ->
logger.debug("activating no mixdown")
rest.markMixdownActive({id: @jamTrack.id, mixdown_id: null})
.done((edited) =>
logger.debug("marked JamTrack as active")
@jamTrack = edited
@changed()
SessionActions.mixdownActive({id:null})
)
.fail((jqxhr) =>
@app.layout.notify({title:'Unable to Edit Mixdown', text: 'Unable to mark this mixdown as active.'})
)
onCloseMixdown: (mixdown) ->
logger.debug("closing mixdown", mixdown)
onEnqueueMixdown: (mixdown, done, fail) ->
logger.debug("enqueuing mixdown", mixdown)
package_settings = {file_type: 'ogg', encrypt_type: 'jkz', sample_rate: @sampleRate}
package_settings.id = mixdown.id
rest.enqueueMixdown(package_settings)
.done((enqueued) =>
@enqueuedMixdowns[mixdown.id] = {}
logger.debug("enqueued mixdown package", package_settings)
@addOrUpdatePackage(enqueued)
done(enqueued) if done
)
.fail((jqxhr) =>
@app.layout.notify({title:'Unable to Create Custom Mix', text: 'Click the error icon to retry.'})
fail(jqxhr) if fail?
)
onDownloadMixdown: (mixdown) ->
logger.debug("download mixdown", mixdown)
onRefreshMixdown: (mixdown) ->
logger.debug("refresh mixdown", mixdown)
addMixdown: (mixdown) ->
if @jamTrack?
logger.debug("adding mixdown to JamTrackStore", mixdown)
@jamTrack.mixdowns.splice(0, 0, mixdown)
@changed()
else
logger.warn("no jamtrack to add mixdown to in JamTrackStore", mixdown)
deleteMixdown: (mixdown) ->
if @jamTrack?
logger.debug("deleting mixdown from JamTrackStore", mixdown)
index = null
for matchMixdown, i in @jamTrack.mixdowns
if mixdown.id == matchMixdown.id
index = i
if index?
@jamTrack.mixdowns.splice(index, 1)
if @jamTrack.activeMixdown?.id == mixdown.id
@onActivateNoMixdown(@jamTrack)
@changed()
else
logger.warn("unable to find mixdown to delete in JamTrackStore", mixdown)
else
logger.warn("no jamtrack to delete mixdown for in JamTrackStore", mixdown)
updateMixdown: (mixdown) ->
if @jamTrack?
logger.debug("editing mixdown from JamTrackStore", mixdown)
index = null
for matchMixdown, i in @jamTrack.mixdowns
if mixdown.id == matchMixdown.id
index = i
if index?
@jamTrack.mixdowns[index] = mixdown
@changed()
else
logger.warn("unable to find mixdown to edit in JamTrackStore", mixdown)
else
logger.warn("no jamtrack to edit mixdown for in JamTrackStore", mixdown)
addOrUpdatePackage: (mixdown_package) ->
if @jamTrack?
added = false
index = null
for mixdown in @jamTrack.mixdowns
existing = false
if mixdown_package.jam_track_mixdown_id == mixdown.id
for possiblePackage, i in mixdown.packages
if possiblePackage.id == mixdown_package.id
existing = true
index = i
break
if existing
mixdown.packages[index] = mixdown_package
logger.debug("replacing mixdown package in JamTrackStore", mixdown_package)
else
mixdown.packages.splice(0, 0, mixdown_package)
logger.debug("adding mixdown package in JamTrackStore")
added = true
@changed()
break
if !added
logger.debug("couldn't find the mixdown associated with package in JamTrackStore", mixdown_package)
else
logger.warn("no mixdown to add package to in JamTrackStore", mixdown_package)
updateDownloadProgress: () ->
if @bytesReceived? and @bytesTotal?
progress = "#{Math.round(@bytesReceived/@bytesTotal * 100)}%"
else
progress = '0%'
#@root.find('.state-downloading .progress').text(progress)
downloadProgressCallback: (bytesReceived, bytesTotal) ->
logger.debug("download #{bytesReceived}/#{bytesTotal}")
@bytesReceived = Number(bytesReceived)
@bytesTotal = Number(bytesTotal)
# the reason this timeout is set is because, without it,
# we observe that the client will hang. So, if you remove this timeout, make sure to test with real client
setTimeout(this.updateDownloadProgress, 100)
downloadSuccessCallback: (updateLocation) ->
# is the package loadable yet?
logger.debug("JamTrackStore: download complete - on to keying")
@attemptKeying()
@changed()
downloadFailureCallback: (errorMsg) ->
if @jamTrack?.activeMixdown?
@jamTrack.activeMixdown.client_state = 'download_fail'
@reportError(@jamTrack.activeMixdown)
@changed()
# makes a function name for the backend
makeDownloadProgressCallback: () ->
"JamTrackStore.downloadProgressCallback"
# makes a function name for the backend
makeDownloadSuccessCallback: () ->
"JamTrackStore.downloadSuccessCallback"
# makes a function name for the backend
makeDownloadFailureCallback: () ->
"JamTrackStore.downloadFailureCallback"
reportError: (mixdown) ->
enqueued = @enqueuedMixdowns[mixdown?.id]
# don't double-report
if !enqueued? || enqueued.marked
return
enqueued.marked = true
data = {
value: 1,
user_id: context.JK.currentUserId,
user_name: context.JK.currentUserName,
result: "signing state: #{mixdown.myPackage?.signing_state}, client state: #{mixdown.client_state}",
mixdown: mixdown.id,
package: mixdown.myPackage?.id
detail: mixdown.myPackage?.error_reason
}
rest.createAlert("Mixdown Sync failed for #{context.JK.currentUserName}", data)
context.stats.write('web.mixdown.error', data)
}
)

View File

@ -53,6 +53,11 @@ VideoActions = @VideoActions
RecordingActions.initModel(@recordingModel)
@helper = new context.SessionHelper(@app, @currentSession, @participantsEverSeen, @isRecording, @downloadingJamTrack)
onMixdownActive: (mixdown) ->
if @currentSession?.jam_track?
@currentSession.jam_track.mixdown = mixdown
@issueChange()
onVideoChanged: (@videoState) ->
@ -241,6 +246,7 @@ VideoActions = @VideoActions
rest.closeJamTrack({id: @currentSessionId})
.done(() =>
@downloadingJamTrack = false
@refreshCurrentSession(true)
)
.fail((jqXHR) =>

View File

@ -921,6 +921,24 @@
return ul;
}
context.JK.reactErrors = function (errors_data, fieldMapper) {
var errors = errors_data["errors"];
if (errors == null) return null;
var items = []
$.each(errors, function (fieldName, field_errors) {
var displayName = fieldMapper && fieldMapper[fieldName]
if (!displayName) {
displayName = fieldName;
}
$.each(field_errors, function (index, item) {
items.push(React.DOM.li({key: fieldName + item}, displayName + ' ' + item))
});
});
return React.DOM.ul({className: 'error-text'}, null, items)
}
/**
* Way to verify that a number of parallel tasks have all completed.

View File

@ -36,10 +36,213 @@ body.media-controls-popup.popup {
.close-link {
margin-top:20px;
font-size:11px;
margin-bottom:10px;
}
.display-metronome {
font-size:12px;
margin-top:35px;
}
.header {
padding-bottom:20px;
h3 {
text-align:center;
font-weight:bold;
}
h4 {
margin-top:15px;
font-size:12px;
font-weight:normal;
span {
vertical-align:middle;
}
img {
vertical-align:middle;
margin-left:5px;
height:16px;
}
}
h5 {
font-size:12px;
font-weight:normal;
span {
vertical-align:middle;
}
img {
vertical-align:middle;
margin-left:5px;
height:16px;
}
}
}
.extra-controls {
margin-top:20px;
h4 {
text-align:left;
font-size:14px;
a {
font-size:11px;
position:absolute;
right:20px;
}
&.custom-mix-header {
margin-top:20px;
}
}
.my-mixes {
margin-top:5px;
max-height:170px;
border-width:1px;
border-bottom-color:#676767;
border-top-color:#676767;
border-left-color:#171717;
border-right-color:#171717;
border-style:solid;
overflow:auto;
@include border_box_sizing;
}
.mixdown-display {
display:table;
font-size:12px;
color:$ColorTextTypical;
width:100%;
border-width:1px 0;
border-top-color:#343434;
border-bottom-color:#282828;
border-style:solid;
background-color:#2c2c2c;
@include border_box_sizing;
border-spacing:7px;
text-align: left;
&.active {
background-color:#44423f;
}
}
.mixdown-name {
line-height:125%;
width:210px;
text-align:left;
display: table-cell;
vertical-align: middle;
}
.mixdown-actions {
display: table-cell;
vertical-align: middle;
margin-left:10px;
width:100px;
white-space:nowrap;
min-width:100px;
}
.mixdown-stateful {
display:inline-block;
vertical-align:middle;
width:24px;
height:24px;
cursor:pointer;
}
.mixdown-play {
width:24px;
height:24px;
cursor:pointer;
}
.mixdown-edit {
margin-left:10px;
width:24px;
height:24px;
cursor:pointer;
}
.mixdown-delete {
margin-left:10px;
width:24px;
height:24px;
cursor:pointer;
}
.create-mix {
margin-top:5px;
border-color:$ColorTextTypical;
border-style: solid;
border-width:1px 0;
padding: 7px 0 20px;
p {
line-height:125%;
color:$ColorTextTypical;
text-align:left;
font-size:12px;
}
.field {
display:block;
height:25px;
margin-top:15px;
}
ul.error-text {
float:right;
display:block !important;
color: red;
margin-top: 5px;
}
a.create-mix-btn {
margin-top:15px;
float:right;
margin-right: 2px;
margin-top: 3px;
}
label {
display:inline;
float:left;
}
select, input {
width:170px;
float:right;
@include border_box_sizing;
background-color:$ColorTextBoxBackground;
}
}
}
.arrow-down {
float:none;
margin-left:5px;
margin-top:0;
margin-right:0;
border-top: 4px solid #fc0;
border-left: 4px solid transparent;
border-right: 4px solid transparent;
display:inline-block;
padding-top:1px;
}
.arrow-up {
float:none;
margin-right:0;
margin-left:5px;
margin-bottom:2px;
border-bottom: 4px solid #fc0;
border-left: 4px solid transparent;
border-right: 4px solid transparent;
display:inline-block;
padding-top:1px;
}
}

View File

@ -0,0 +1,135 @@
class ApiJamTrackMixdownsController < ApiController
# have to be signed in currently to see this screen
before_filter :api_signed_in_user
before_filter :lookup_jam_track_mixdown, :only => [:download, :enqueue, :update]
before_filter :lookup_jam_track_right, :only => [:download, :enqueue, :update]
respond_to :json
def log
@log || Logging.logger[ApiJamTrackMixdownsController]
end
def index
data = JamTrackMixdown.index(params, current_user)
@jam_track_mixdowns, @next, @count = data[0], data[1], data[2]
render "api_jam_track_mixdowns/index", :layout => nil
end
def show
@jam_track_mixdown = JamTrackMixdown.find(params[:id])
end
def delete
@jam_track_mixdown = JamTrackMixdown.find(params[:id])
@jam_track_mixdown.destroy
render json: {}, status:204
end
def update
@mixdown = JamTrackMixdown.find(params[:id])
@mixdown.name = params[:name] if params[:name]
@mixdown.description = params[:description] if params[:description]
@mixdown.save
if params[:active]
@jam_track_right.last_mixdown = @mixdown
@jam_track_right.save
end
if @mixdown.errors.any?
respond_with_model(@mixdown)
return
else
end
end
def show_package
@package = JamTrackMixdownPackage.find(params[:id])
end
def create
@mixdown = JamTrackMixdown.create(params[:name], params[:description], current_user, JamTrack.find(params[:jamTrackID]), params[:settings])
if @mixdown.errors.any?
respond_with_model(@mixdown)
return
end
end
def download
if @jam_track_right.valid?
begin
@package = JamTrackMixdownPackage.where('jam_track_mixdown_id = ?', @jam_track_mixdown.id).where(file_type: params[:file_type]).where(encrypt_type: params[:encrypt_type]).where(sample_rate: params[:sample_rate]).first
rescue Exception => e
log.error("failed to find mixdown package", e)
render :json => {:message => "unable to locate mixdown package due to error; check arguments"}, :status => 404
return
end
@package = JamTrackMixdownPackage.create(@jam_track_mixdown, params[:file_type], params[:sample_rate], params[:encrypt_type]) unless @package
if @package.errors.any?
respond_with_model(@package)
return
end
if @package.ready?
@package.update_download_count
now = Time.now
@package.last_downloaded_at = now
@package.first_downloaded_at = now if @package.first_downloaded_at.nil?
@package.save!
redirect_to @package.sign_url(120)
else
@package.enqueue_if_needed
render :json => { :message => "not available, digitally signing Jam Track Mixdown offline." }, :status => 202
end
else
render :json => { :message => "download limit surpassed", :errors=>@package.errors }, :status => 403
end
end
def enqueue
if @jam_track_right.valid?
begin
@package = JamTrackMixdownPackage.where('jam_track_mixdown_id = ?', @jam_track_mixdown.id).where(file_type: params[:file_type]).where(encrypt_type: params[:encrypt_type]).where(sample_rate: params[:sample_rate]).first
rescue Exception => e
puts "enqueue failure #{e}"
log.error("failed to find mixdown package #{e}")
render :json => {:message => "unable to locate mixdown package due to error; check arguments"}, :status => 404
return
end
@package = JamTrackMixdownPackage.create(@jam_track_mixdown, params[:file_type], params[:sample_rate], params[:encrypt_type]) unless @package
if @package.errors.any?
respond_with_model(@package)
return
end
enqueued = @package.enqueue_if_needed
log.debug("jamtrack mixdown #{enqueued ? "ENQUEUED" : "NOT ENQUEUED"}: mixdown_package=#{@package.id} ")
@queue_time = enqueued ? enqueued : 0
return
else
render :json => { :message => "download limit surpassed", :errors=>@package.errors }, :status => 403
end
end
private
def lookup_jam_track_right
@jam_track_right = JamTrackRight.where("jam_track_id=? AND user_id=?", @jam_track_mixdown.jam_track.id, current_user.id).first
raise JamPermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR unless @jam_track_right
end
def lookup_jam_track_mixdown
@jam_track_mixdown = JamTrackMixdown.find(params[:id])
end
end # class ApiJamTracksController

View File

@ -1,9 +1,9 @@
class ApiJamTracksController < ApiController
# have to be signed in currently to see this screen
before_filter :api_signed_in_user, :except => [:index, :show, :autocomplete, :show_with_artist_info, :artist_index]
before_filter :api_any_user, :only => [:index, :show, :autocomplete, :show_with_artist_info, :artist_index]
before_filter :lookup_jam_track_right, :only => [:download,:enqueue, :show_jam_track_right]
before_filter :api_signed_in_user, :except => [:index, :autocomplete, :show_with_artist_info, :artist_index]
before_filter :api_any_user, :only => [:index, :autocomplete, :show_with_artist_info, :artist_index]
before_filter :lookup_jam_track_right, :only => [:download,:enqueue, :show_jam_track_right, :mark_active]
respond_to :json
@ -12,7 +12,8 @@ class ApiJamTracksController < ApiController
end
def show
@jam_track = JamTrack.find_by_plan_code!(params[:plan_code])
@jam_track = JamTrack.find(params[:id])
render "api_jam_tracks/show_for_client", :layout => nil
end
def show_with_artist_info
@ -26,6 +27,23 @@ class ApiJamTracksController < ApiController
render "api_jam_tracks/index", :layout => nil
end
def mark_active
mixdown_id = params[:mixdown_id]
@jam_track_right.last_mixdown_id = mixdown_id
@jam_track_right.save
if @jam_track_right.errors.any?
respond_with_model(@jam_track_right)
return
else
@jam_track = @jam_track_right.jam_track
render "api_jam_tracks/show_for_client", :layout => nil
end
end
def autocomplete
autocomplete = JamTrack.autocomplete(params, any_user)
@ -62,7 +80,7 @@ class ApiJamTracksController < ApiController
play.save
if play.errors.any?
render :json => { :message => "Unexpected error occurred" }, :status => 500
render :json => { :message => "Unexpected error occurred" }, :status => 422
else
render :json => {}, :status => 201
end
@ -139,7 +157,10 @@ class ApiJamTracksController < ApiController
end
def keys
puts "Keys"
puts "--------------------------"
jamtrack_holder = params[:jamtracks]
puts jamtrack_holder.inspect
unless jamtrack_holder.kind_of?(Hash)
render :json => {message: 'jamtracks parameter must be an hash'}, :status => 422
@ -155,20 +176,49 @@ class ApiJamTracksController < ApiController
# jamtracks come in the form id-44 or id-48, so we need to do a little extra parsing
# mixdowns come in the form id_mixid-44 or id_mixid-48, so we also need to handle that
jamtrack_ids = Set.new
jamtracks_fq_ids = Set.new
jamtrack_mixdowns = {}
jamtracks.each do |jamtrack|
rindex = jamtrack.rindex('-')
if rindex
id = jamtrack[0..(rindex-1)]
jamtrack_ids << id
# let's see if a mixid is in this ID
rindex = id.rindex('_')
if rindex
# ok, this is id_mixid-44 format; so we need to parse again for the ID
just_id = jamtrack[0..(rindex-1)]
sample_rate = jamtrack[-2..-1]
jamtrack_ids << just_id
simulated_fq_id = "#{just_id}-#{sample_rate}"
mixdown_info = jamtrack_mixdowns[simulated_fq_id]
unless mixdown_info
mixdown_info = []
jamtrack_mixdowns[simulated_fq_id] = mixdown_info
end
mixdown_info << id
else
jamtrack_ids << id
end
jamtracks_fq_ids << jamtrack # includes sample rate
end
end
@jam_tracks = JamTrackRight.list_keys(current_user, jamtrack_ids)
@jamtracks_fq_ids = jamtracks_fq_ids
@jamtrack_mixdowns = jamtrack_mixdowns
puts "jamtrack_mixdowns #{jamtrack_mixdowns}"
end
private

View File

@ -7,7 +7,7 @@ class ApiSearchController < ApiController
def index
if 1 == params[Search::PARAM_MUSICIAN].to_i || 1 == params[Search::PARAM_BAND].to_i
query = parasobj.clone
query = params.clone
query[:remote_ip] = request.remote_ip
if 1 == query[Search::PARAM_MUSICIAN].to_i
@search = Search.musician_filter(query, current_user)

View File

@ -1,5 +1,6 @@
require 'sanitize'
class ApiUsersController < ApiController
class
ApiUsersController < ApiController
before_filter :api_signed_in_user, :except => [:create, :calendar, :show, :signup_confirm, :auth_session_create, :complete, :finalize_update_email, :isp_scoring, :add_play, :crash_dump, :validate_data]
before_filter :auth_user, :only => [:session_settings_show, :session_history_index, :session_user_history_index, :update, :delete,
@ -575,14 +576,14 @@ class ApiUsersController < ApiController
# This should largely be moved into a library somewhere in jam-ruby.
def crash_dump
# example of using curl to access this API:
# curl -L -T some_file -X PUT http://localhost:3000/api/dumps?client_type=[MACOSX/Win32/JamBox]&client_version=[VERSION]&client_id=[CLIENT_ID]&session_id=[SESSION_ID]&timestamp=[TIMESTAMP]
# curl -L -T some_file -X PUT http://localhost:3000/api/dumps?client_type=[MacOSX/Win32/JamBox]&client_version=[VERSION]&client_id=[CLIENT_ID]&session_id=[SESSION_ID]&timestamp=[TIMESTAMP]
# user_id is deduced if possible from the user's cookie.
@dump = CrashDump.new
@dump.client_type = params[:client_type]
@dump.client_version = params[:client_version]
@dump.client_id = params[:client_id]
@dump.user_id = current_user.try(:id)
@dump.user_id = params[:user_id]
@dump.session_id = params[:session_id]
@dump.timestamp = params[:timestamp]
@ -603,7 +604,7 @@ class ApiUsersController < ApiController
read_url = bucket.objects[uri].url_for(:read,
:expires => expire,
:'response_content_type' => 'application/octet-stream').to_s
@dump.update_attribute(:uri, read_url)
#@dump.update_attribute(:uri, read_url)
write_url = bucket.objects[uri].url_for(:write,
:expires => Rails.application.config.crash_dump_data_signed_url_timeout,

View File

@ -0,0 +1,3 @@
object @mixdown
extends "api_jam_track_mixdowns/show"

View File

@ -0,0 +1,7 @@
object @package
node :queue_time do
@queue_time
end
extends "api_jam_track_mixdowns/show_package"

View File

@ -0,0 +1,11 @@
node :next do |page|
@next
end
node :count do |page|
@count
end
node :mixdowns do |page|
partial "api_jam_track_mixdowns/show", object: @jam_track_mixdowns
end

View File

@ -0,0 +1,13 @@
object @jam_track_mixdown
attributes :id, :name, :description, :jam_track_id
node :settings do |item|
JSON.parse(item.settings)
end
child(:jam_track_mixdown_packages => :packages) {
node do |package|
partial("api_jam_track_mixdowns/show_package", :object => package)
end
}

View File

@ -0,0 +1,3 @@
object @package
attributes :id, :jam_track_mixdown_id, :file_type, :sample_rate, :encrypt_type, :error_count, :error_reason, :error_detail, :signing_state, :packaging_steps, :current_packaging_step, :version

View File

@ -0,0 +1,3 @@
object @mixdown
extends "api_jam_track_mixdowns/show"

View File

@ -17,7 +17,35 @@ node do |jam_track|
private: jam_track['private_key_48'],
error: jam_track['private_key_48'] ? nil : ( jam_track['jam_track_right_id'] ? 'no_key' : 'not_purchased' )
}
end
# now include mixdown info
mixdowns_44 = []
mixdown_info = @jamtrack_mixdowns[id]
if mixdown_info
mixdown_info.each do |mixdown_id|
mixdowns_44 << {
id: mixdown_id + '-44',
private: jam_track['private_key_44'],
error: jam_track['private_key_44'] ? nil : ( jam_track['jam_track_right_id'] ? 'no_key' : 'not_purchased' )
}
end
end
result['mixdowns_44'] = mixdowns_44
# now include mixdown info
mixdowns_48 = []
mixdown_info = @jamtrack_mixdowns[id + '-48']
if mixdown_info
mixdown_info.each do |mixdown_id|
mixdowns_48 << {
id: mixdown_id + '-48',
private: jam_track['private_key_48'],
error: jam_track['private_key_48'] ? nil : ( jam_track['jam_track_right_id'] ? 'no_key' : 'not_purchased' )
}
end
end
result['mixdowns_48'] = mixdowns_48
result
end

View File

@ -18,6 +18,19 @@ child(:jam_track_tracks => :tracks) {
attributes :id, :part, :instrument, :track_type
}
node :last_mixdown_id do |jam_track|
jam_track.right_for_user(current_user).last_mixdown_id
end
node :mixdowns do |jam_track|
items = []
jam_track.mixdowns_for_user(current_user).each do |mixdown|
items << partial("api_jam_track_mixdowns/show", :object => mixdown)
end
items
end
child(:jam_track_tap_ins => :tap_ins) {
attributes :offset_time, :bpm, :tap_in_count
}

View File

@ -81,6 +81,12 @@ else
child({:jam_track => :jam_track}, :if => lambda { |music_session| music_session.users.exists?(current_user) }) {
attributes :id, :name, :description
node :mixdown do |jam_track|
right = jam_track.right_for_user(User.find(@music_session.jam_track_initiator_id))
{id: right ? right.last_mixdown_id : nil}
end
child(:jam_track_tracks => :tracks) {
attributes :id, :part, :instrument, :track_type
}

View File

@ -349,4 +349,7 @@ script type="text/template" id="template-help-ftue-video-disable"
li If you know you never want to see anyone else's video.
li If you are experiencing technical problems with others send you video.
script type="text/template" id="template-help-no-change-while-loading"
span Certain actions are disabled while a track is being loaded.

View File

@ -227,6 +227,18 @@ if defined?(Bundler)
# amount of time to allow before giving up on a single step in packaging job
config.signing_step_max_time = 60; # 60 seconds
config.signing_job_signing_max_time = 300
# amount of time before we think the queue is stuck
config.signing_job_queue_max_time = 300
# amount of time to allow before giving up on a single step in packaging job
config.mixdown_step_max_time = 300
config.mixdown_job_queue_max_time = 300
config.estimated_jam_track_time = 40
config.estimated_fast_mixdown_time = 30
config.estimated_slow_mixdown_time = 80
config.num_packaging_nodes = 2
config.email_alerts_alias = 'alerts@jamkazam.com' # should be used for 'oh no' server down/service down sorts of emails
config.email_generic_from = 'nobody@jamkazam.com'
config.email_recurly_notice = 'recurly-alerts@jamkazam.com'
@ -355,5 +367,7 @@ if defined?(Bundler)
config.react.variant = :production
config.react.addons = true
config.time_shift_style = :sbsms # or sox
end
end

View File

@ -98,4 +98,6 @@ SampleApp::Application.configure do
config.guard_against_fraud = true
config.react.variant = :development
config.time_shift_style = :sox # or sbsms
end

View File

@ -240,7 +240,7 @@ SampleApp::Application.routes.draw do
match '/jamtracks/autocomplete' => 'api_jam_tracks#autocomplete', :via => :get, :as => 'api_jam_tracks_autocomplete'
match '/jamtracks/purchased' => 'api_jam_tracks#purchased', :via => :get, :as => 'api_jam_tracks_purchased'
match '/jamtracks/artists' => 'api_jam_tracks#artist_index', :via => :get, :as => 'api_jam_tracks_list_artists'
match '/jamtracks/:plan_code' => 'api_jam_tracks#show', :via => :get, :as => 'api_jam_tracks_show'
match '/jamtracks/:id' => 'api_jam_tracks#show', :via => :get, :as => 'api_jam_tracks_show'
match '/jamtracks/band/:plan_code' => 'api_jam_tracks#show_with_artist_info', :via => :get, :as => 'api_jam_tracks_show_with_artist_info'
match '/jamtracks' => 'api_jam_tracks#index', :via => :get, :as => 'api_jam_tracks_list'
match '/jamtracks/download/:id' => 'api_jam_tracks#download', :via => :get, :as => 'api_jam_tracks_download'
@ -249,6 +249,18 @@ SampleApp::Application.routes.draw do
match '/jamtracks/rights/:id' => 'api_jam_tracks#show_jam_track_right', :via => :get, :as => 'api_jam_tracks_show_right'
match '/jamtracks/keys' => 'api_jam_tracks#keys', :via => :post, :as => 'api_jam_tracks_keys'
# mixdowns
match '/jamtracks/:id/mixdowns/active' => 'api_jam_tracks#mark_active', :via => :POST
match '/jamtracks/:id/mixdowns' => 'api_jam_track_mixdowns#index', :via => :get
match '/mixdowns/:id/download' => 'api_jam_track_mixdowns#download', :via => :get
match '/mixdowns/:id/enqueue' => 'api_jam_track_mixdowns#enqueue', :via => :post
match '/mixdowns/:id' => 'api_jam_track_mixdowns#show', :via => :get
match '/mixdowns/:id' => 'api_jam_track_mixdowns#update', :via => :post
match '/mixdowns' => 'api_jam_track_mixdowns#create', :via => :post
match '/mixdowns/:id' => 'api_jam_track_mixdowns#delete', :via => :delete
match '/mixdown_packages/:id' => 'api_jam_track_mixdowns#show_package', :via => :get
# Shopping carts
match '/shopping_carts/add_jamtrack' => 'api_shopping_carts#add_jamtrack', :via => :post
match '/shopping_carts' => 'api_shopping_carts#index', :via => :get
@ -565,7 +577,7 @@ SampleApp::Application.routes.draw do
match '/artifacts/clients' => 'artifacts#client_downloads'
# crash logs
match '/dumps' => 'api_users#crash_dump', :via => :put
match '/crashes' => 'api_users#crash_dump', :via => :put
# feedback from corporate site api
match '/feedback' => 'api_corporate#feedback', :via => :post

View File

@ -16,9 +16,9 @@ IcecastSourceCheck:
description: "Finds icecast mounts that need their 'sourced' state to change, but haven't in some time"
JamTracksCleaner:
cron: "0 5 * * *"
class: "JamRuby::UnusedMusicNotationCleaner"
description: "Remove unused music notations"
cron: "0,30 * * * *"
class: "JamRuby::JamTracksCleaner"
description: "Clean up JamTrack related stuff; every 30 minutes"
CleanupFacebookSignup:
cron: "30 2 * * *"

View File

@ -158,4 +158,29 @@ namespace :jam_tracks do
mapper = TencyStemMapping.new
mapper.correlate
end
task generate_private_key: :environment do |task, arg|
JamTrackRight.all.each do |right|
if right.private_key_44.nil? || right.private_key_48.nil?
if right.private_key_44.nil? && right.private_key_48
right.private_key_44 = right.private_key_48
puts "COPY 48 > 44"
elsif right.private_key_48.nil? && right.private_key_44
right.private_key_48 = right.private_key_44
puts "COPY 44 > 48"
elsif right.private_key_48.nil? && right.private_key_44.nil?
rsa_key = OpenSSL::PKey::RSA.new(1024)
key = rsa_key.to_pem()
right.private_key_44 = key
right.private_key_48 = key
puts "GEN 44 + 48"
end
right.save
else
puts "OK 44 + 48"
end
end
end
end

View File

@ -0,0 +1,148 @@
require 'spec_helper'
describe ApiJamTrackMixdownsController, type: :controller do
render_views
let(:user) { FactoryGirl.create(:user) }
let(:jam_track) { FactoryGirl.create(:jam_track) }
let(:mixdown) { FactoryGirl.create(:jam_track_mixdown, user: user, jam_track: jam_track) }
let(:jam_track_right) { FactoryGirl.create(:jam_track_right, jam_track: jam_track, user:user)}
let(:package) {FactoryGirl.create(:jam_track_mixdown_package, jam_track_mixdown: mixdown)}
before(:each) do
controller.current_user = user
JamTrackMixdown.destroy_all
end
describe "index" do
it "one result" do
# make a mixdown with no packages
get :index, {id: mixdown.jam_track.id}
response.status.should eq(200)
json = JSON.parse(response.body)
json["next"].should be_nil
json["count"].should eq(1)
json["mixdowns"][0]["settings"].should eq({"speed" => 5})
# and then add a package
package = FactoryGirl.create(:jam_track_mixdown_package, jam_track_mixdown: mixdown)
get :index, {id: mixdown.jam_track.id}
response.status.should eq(200)
json = JSON.parse(response.body)
json["next"].should be_nil
json["count"].should eq(1)
json["mixdowns"][0]["packages"][0]["signing_state"].should eq('QUIET')
end
end
describe "create" do
it "success" do
post :create, {:format => 'json', jamTrackID: jam_track.id, name: 'some name', description: 'some description', settings: {speed:5}}
response.status.should eq(200)
json = JSON.parse(response.body)
json["name"].should eq('some name')
json["jam_track_id"].should eq(jam_track.id)
json["description"].should eq('some description')
json["settings"].should eq({"speed" => 5})
json["packages"].should eq([])
end
it "validates name" do
post :create, {:format => 'json', jamTrackID: jam_track.id, description: 'some description', settings: {speed:5}}
response.status.should eq(422)
json = JSON.parse(response.body)
json["errors"]["name"].should eq(["can't be blank"])
end
end
describe "enqueue" do
it "success" do
jam_track_right.touch
post :enqueue, {:format => 'json', id: mixdown.id, file_type: JamTrackMixdownPackage::FILE_TYPE_AAC, encrypt_type: nil, sample_rate: 48}
response.status.should eq(200)
json = JSON.parse(response.body)
puts json
json["id"].should_not be_nil
package = JamTrackMixdownPackage.find(json["id"])
package.file_type.should eq(JamTrackMixdownPackage::FILE_TYPE_AAC)
package.encrypt_type.should eq(nil)
package.sample_rate.should eq(48)
end
it "validates file_type" do
jam_track_right.touch
post :enqueue, {:format => 'json', id: mixdown.id, file_type: 'wrong', encrypt_type: nil, sample_rate: 48}
response.status.should eq(422)
json = JSON.parse(response.body)
json["errors"]["file_type"].should eq(["is not included in the list"])
end
it "finds existing package to enqueue" do
jam_track_right.touch
package.touch
JamTrackMixdownPackage.count.should eq(1)
package.jam_track_mixdown.should eq(mixdown)
post :enqueue, {:format => 'json', id: mixdown.id, file_type: package.file_type, encrypt_type: package.encrypt_type, sample_rate: package.sample_rate}
response.status.should eq(200)
json = JSON.parse(response.body)
puts json
json["id"].should eq(package.id)
JamTrackMixdownPackage.count.should eq(1)
end
end
describe "download" do
it "enqueues if not available" do
jam_track_right.touch
package.touch
post :download, {:format => 'json', id: mixdown.id, file_type: package.file_type, encrypt_type: package.encrypt_type, sample_rate: package.sample_rate}
response.status.should eq(202)
json = JSON.parse(response.body)
json["message"].should eq("not available, digitally signing Jam Track Mixdown offline.")
package.reload
package.signing_state.should eq('QUEUED')
end
it "success" do
jam_track_right.touch
package.touch
package.enqueue_if_needed
package.signed = true
package.url = 'some/bogus/place'
package.save!
post :download, {:format => 'json', id: mixdown.id, file_type: package.file_type, encrypt_type: package.encrypt_type, sample_rate: package.sample_rate}
response.status.should eq(302)
response['Location'].should include('/some/bogus/place')
end
end
end

View File

@ -115,7 +115,7 @@ describe ApiJamTracksController do
it "handle api call 500" do
post :played, { id: 999, user: @user }
expect(response.status).to eq(500)
expect(response.status).to eq(422)
json = JSON.parse(response.body)
expect(/Unexpected error occurred/).to match(json['message'])
end
@ -155,8 +155,8 @@ describe ApiJamTracksController do
get :download, :id=>@jam_track.id, sample_rate: 48, all_fp: 'all', running_fp: 'running'
response.status.should == 202
right.download_count.should eq(0)
right.private_key_44.should be_nil
right.private_key_48.should be_nil
right.private_key_44.should_not be_nil
right.private_key_48.should_not be_nil
qname = "#{ResqueSpec.queue_name(JamRuby::JamTracksBuilder)}"
#puts "ResqueSpec.peek(qname)#{ResqueSpec.peek(qname)}"
@ -167,7 +167,7 @@ describe ApiJamTracksController do
JamTracksBuilder.should_not have_queued(right.id,nil).in(:jam_tracks_builder)
right.reload
right.private_key_44.should be_nil
right.private_key_44.should_not be_nil
right.private_key_48.should_not be_nil
right.download_count.should eq(0)
@ -186,8 +186,8 @@ describe ApiJamTracksController do
get :download, :id=>@jam_track.id, :sample_rate=>44, all_fp: 'all', running_fp: 'running'
response.status.should == 202
right.download_count.should eq(0)
right.private_key_44.should be_nil
right.private_key_48.should be_nil
right.private_key_44.should_not be_nil
right.private_key_48.should_not be_nil
qname = "#{ResqueSpec.queue_name(JamRuby::JamTracksBuilder)}"
#puts "ResqueSpec.peek(qname)#{ResqueSpec.peek(qname)}"
@ -199,7 +199,7 @@ describe ApiJamTracksController do
JamTracksBuilder.should_not have_queued(right.id, 44).in(:jam_tracks_builder)
right.reload
right.private_key_44.should_not be_nil
right.private_key_48.should be_nil
right.private_key_48.should_not be_nil
right.download_count.should eq(0)
get :download, :id=>@jam_track.id, :sample_rate=>44, all_fp: 'all', running_fp: 'running'
@ -239,11 +239,11 @@ describe ApiJamTracksController do
json = JSON.parse(response.body)
json.length.should == 1
json[0]['44'].should_not be_nil
json[0]['44']['private'].should be_nil
json[0]['44']['error'].should == 'no_key'
json[0]['44']['private'].should_not be_nil
json[0]['44']['error'].should be_nil
json[0]['48'].should_not be_nil
json[0]['48']['private'].should be_nil
json[0]['48']['error'].should == 'no_key'
json[0]['48']['private'].should_not be_nil
json[0]['48']['error'].should be_nil
end
it "track with key" do
@ -254,11 +254,11 @@ describe ApiJamTracksController do
json.length.should == 1
json[0]['id'].should == @jam_track.id.to_s
json[0]['44'].should_not be_nil
json[0]['44']['private'].should eq('abc')
json[0]['44']['private'].should eq(right.private_key_44)
json[0]['44']['error'].should be_nil
json[0]['48'].should_not be_nil
json[0]['48']['private'].should be_nil
json[0]['48']['error'].should == 'no_key'
json[0]['48']['private'].should eq(right.private_key_48)
json[0]['48']['error'].should be_nil
end
it "non-owning user asking for a real track" do

View File

@ -711,6 +711,23 @@ 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 '{"speed":5}'
end
factory :jam_track_mixdown_package, :class => JamRuby::JamTrackMixdownPackage do
file_type JamRuby::JamTrackMixdownPackage::FILE_TYPE_OGG
sample_rate 48
signing false
signed false
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}" }

View File

@ -53,7 +53,7 @@ describe "Individual JamTrack", :js => true, :type => :feature, :capybara_featur
end
end
find('.browse-band a')['href'].should eq("/client?artist=#{jamtrack_acdc_backinblack.original_artist}#/jamtrack/search")
find('.browse-all a')['href'].should eq("/client#/jamtrack/search")
find('.browse-all a')['href'].should eq("/client?search=#/jamtrack/search")
find('a.cta-free-jamtrack')['href'].should eq("/client#/jamtrack/search")
find('a.cta-free-jamtrack').trigger(:click)
find('h1', text: 'check out')

View File

@ -262,10 +262,13 @@ describe "Active Music Session API ", :type => :api do
login(user2)
get location_header + ".json", "CONTENT_TYPE" => 'application/json'
participant = JSON.parse(last_response.body)
music_session = JSON.parse(last_response.body)
# and the creator should be in the session
# and the second person should be in the session
# and should have tracks
music_session["participants"].length.should == 2
participant = music_session["participants"][1]
participant["tracks"].length.should == 1
participant["tracks"][0]["instrument_id"].should == 'bass guitar'
participant["tracks"][0]["sound"].should == 'mono'
@ -451,18 +454,18 @@ describe "Active Music Session API ", :type => :api do
# users are friends, but no invitation... so we shouldn't be able to join as user 2
login(user2)
post "/api/sessions/#{session["music_session_id"]}/participants.json", { :client_id => client2.client_id, :as_musician => true, :tracks => [{"instrument_id" => "bass guitar", "sound" => "mono", "client_track_id" => "client_track_guid"}]}.to_json, "CONTENT_TYPE" => 'application/json'
post "/api/sessions/#{music_session["id"]}/participants.json", { :client_id => client2.client_id, :as_musician => true, :tracks => [{"instrument_id" => "bass guitar", "sound" => "mono", "client_track_id" => "client_track_guid"}]}.to_json, "CONTENT_TYPE" => 'application/json'
last_response.status.should eql(422)
join_response = JSON.parse(last_response.body)
join_response["errors"]["musician_access"].should == [ValidationMessages::INVITE_REQUIRED]
# but let's make sure if we then invite, that we can then join'
login(user)
post '/api/invitations.json', { :music_session => session["music_session_id"], :receiver => user2.id }.to_json, "CONTENT_TYPE" => 'application/json'
post '/api/invitations.json', { :music_session => music_session["id"], :receiver => user2.id }.to_json, "CONTENT_TYPE" => 'application/json'
last_response.status.should eql(201)
login(user2)
post "/api/sessions/#{session["music_session_id"]}/participants.json", { :client_id => client2.client_id, :as_musician => true, :tracks => [{"instrument_id" => "bass guitar", "sound" => "mono", "client_track_id" => "client_track_guid"}] }.to_json, "CONTENT_TYPE" => 'application/json'
post "/api/sessions/#{music_session["id"]}/participants.json", { :client_id => client2.client_id, :as_musician => true, :tracks => [{"instrument_id" => "bass guitar", "sound" => "mono", "client_track_id" => "client_track_guid"}] }.to_json, "CONTENT_TYPE" => 'application/json'
last_response.status.should eql(201)
end

View File

@ -106,6 +106,22 @@ def web_config
def google_public_server_key
"AIzaSyCPTPq5PEcl4XWcm7NZ2IGClZlbsiE8JNo"
end
def estimated_jam_track_time
40
end
def estimated_fast_mixdown_time
30
end
def estimated_slow_mixdown_time
80
end
def num_packaging_nodes
2
end
end
klass.new
end