From 30965c6351a4db3897617a0b0d9ae8aabd06d930 Mon Sep 17 00:00:00 2001 From: Seth Call Date: Tue, 15 Sep 2015 05:23:27 -0500 Subject: [PATCH 01/25] * allow jamblaster to fetch http --- ruby/lib/jam_ruby/models/jam_track_right.rb | 4 ++-- web/app/controllers/api_jam_tracks_controller.rb | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/ruby/lib/jam_ruby/models/jam_track_right.rb b/ruby/lib/jam_ruby/models/jam_track_right.rb index f0749dc94..5217b620d 100644 --- a/ruby/lib/jam_ruby/models/jam_track_right.rb +++ b/ruby/lib/jam_ruby/models/jam_track_right.rb @@ -99,9 +99,9 @@ module JamRuby # 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, bitrate=48) + def sign_url(expiration_time = 120, bitrate=48, secure=true) field_name = (bitrate==48) ? "url_48" : "url_44" - s3_manager.sign_url(self[field_name], {:expires => expiration_time, :secure => true}) + s3_manager.sign_url(self[field_name], {:expires => expiration_time, :secure => secure}) end def delete_s3_files diff --git a/web/app/controllers/api_jam_tracks_controller.rb b/web/app/controllers/api_jam_tracks_controller.rb index 52b57bc77..428018462 100644 --- a/web/app/controllers/api_jam_tracks_controller.rb +++ b/web/app/controllers/api_jam_tracks_controller.rb @@ -101,7 +101,11 @@ class ApiJamTracksController < ApiController @jam_track_right.last_downloaded_at = now @jam_track_right.first_downloaded_at = now if @jam_track_right.first_downloaded_at.nil? @jam_track_right.save! - redirect_to @jam_track_right.sign_url(120, sample_rate) + + is_jamblaster = !!params[:is_jamblaster] + + # if it's not the jamblaster, keep the URL https + redirect_to @jam_track_right.sign_url(120, sample_rate, !is_jamblaster) else @jam_track_right.enqueue_if_needed(sample_rate) render :json => { :message => "not available, digitally signing Jam Track offline." }, :status => 202 From 4a647b8bd18d306f8f305452b79b65d0520c4312 Mon Sep 17 00:00:00 2001 From: Seth Call Date: Mon, 21 Sep 2015 20:01:39 -0500 Subject: [PATCH 02/25] * VRFS-3519 - client and server jamtrack mixdown support --- db/manifest | 3 +- db/up/mixdown.sql | 61 +++ pb/src/client_container.proto | 17 + ruby/lib/jam_ruby.rb | 3 + .../jam_ruby/constants/notification_types.rb | 3 + ruby/lib/jam_ruby/jam_track_importer.rb | 7 +- ruby/lib/jam_ruby/lib/subscription_message.rb | 20 +- ruby/lib/jam_ruby/message_factory.rb | 24 + ruby/lib/jam_ruby/models/crash_dump.rb | 2 +- ruby/lib/jam_ruby/models/jam_track.rb | 5 +- ruby/lib/jam_ruby/models/jam_track_mixdown.rb | 119 +++++ .../models/jam_track_mixdown_package.rb | 249 +++++++++ ruby/lib/jam_ruby/models/jam_track_right.rb | 12 +- ruby/lib/jam_ruby/models/notification.rb | 27 +- .../resque/jam_track_mixdown_packager.rb | 461 ++++++++++++++++ .../lib/jam_ruby/resque/jam_tracks_builder.rb | 3 +- .../resque/scheduled/jam_tracks_cleaner.rb | 5 + .../jam_ruby/resque/scheduled/stats_maker.rb | 1 + ruby/spec/factories.rb | 17 + .../models/jam_track_mixdown_package_spec.rb | 103 ++++ .../jam_ruby/models/jam_track_mixdown_spec.rb | 75 +++ .../jam_ruby/models/jam_track_right_spec.rb | 17 +- ruby/spec/support/utilities.rb | 30 +- web/Gemfile | 2 + .../javascripts/dialog/openJamTrackDialog.js | 3 +- .../javascripts/download_jamtrack.js.coffee | 2 +- web/app/assets/javascripts/fakeJamClient.js | 8 + web/app/assets/javascripts/jam_rest.js | 86 +++ .../assets/javascripts/react-components.js | 1 + .../MediaControls.js.jsx.coffee | 12 +- .../PopupMediaControls.js.jsx.coffee | 477 ++++++++++++++++- .../SessionMediaTracks.js.jsx.coffee | 25 +- .../SessionTrackVolumeHover.js.jsx.coffee | 5 +- .../actions/JamTrackActions.js.coffee | 1 + .../actions/JamTrackMixdownActions.js.coffee | 13 + .../actions/SessionActions.js.coffee | 1 + .../helpers/MixerHelper.js.coffee | 122 +++-- .../helpers/SessionHelper.js.coffee | 3 + .../mixins/SessionMediaTracksMixin.js.coffee | 1 + .../stores/JamTrackMixdownStore.js.coffee | 90 ++++ .../stores/JamTrackStore.js.coffee | 493 +++++++++++++++++- .../stores/SessionStore.js.coffee | 6 + web/app/assets/javascripts/utils.js | 18 + .../minimal/media_controls.css.scss | 203 ++++++++ .../api_jam_track_mixdowns_controller.rb | 135 +++++ .../controllers/api_jam_tracks_controller.rb | 62 ++- web/app/controllers/api_search_controller.rb | 2 +- web/app/controllers/api_users_controller.rb | 9 +- .../views/api_jam_track_mixdowns/create.rabl | 3 + .../views/api_jam_track_mixdowns/enqueue.rabl | 7 + .../views/api_jam_track_mixdowns/index.rabl | 11 + .../views/api_jam_track_mixdowns/show.rabl | 13 + .../api_jam_track_mixdowns/show_package.rabl | 3 + .../views/api_jam_track_mixdowns/update.rabl | 3 + web/app/views/api_jam_tracks/keys.rabl | 28 + .../views/api_jam_tracks/show_for_client.rabl | 13 + web/app/views/api_music_sessions/show.rabl | 6 + web/app/views/clients/_help.html.slim | 3 + web/config/application.rb | 14 + web/config/environments/development.rb | 2 + web/config/routes.rb | 16 +- web/config/scheduler.yml | 6 +- web/lib/tasks/jam_tracks.rake | 25 + .../api_jam_track_mixdowns_controller_spec.rb | 148 ++++++ .../api_jam_tracks_controller_spec.rb | 28 +- web/spec/factories.rb | 17 + web/spec/features/individual_jamtrack_spec.rb | 2 +- .../active_music_sessions_api_spec.rb | 13 +- web/spec/support/app_config.rb | 16 + 69 files changed, 3289 insertions(+), 132 deletions(-) create mode 100644 db/up/mixdown.sql create mode 100644 ruby/lib/jam_ruby/models/jam_track_mixdown.rb create mode 100644 ruby/lib/jam_ruby/models/jam_track_mixdown_package.rb create mode 100644 ruby/lib/jam_ruby/resque/jam_track_mixdown_packager.rb create mode 100644 ruby/spec/jam_ruby/models/jam_track_mixdown_package_spec.rb create mode 100644 ruby/spec/jam_ruby/models/jam_track_mixdown_spec.rb create mode 100644 web/app/assets/javascripts/react-components/actions/JamTrackMixdownActions.js.coffee create mode 100644 web/app/assets/javascripts/react-components/stores/JamTrackMixdownStore.js.coffee create mode 100644 web/app/controllers/api_jam_track_mixdowns_controller.rb create mode 100644 web/app/views/api_jam_track_mixdowns/create.rabl create mode 100644 web/app/views/api_jam_track_mixdowns/enqueue.rabl create mode 100644 web/app/views/api_jam_track_mixdowns/index.rabl create mode 100644 web/app/views/api_jam_track_mixdowns/show.rabl create mode 100644 web/app/views/api_jam_track_mixdowns/show_package.rabl create mode 100644 web/app/views/api_jam_track_mixdowns/update.rabl create mode 100644 web/spec/controllers/api_jam_track_mixdowns_controller_spec.rb diff --git a/db/manifest b/db/manifest index 92a320cd7..9b209f892 100755 --- a/db/manifest +++ b/db/manifest @@ -302,4 +302,5 @@ jam_track_onboarding_enhancements.sql jam_track_name_drop_unique.sql jam_track_searchability.sql harry_fox_agency.sql -jam_track_slug.sql \ No newline at end of file +jam_track_slug.sql +mixdown.sql \ No newline at end of file diff --git a/db/up/mixdown.sql b/db/up/mixdown.sql new file mode 100644 index 000000000..7c72e03e8 --- /dev/null +++ b/db/up/mixdown.sql @@ -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); \ No newline at end of file diff --git a/pb/src/client_container.proto b/pb/src/client_container.proto index 5d5ecf100..4acccc86d 100644 --- a/pb/src/client_container.proto +++ b/pb/src/client_container.proto @@ -82,6 +82,10 @@ message ClientMessage { JAM_TRACK_SIGN_COMPLETE = 260; JAM_TRACK_SIGN_FAILED = 261; + // jamtracks mixdown notifications + MIXDOWN_SIGN_COMPLETE = 270; + MIXDOWN_SIGN_FAILED = 271; + TEST_SESSION_MESSAGE = 295; PING_REQUEST = 300; @@ -188,6 +192,10 @@ message ClientMessage { optional JamTrackSignComplete jam_track_sign_complete = 260; optional JamTrackSignFailed jam_track_sign_failed = 261; + // jamtrack mixdown notification + optional MixdownSignComplete mixdown_sign_complete = 270; + optional MixdownSignFailed mixdown_sign_failed = 271; + // Client-Session messages (to/from) optional TestSessionMessage test_session_message = 295; @@ -612,6 +620,15 @@ message JamTrackSignFailed { required int32 jam_track_right_id = 1; // jam track right id } +message MixdownSignComplete { + required 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 diff --git a/ruby/lib/jam_ruby.rb b/ruby/lib/jam_ruby.rb index 16890a9d9..8f62cecf9 100755 --- a/ruby/lib/jam_ruby.rb +++ b/ruby/lib/jam_ruby.rb @@ -64,6 +64,7 @@ require "jam_ruby/resque/scheduled/jam_tracks_cleaner" require "jam_ruby/resque/scheduled/stats_maker" require "jam_ruby/resque/scheduled/tally_affiliates" require "jam_ruby/resque/jam_tracks_builder" +require "jam_ruby/resque/jam_track_mixdown_packager" require "jam_ruby/resque/google_analytics_event" require "jam_ruby/resque/batch_email_job" require "jam_ruby/resque/long_running" @@ -209,6 +210,8 @@ require "jam_ruby/models/jam_track_track" require "jam_ruby/models/jam_track_right" require "jam_ruby/models/jam_track_tap_in" require "jam_ruby/models/jam_track_file" +require "jam_ruby/models/jam_track_mixdown" +require "jam_ruby/models/jam_track_mixdown_package" require "jam_ruby/models/genre_jam_track" require "jam_ruby/app/mailers/async_mailer" require "jam_ruby/app/mailers/batch_mailer" diff --git a/ruby/lib/jam_ruby/constants/notification_types.rb b/ruby/lib/jam_ruby/constants/notification_types.rb index e05c8e00e..60dfe9f1b 100644 --- a/ruby/lib/jam_ruby/constants/notification_types.rb +++ b/ruby/lib/jam_ruby/constants/notification_types.rb @@ -51,4 +51,7 @@ module NotificationTypes JAM_TRACK_SIGN_COMPLETE = "JAM_TRACK_SIGN_COMPLETE" JAM_TRACK_SIGN_FAILED = "JAM_TRACK_SIGN_FAILED" + MIXDOWN_SIGN_COMPLETE = "MIXDOWN_SIGN_COMPLETE" + MIXDOWN_SIGN_FAILED = "MIXDOWN_SIGN_FAILED" + end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/jam_track_importer.rb b/ruby/lib/jam_ruby/jam_track_importer.rb index 403c75330..c6ef0b135 100644 --- a/ruby/lib/jam_ruby/jam_track_importer.rb +++ b/ruby/lib/jam_ruby/jam_track_importer.rb @@ -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 diff --git a/ruby/lib/jam_ruby/lib/subscription_message.rb b/ruby/lib/jam_ruby/lib/subscription_message.rb index 6be9f7d16..02b98f3b9 100644 --- a/ruby/lib/jam_ruby/lib/subscription_message.rb +++ b/ruby/lib/jam_ruby/lib/subscription_message.rb @@ -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 diff --git a/ruby/lib/jam_ruby/message_factory.rb b/ruby/lib/jam_ruby/message_factory.rb index e8cf40b1b..6b7e98034 100644 --- a/ruby/lib/jam_ruby/message_factory.rb +++ b/ruby/lib/jam_ruby/message_factory.rb @@ -736,6 +736,30 @@ module JamRuby ) end + def mixdown_sign_complete(receiver_id, mixdown_package_id) + signed = Jampb::MixdownSignComplete.new( + :mixdown_package_id => mixdown_package_id + ) + + Jampb::ClientMessage.new( + :type => ClientMessage::Type::MIXDOWN_SIGN_COMPLETE, + :route_to => USER_TARGET_PREFIX + receiver_id, #:route_to => CLIENT_TARGET, + :mixdown_sign_complete => signed + ) + end + + def mixdown_sign_failed(receiver_id, mixdown_package_id) + signed = Jampb::MixdownSignFailed.new( + :mixdown_package_id => mixdown_package_id + ) + + Jampb::ClientMessage.new( + :type => ClientMessage::Type::MIXDOWN_SIGN_FAILED, + :route_to => USER_TARGET_PREFIX + receiver_id, #:route_to => CLIENT_TARGET, + :mixdown_sign_failed=> signed + ) + end + def recording_master_mix_complete(receiver_id, recording_id, claimed_recording_id, band_id, msg, notification_id, created_at) recording_master_mix_complete = Jampb::RecordingMasterMixComplete.new( diff --git a/ruby/lib/jam_ruby/models/crash_dump.rb b/ruby/lib/jam_ruby/models/crash_dump.rb index 6c4e54d84..bab31fd97 100644 --- a/ruby/lib/jam_ruby/models/crash_dump.rb +++ b/ruby/lib/jam_ruby/models/crash_dump.rb @@ -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 diff --git a/ruby/lib/jam_ruby/models/jam_track.rb b/ruby/lib/jam_ruby/models/jam_track.rb index 29995e9ad..3bf55444d 100644 --- a/ruby/lib/jam_ruby/models/jam_track.rb +++ b/ruby/lib/jam_ruby/models/jam_track.rb @@ -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 diff --git a/ruby/lib/jam_ruby/models/jam_track_mixdown.rb b/ruby/lib/jam_ruby/models/jam_track_mixdown.rb new file mode 100644 index 000000000..59527b659 --- /dev/null +++ b/ruby/lib/jam_ruby/models/jam_track_mixdown.rb @@ -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 + diff --git a/ruby/lib/jam_ruby/models/jam_track_mixdown_package.rb b/ruby/lib/jam_ruby/models/jam_track_mixdown_package.rb new file mode 100644 index 000000000..687727f4c --- /dev/null +++ b/ruby/lib/jam_ruby/models/jam_track_mixdown_package.rb @@ -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 + diff --git a/ruby/lib/jam_ruby/models/jam_track_right.rb b/ruby/lib/jam_ruby/models/jam_track_right.rb index 5217b620d..f48ceb498 100644 --- a/ruby/lib/jam_ruby/models/jam_track_right.rb +++ b/ruby/lib/jam_ruby/models/jam_track_right.rb @@ -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 diff --git a/ruby/lib/jam_ruby/models/notification.rb b/ruby/lib/jam_ruby/models/notification.rb index 5b008a55f..d86d73d12 100644 --- a/ruby/lib/jam_ruby/models/notification.rb +++ b/ruby/lib/jam_ruby/models/notification.rb @@ -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) diff --git a/ruby/lib/jam_ruby/resque/jam_track_mixdown_packager.rb b/ruby/lib/jam_ruby/resque/jam_track_mixdown_packager.rb new file mode 100644 index 000000000..1d54bf67d --- /dev/null +++ b/ruby/lib/jam_ruby/resque/jam_track_mixdown_packager.rb @@ -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 diff --git a/ruby/lib/jam_ruby/resque/jam_tracks_builder.rb b/ruby/lib/jam_ruby/resque/jam_tracks_builder.rb index 359bdc514..381bb8b83 100644 --- a/ruby/lib/jam_ruby/resque/jam_tracks_builder.rb +++ b/ruby/lib/jam_ruby/resque/jam_tracks_builder.rb @@ -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) diff --git a/ruby/lib/jam_ruby/resque/scheduled/jam_tracks_cleaner.rb b/ruby/lib/jam_ruby/resque/scheduled/jam_tracks_cleaner.rb index 5039fc862..7449d6f0e 100644 --- a/ruby/lib/jam_ruby/resque/scheduled/jam_tracks_cleaner.rb +++ b/ruby/lib/jam_ruby/resque/scheduled/jam_tracks_cleaner.rb @@ -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}") diff --git a/ruby/lib/jam_ruby/resque/scheduled/stats_maker.rb b/ruby/lib/jam_ruby/resque/scheduled/stats_maker.rb index 6bff4192e..78a40aa6c 100644 --- a/ruby/lib/jam_ruby/resque/scheduled/stats_maker.rb +++ b/ruby/lib/jam_ruby/resque/scheduled/stats_maker.rb @@ -31,6 +31,7 @@ module JamRuby Stats.write('users', User.stats) Stats.write('sessions', ActiveMusicSession.stats) Stats.write('jam_track_rights', JamTrackRight.stats) + Stats.write('jam_track_mixdown_packages', JamTrackMixdownPackage.stats) end end diff --git a/ruby/spec/factories.rb b/ruby/spec/factories.rb index f181e00da..5de311a87 100644 --- a/ruby/spec/factories.rb +++ b/ruby/spec/factories.rb @@ -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}" } diff --git a/ruby/spec/jam_ruby/models/jam_track_mixdown_package_spec.rb b/ruby/spec/jam_ruby/models/jam_track_mixdown_package_spec.rb new file mode 100644 index 000000000..3a90ffcc3 --- /dev/null +++ b/ruby/spec/jam_ruby/models/jam_track_mixdown_package_spec.rb @@ -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 + diff --git a/ruby/spec/jam_ruby/models/jam_track_mixdown_spec.rb b/ruby/spec/jam_ruby/models/jam_track_mixdown_spec.rb new file mode 100644 index 000000000..8da940afd --- /dev/null +++ b/ruby/spec/jam_ruby/models/jam_track_mixdown_spec.rb @@ -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 + diff --git a/ruby/spec/jam_ruby/models/jam_track_right_spec.rb b/ruby/spec/jam_ruby/models/jam_track_right_spec.rb index 9119bdfd9..9e7bc225e 100644 --- a/ruby/spec/jam_ruby/models/jam_track_right_spec.rb +++ b/ruby/spec/jam_ruby/models/jam_track_right_spec.rb @@ -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 diff --git a/ruby/spec/support/utilities.rb b/ruby/spec/support/utilities.rb index 2717f63dc..d909b57d2 100644 --- a/ruby/spec/support/utilities.rb +++ b/ruby/spec/support/utilities.rb @@ -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 diff --git a/web/Gemfile b/web/Gemfile index 96604366d..d34a1d2a8 100644 --- a/web/Gemfile +++ b/web/Gemfile @@ -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' diff --git a/web/app/assets/javascripts/dialog/openJamTrackDialog.js b/web/app/assets/javascripts/dialog/openJamTrackDialog.js index 8f58b0728..ed400d8c1 100644 --- a/web/app/assets/javascripts/dialog/openJamTrackDialog.js +++ b/web/app/assets/javascripts/dialog/openJamTrackDialog.js @@ -103,9 +103,8 @@ sampleRate = context.jamClient.GetSampleRate() sampleRateForFilename = sampleRate == 48 ? '48' : '44'; doSearch(); - - } + function afterHide() { showing = false; } diff --git a/web/app/assets/javascripts/download_jamtrack.js.coffee b/web/app/assets/javascripts/download_jamtrack.js.coffee index 8890e6b87..c4727631d 100644 --- a/web/app/assets/javascripts/download_jamtrack.js.coffee +++ b/web/app/assets/javascripts/download_jamtrack.js.coffee @@ -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()) diff --git a/web/app/assets/javascripts/fakeJamClient.js b/web/app/assets/javascripts/fakeJamClient.js index 7ff48dd44..077ad4ced 100644 --- a/web/app/assets/javascripts/fakeJamClient.js +++ b/web/app/assets/javascripts/fakeJamClient.js @@ -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; diff --git a/web/app/assets/javascripts/jam_rest.js b/web/app/assets/javascripts/jam_rest.js index ce9a6f3d2..9f1e0a56b 100644 --- a/web/app/assets/javascripts/jam_rest.js +++ b/web/app/assets/javascripts/jam_rest.js @@ -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 diff --git a/web/app/assets/javascripts/react-components.js b/web/app/assets/javascripts/react-components.js index 118242dda..350dce7e9 100644 --- a/web/app/assets/javascripts/react-components.js +++ b/web/app/assets/javascripts/react-components.js @@ -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 diff --git a/web/app/assets/javascripts/react-components/MediaControls.js.jsx.coffee b/web/app/assets/javascripts/react-components/MediaControls.js.jsx.coffee index 34899a6cf..fed32bea6 100644 --- a/web/app/assets/javascripts/react-components/MediaControls.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/MediaControls.js.jsx.coffee @@ -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')) -
0:00
+
{this.state.time}
@@ -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 diff --git a/web/app/assets/javascripts/react-components/PopupMediaControls.js.jsx.coffee b/web/app/assets/javascripts/react-components/PopupMediaControls.js.jsx.coffee index 37673bf87..a15428170 100644 --- a/web/app/assets/javascripts/react-components/PopupMediaControls.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/PopupMediaControls.js.jsx.coffee @@ -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 = `

{mediaType}: {mediaName} ({this.state.time})

` - else if @props.mediaSummary.jamTrackOpen + header = `

{mediaType}: {mediaName}

` + else if @state.jamTrackState.jamTrack? + jamTrack = @state.jamTrackState.jamTrack mediaType = "JamTrack" - mediaName = @props.jamTracks[0].name - closeLinkText = 'close JamTrack' - header = `

{mediaType}: {mediaName} ({this.state.time})

` - 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 = `
{selectedMixdown.name}
` + when 'keying_timeout' + customMixName = `
{selectedMixdown.name}
` + when 'download_fail' + customMixName = `
{selectedMixdown.name}
` + when 'keying' + customMixName = `
Loading selected mix...
` + when 'downloading' + customMixName = `
Loading selected mix...
` + when 'ready' + customMixName = `
{selectedMixdown.name}
` + disabled = false + else + customMixName = `
Creating mixdown...
` + + else + if SessionStore.downloadingJamTrack + downloader = `` + + jamTrackTypeHeader = `Full JamTrack {downloader}` + + header = ` +
+

{mediaType}: {mediaName}

+

{jamTrackTypeHeader}

+ {customMixName} +
` + + myMixes = null + if @state.showMyMixes + myMixdowns = [] + + boundPlayClick = this.jamTrackPlay.bind(this, jamTrack); + + active = jamTrack.last_mixdown_id == null + + myMixdowns.push ` +
+
+ Full JamTrack +
+
+ +
+
` + + 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 = `` + when 'QUIET' + action = `` + when 'QUEUED' + action = `` + when 'QUEUED_TIMEOUT' + action = `` + when 'SIGNING' + action = `` + when 'SIGNING_TIMEOUT' + action = `` + when 'SIGNED' + action = `` + when 'ERROR' + action = `` + else + action = `` + + if editing + mixdownName = `` + editIcon = `` + else + mixdownName = mixdown.name + editIcon = `` + + myMixdowns.push ` +
+
+ {mixdownName} +
+
+ {action} + + {editIcon} + + +
+
` + + myMixes = `
{myMixdowns}
` + + 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 = ` +
+

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.

+
+ + +
+
+ + +
+
+ + +
+
+ CREATE MIX + {errorHtml} +
+
+ +
` + + if @state.showMyMixes + showMyMixesText = `hide my mixes
` + else + showMyMixesText = `show my mixes
` + + if @state.showCustomMixes + showMixControlsText = `hide mix controls
` + else + showMixControlsText = `show mix controls
` + + + extraControls = ` +
+

My Mixes {showMyMixesText}

+ + {myMixes} + +

Create Custom Mix {showMixControlsText}

+ + {mixControls} + +
` + + else if @state.media.mediaSummary.backingTrackOpen mediaType = "Audio File" - mediaName = context.JK.getNameOfFile(@props.backingTracks[0].shortFilename) - closeLinkText = 'close audio file' - header = `

{mediaType}: {mediaName} ({this.state.time})

` + mediaName = context.JK.getNameOfFile(@state.media.backingTracks[0].shortFilename) + closeLinkText = 'CLOSE AUDIO FILE' + header = `

{mediaType}: {mediaName}

` extraControls = `
@@ -69,9 +336,9 @@ mixins.push(Reflux.listenTo(MediaPlaybackStore, 'onMediaStateChanged'))

` - else if @props.mediaSummary.metronomeOpen + else if @state.media.mediaSummary.metronomeOpen mediaType = "Metronome" - closeLinkText = 'close metronome' + closeLinkText = 'CLOSE METRONOME' header = `

Metronome

` extraControls = `
@@ -84,12 +351,161 @@ mixins.push(Reflux.listenTo(MediaPlaybackStore, 'onMediaStateChanged')) {header} {extraControls} - {closeLinkText} + {closeLinkText}
` 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 + + }) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/SessionMediaTracks.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionMediaTracks.js.jsx.coffee index 6ca6d46f1..afb9a13f2 100644 --- a/web/app/assets/javascripts/react-components/SessionMediaTracks.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/SessionMediaTracks.js.jsx.coffee @@ -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(``) - - 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(``) @@ -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 diff --git a/web/app/assets/javascripts/react-components/SessionTrackVolumeHover.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionTrackVolumeHover.js.jsx.coffee index 63c02f9bb..e14db1c14 100644 --- a/web/app/assets/javascripts/react-components/SessionTrackVolumeHover.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/SessionTrackVolumeHover.js.jsx.coffee @@ -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) diff --git a/web/app/assets/javascripts/react-components/actions/JamTrackActions.js.coffee b/web/app/assets/javascripts/react-components/actions/JamTrackActions.js.coffee index 0c122c4c6..1b5e6a9ae 100644 --- a/web/app/assets/javascripts/react-components/actions/JamTrackActions.js.coffee +++ b/web/app/assets/javascripts/react-components/actions/JamTrackActions.js.coffee @@ -3,6 +3,7 @@ context = window @JamTrackActions = Reflux.createActions({ open: {} close: {} + activateNoMixdown: {} requestSearch: {} requestFilter: {} }) diff --git a/web/app/assets/javascripts/react-components/actions/JamTrackMixdownActions.js.coffee b/web/app/assets/javascripts/react-components/actions/JamTrackMixdownActions.js.coffee new file mode 100644 index 000000000..98169165b --- /dev/null +++ b/web/app/assets/javascripts/react-components/actions/JamTrackMixdownActions.js.coffee @@ -0,0 +1,13 @@ +context = window + +@JamTrackMixdownActions = Reflux.createActions({ + createMixdown: {} + editMixdown: {} + refreshMixdown: {} + deleteMixdown: {} + openMixdown: {} + closeMixdown: {} + enqueueMixdown: {} + downloadMixdown: {} +}) + diff --git a/web/app/assets/javascripts/react-components/actions/SessionActions.js.coffee b/web/app/assets/javascripts/react-components/actions/SessionActions.js.coffee index 634e7d419..d9d31fada 100644 --- a/web/app/assets/javascripts/react-components/actions/SessionActions.js.coffee +++ b/web/app/assets/javascripts/react-components/actions/SessionActions.js.coffee @@ -19,4 +19,5 @@ context = window broadcastFailure: {} broadcastSuccess: {} broadcastStopped: {} + mixdownActive: {} }) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/helpers/MixerHelper.js.coffee b/web/app/assets/javascripts/react-components/helpers/MixerHelper.js.coffee index 4639dc8e2..c1fe26514 100644 --- a/web/app/assets/javascripts/react-components/helpers/MixerHelper.js.coffee +++ b/web/app/assets/javascripts/react-components/helpers/MixerHelper.js.coffee @@ -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 diff --git a/web/app/assets/javascripts/react-components/helpers/SessionHelper.js.coffee b/web/app/assets/javascripts/react-components/helpers/SessionHelper.js.coffee index 1cb5023c7..e9d88c04a 100644 --- a/web/app/assets/javascripts/react-components/helpers/SessionHelper.js.coffee +++ b/web/app/assets/javascripts/react-components/helpers/SessionHelper.js.coffee @@ -75,6 +75,9 @@ context = window else null + jamTrackMixdown: () -> + { id: @session?.jam_track?.mixdown.id } + jamTrackName: () -> @session?.jam_track?.name diff --git a/web/app/assets/javascripts/react-components/mixins/SessionMediaTracksMixin.js.coffee b/web/app/assets/javascripts/react-components/mixins/SessionMediaTracksMixin.js.coffee index 92dc025f7..6297c7be0 100644 --- a/web/app/assets/javascripts/react-components/mixins/SessionMediaTracksMixin.js.coffee +++ b/web/app/assets/javascripts/react-components/mixins/SessionMediaTracksMixin.js.coffee @@ -31,6 +31,7 @@ logger = context.JK.logger mediaCategoryMixer: mediaCategoryMixer recordingName: mixers.recordingName() jamTrackName: mixers.jamTrackName() + jamTrackMixdown: session.jamTrackMixdown() @inputsChangedProcessed(state) if @inputsChangedProcessed? diff --git a/web/app/assets/javascripts/react-components/stores/JamTrackMixdownStore.js.coffee b/web/app/assets/javascripts/react-components/stores/JamTrackMixdownStore.js.coffee new file mode 100644 index 000000000..ae24d1cce --- /dev/null +++ b/web/app/assets/javascripts/react-components/stores/JamTrackMixdownStore.js.coffee @@ -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) + } +) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/stores/JamTrackStore.js.coffee b/web/app/assets/javascripts/react-components/stores/JamTrackStore.js.coffee index 53002cf8f..1daf1beec 100644 --- a/web/app/assets/javascripts/react-components/stores/JamTrackStore.js.coffee +++ b/web/app/assets/javascripts/react-components/stores/JamTrackStore.js.coffee @@ -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) } ) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/stores/SessionStore.js.coffee b/web/app/assets/javascripts/react-components/stores/SessionStore.js.coffee index f56828030..b0bbb0ac7 100644 --- a/web/app/assets/javascripts/react-components/stores/SessionStore.js.coffee +++ b/web/app/assets/javascripts/react-components/stores/SessionStore.js.coffee @@ -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) => diff --git a/web/app/assets/javascripts/utils.js b/web/app/assets/javascripts/utils.js index fed60372a..5dfcbde51 100644 --- a/web/app/assets/javascripts/utils.js +++ b/web/app/assets/javascripts/utils.js @@ -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. diff --git a/web/app/assets/stylesheets/minimal/media_controls.css.scss b/web/app/assets/stylesheets/minimal/media_controls.css.scss index 2abdda8fc..b8079dd95 100644 --- a/web/app/assets/stylesheets/minimal/media_controls.css.scss +++ b/web/app/assets/stylesheets/minimal/media_controls.css.scss @@ -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; + } } \ No newline at end of file diff --git a/web/app/controllers/api_jam_track_mixdowns_controller.rb b/web/app/controllers/api_jam_track_mixdowns_controller.rb new file mode 100644 index 000000000..a38c2f734 --- /dev/null +++ b/web/app/controllers/api_jam_track_mixdowns_controller.rb @@ -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 diff --git a/web/app/controllers/api_jam_tracks_controller.rb b/web/app/controllers/api_jam_tracks_controller.rb index 428018462..14e3a024d 100644 --- a/web/app/controllers/api_jam_tracks_controller.rb +++ b/web/app/controllers/api_jam_tracks_controller.rb @@ -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 diff --git a/web/app/controllers/api_search_controller.rb b/web/app/controllers/api_search_controller.rb index 54e3ab5b0..d9a362d36 100644 --- a/web/app/controllers/api_search_controller.rb +++ b/web/app/controllers/api_search_controller.rb @@ -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) diff --git a/web/app/controllers/api_users_controller.rb b/web/app/controllers/api_users_controller.rb index 18c2db8f8..a9640ba45 100644 --- a/web/app/controllers/api_users_controller.rb +++ b/web/app/controllers/api_users_controller.rb @@ -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]×tamp=[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]×tamp=[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, diff --git a/web/app/views/api_jam_track_mixdowns/create.rabl b/web/app/views/api_jam_track_mixdowns/create.rabl new file mode 100644 index 000000000..dde043534 --- /dev/null +++ b/web/app/views/api_jam_track_mixdowns/create.rabl @@ -0,0 +1,3 @@ +object @mixdown + +extends "api_jam_track_mixdowns/show" \ No newline at end of file diff --git a/web/app/views/api_jam_track_mixdowns/enqueue.rabl b/web/app/views/api_jam_track_mixdowns/enqueue.rabl new file mode 100644 index 000000000..5fde4660c --- /dev/null +++ b/web/app/views/api_jam_track_mixdowns/enqueue.rabl @@ -0,0 +1,7 @@ +object @package + +node :queue_time do + @queue_time +end + +extends "api_jam_track_mixdowns/show_package" \ No newline at end of file diff --git a/web/app/views/api_jam_track_mixdowns/index.rabl b/web/app/views/api_jam_track_mixdowns/index.rabl new file mode 100644 index 000000000..3cfb8508b --- /dev/null +++ b/web/app/views/api_jam_track_mixdowns/index.rabl @@ -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 \ No newline at end of file diff --git a/web/app/views/api_jam_track_mixdowns/show.rabl b/web/app/views/api_jam_track_mixdowns/show.rabl new file mode 100644 index 000000000..a599f8a64 --- /dev/null +++ b/web/app/views/api_jam_track_mixdowns/show.rabl @@ -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 +} diff --git a/web/app/views/api_jam_track_mixdowns/show_package.rabl b/web/app/views/api_jam_track_mixdowns/show_package.rabl new file mode 100644 index 000000000..b4899b037 --- /dev/null +++ b/web/app/views/api_jam_track_mixdowns/show_package.rabl @@ -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 diff --git a/web/app/views/api_jam_track_mixdowns/update.rabl b/web/app/views/api_jam_track_mixdowns/update.rabl new file mode 100644 index 000000000..dde043534 --- /dev/null +++ b/web/app/views/api_jam_track_mixdowns/update.rabl @@ -0,0 +1,3 @@ +object @mixdown + +extends "api_jam_track_mixdowns/show" \ No newline at end of file diff --git a/web/app/views/api_jam_tracks/keys.rabl b/web/app/views/api_jam_tracks/keys.rabl index a5cd9d471..2b482e44a 100644 --- a/web/app/views/api_jam_tracks/keys.rabl +++ b/web/app/views/api_jam_tracks/keys.rabl @@ -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 \ No newline at end of file diff --git a/web/app/views/api_jam_tracks/show_for_client.rabl b/web/app/views/api_jam_tracks/show_for_client.rabl index f9531ce94..27f4fb616 100644 --- a/web/app/views/api_jam_tracks/show_for_client.rabl +++ b/web/app/views/api_jam_tracks/show_for_client.rabl @@ -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 } \ No newline at end of file diff --git a/web/app/views/api_music_sessions/show.rabl b/web/app/views/api_music_sessions/show.rabl index 3472f18a3..d5afc2a02 100644 --- a/web/app/views/api_music_sessions/show.rabl +++ b/web/app/views/api_music_sessions/show.rabl @@ -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 } diff --git a/web/app/views/clients/_help.html.slim b/web/app/views/clients/_help.html.slim index 721a5f191..6823d0b45 100644 --- a/web/app/views/clients/_help.html.slim +++ b/web/app/views/clients/_help.html.slim @@ -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. + diff --git a/web/config/application.rb b/web/config/application.rb index 2baaf5aee..f87e02c44 100644 --- a/web/config/application.rb +++ b/web/config/application.rb @@ -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 diff --git a/web/config/environments/development.rb b/web/config/environments/development.rb index ed6af30ce..032b3a33e 100644 --- a/web/config/environments/development.rb +++ b/web/config/environments/development.rb @@ -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 diff --git a/web/config/routes.rb b/web/config/routes.rb index 233521e2c..e14387176 100644 --- a/web/config/routes.rb +++ b/web/config/routes.rb @@ -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 diff --git a/web/config/scheduler.yml b/web/config/scheduler.yml index ea694ba60..339dfd4f6 100644 --- a/web/config/scheduler.yml +++ b/web/config/scheduler.yml @@ -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 * * *" diff --git a/web/lib/tasks/jam_tracks.rake b/web/lib/tasks/jam_tracks.rake index 0d758bada..f74a6f775 100644 --- a/web/lib/tasks/jam_tracks.rake +++ b/web/lib/tasks/jam_tracks.rake @@ -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 diff --git a/web/spec/controllers/api_jam_track_mixdowns_controller_spec.rb b/web/spec/controllers/api_jam_track_mixdowns_controller_spec.rb new file mode 100644 index 000000000..7f36ea02e --- /dev/null +++ b/web/spec/controllers/api_jam_track_mixdowns_controller_spec.rb @@ -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 + diff --git a/web/spec/controllers/api_jam_tracks_controller_spec.rb b/web/spec/controllers/api_jam_tracks_controller_spec.rb index 25f971ec9..46a7d9c2d 100644 --- a/web/spec/controllers/api_jam_tracks_controller_spec.rb +++ b/web/spec/controllers/api_jam_tracks_controller_spec.rb @@ -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 diff --git a/web/spec/factories.rb b/web/spec/factories.rb index 4421e2c62..10b2d74f2 100644 --- a/web/spec/factories.rb +++ b/web/spec/factories.rb @@ -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}" } diff --git a/web/spec/features/individual_jamtrack_spec.rb b/web/spec/features/individual_jamtrack_spec.rb index 103e0eae8..536c53b49 100644 --- a/web/spec/features/individual_jamtrack_spec.rb +++ b/web/spec/features/individual_jamtrack_spec.rb @@ -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') diff --git a/web/spec/requests/active_music_sessions_api_spec.rb b/web/spec/requests/active_music_sessions_api_spec.rb index 61836348e..af4f9a5b6 100755 --- a/web/spec/requests/active_music_sessions_api_spec.rb +++ b/web/spec/requests/active_music_sessions_api_spec.rb @@ -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 diff --git a/web/spec/support/app_config.rb b/web/spec/support/app_config.rb index 4b6e344d3..76ff7c8f2 100644 --- a/web/spec/support/app_config.rb +++ b/web/spec/support/app_config.rb @@ -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 From d3ca53672b55462eec0b6b644c38139a1a101a21 Mon Sep 17 00:00:00 2001 From: Seth Call Date: Mon, 21 Sep 2015 20:30:51 -0500 Subject: [PATCH 03/25] * remove react_rails_img --- web/app/assets/javascripts/react-components.js | 1 - 1 file changed, 1 deletion(-) diff --git a/web/app/assets/javascripts/react-components.js b/web/app/assets/javascripts/react-components.js index 350dce7e9..118242dda 100644 --- a/web/app/assets/javascripts/react-components.js +++ b/web/app/assets/javascripts/react-components.js @@ -1,6 +1,5 @@ //= 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 From de9856b5be0a9d7f7dd91f4a22b2603555b69050 Mon Sep 17 00:00:00 2001 From: Seth Call Date: Mon, 21 Sep 2015 21:11:01 -0500 Subject: [PATCH 04/25] * pin back react-select --- db/up/crash_dumps_2.sql | 0 web/Gemfile | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 db/up/crash_dumps_2.sql diff --git a/db/up/crash_dumps_2.sql b/db/up/crash_dumps_2.sql new file mode 100644 index 000000000..e69de29bb diff --git a/web/Gemfile b/web/Gemfile index d34a1d2a8..49217de8a 100644 --- a/web/Gemfile +++ b/web/Gemfile @@ -99,7 +99,7 @@ gem 'react-rails-img' source 'https://rails-assets.org' do gem 'rails-assets-reflux' gem 'rails-assets-classnames' - gem 'rails-assets-react-select' + gem 'rails-assets-react-select', '0.6.7' end #group :development, :production do From a0feb09509debe5d9d1b7ed2973bcb25e5838ffb Mon Sep 17 00:00:00 2001 From: Seth Call Date: Tue, 22 Sep 2015 05:48:00 -0500 Subject: [PATCH 05/25] * check in missing images --- db/up/crash_dumps_2.sql | 5 +++++ ruby/lib/jam_ruby/models/crash_dump.rb | 2 +- web/app/assets/images/content/icon-delete.png | Bin 0 -> 606 bytes web/app/assets/images/content/icon-delete@2X.png | Bin 0 -> 1357 bytes web/app/assets/images/content/icon-edit.png | Bin 0 -> 642 bytes web/app/assets/images/content/icon-edit@2X.png | Bin 0 -> 1395 bytes .../assets/images/content/icon-mix-fail@2X.png | Bin 0 -> 806 bytes web/app/assets/images/content/icon-play.png | Bin 0 -> 1383 bytes web/app/assets/images/content/icon-retry@2X.png | Bin 0 -> 837 bytes web/app/assets/images/content/icon-save@2X.png | Bin 0 -> 1405 bytes web/app/assets/images/content/icon_open@2X.png | Bin 0 -> 1237 bytes web/app/controllers/api_users_controller.rb | 2 +- 12 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 web/app/assets/images/content/icon-delete.png create mode 100644 web/app/assets/images/content/icon-delete@2X.png create mode 100644 web/app/assets/images/content/icon-edit.png create mode 100644 web/app/assets/images/content/icon-edit@2X.png create mode 100644 web/app/assets/images/content/icon-mix-fail@2X.png create mode 100644 web/app/assets/images/content/icon-play.png create mode 100644 web/app/assets/images/content/icon-retry@2X.png create mode 100644 web/app/assets/images/content/icon-save@2X.png create mode 100644 web/app/assets/images/content/icon_open@2X.png diff --git a/db/up/crash_dumps_2.sql b/db/up/crash_dumps_2.sql index e69de29bb..20603091c 100644 --- a/db/up/crash_dumps_2.sql +++ b/db/up/crash_dumps_2.sql @@ -0,0 +1,5 @@ +ALTER TABLE crash_dumps ADD COLUMN email VARCHAR(255); +ALTER TABLE crash_dumps ADD COLUMN description VARCHAR(10000); +ALTER TABLE crash_dumps ADD COLUMN os VARCHAR(100); +ALTER TABLE crash_dumps ADD COLUMN os_version VARCHAR(100); + diff --git a/ruby/lib/jam_ruby/models/crash_dump.rb b/ruby/lib/jam_ruby/models/crash_dump.rb index bab31fd97..13087fed0 100644 --- a/ruby/lib/jam_ruby/models/crash_dump.rb +++ b/ruby/lib/jam_ruby/models/crash_dump.rb @@ -15,7 +15,7 @@ module JamRuby before_validation(:on => :create) do self.created_at ||= Time.now self.id = SecureRandom.uuid - self.uri = "dump/#{created_at.strftime('%Y-%m-%d')}/#{self.id}" + self.uri = "dumps/#{created_at.strftime('%Y-%m-%d')}/#{self.id}" end def user_email diff --git a/web/app/assets/images/content/icon-delete.png b/web/app/assets/images/content/icon-delete.png new file mode 100644 index 0000000000000000000000000000000000000000..cae694fba3ddea804209a0f391e3d3b8f84ffb80 GIT binary patch literal 606 zcmV-k0-^nhP)DX?S5j_HRFAHLex*BAKlduDu&muUr*b=e}5el6D~d2L2+@h3DAJe3_#~xb9Q!?XCM(UGcz;X zyLWFX$XXVlv%kV5BqStQHgDeC)!yD71QYZ2_Lkneckc`!tppSM`}Z%yhYugVfLw6X z+}vCYCUNA*k=p+d@S~}z$<@uxP5Arw@8_Um=TuZwSYc`}T(~fc1>I@D06hVWl`lXh zPi<}OS|I%bh*d#yhYlUu`R2`=zo@~A<^m8XEiJYG`Sa)V{|NAV>(;IH+}zwKdORWH{QC7PmWV}j0nqsp@7}#T`X3W~tg5PV0fj!Y3os10a^*@F zOb0MPJ0?$_>0L6Ha27z%mf=?VqzkW>@8_&X)g2tQ&3RgL-+p0ix=mC4A|M--5p43O0=@F z;sxs6!BA0AVGd;MFDNK5=i=fbN;5E^Mc=-CyK>W}O?iwUfy&BC%eJ<*a$ws23QTn$ s@#MyTK+FjYNGV{t*q)u8JpmxV06@x96Pt8OkN^Mx07*qoM6N<$f^2CVfB*mh literal 0 HcmV?d00001 diff --git a/web/app/assets/images/content/icon-delete@2X.png b/web/app/assets/images/content/icon-delete@2X.png new file mode 100644 index 0000000000000000000000000000000000000000..b120befa5b8bffc5bceb0622df8576aecd4dc447 GIT binary patch literal 1357 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz$r9IylHmNblJdl&R0hYC{G?O` z&)mfH)S%SFl*+=BsWuD@%o>>?5hW46K32*3xq68pHF_1f1wh>l3^w)^1&PVosU-?Y zsp*+{wo31J?^jaDOtDo8H}y5}EpSfF$n>ZxN)4{^3rViZPPR-@vbR&PsjvbXkegbP zs8ErclUHn2VXFi-*9yo63F|8hm3bwJ6}oxF$}kgLQj3#|G7CyF^YauyCMG83 zmzLNn0bL65LT&-v*t}wBFaZNhzap_f-%!s0*lEl2^R8JRMC7?NanVBh8&Zfpj1{S79#;%TLhK8=DPHx6VmTs2D zCT?cV#%7k5FugAM$)&lec_lEtDG0sBIQ4=OL~a4lW|!2W%(B!Jx1#)91+d4hGO@VD z!qLFR)WyXTr+HAlDOlVB)$52;uRhQ*`k?4Vif)(?Fb#p2@Wcz`z>|M!9x%-p0TcIQ zIg_Of42-)yT^vIyZXF4?^*ZFXAlF}q$p%4BC-kq7Bp!kd3qsPAas4Sb9 zSh(<`6!q}bO9vffUhOR4)_wVVMeW<&Cl{XmZS_3vU2Uy@1-qE*!~>6ob}*f5P+mXd zFH`&h>ko=89h2rNIr{X=ePEeW@L7F>o`wR0-jBcRJ6P2OCViS^(6!BsT}p;i+b#R! ztm0=98xQYhtbHf3%t}Yv+A3vT-JOW%ZG|(Ruqz!q6X{=7|1LD;h^TwxzuDh+sDuI8m#WB39gD>Cs{bTAvqy)!7koAjEzY?2a<2GSxu9EXYt$g$=!_n zd>8VNGv~B+4GGCV`O6OG<=opJFI9Zn2 z86}wD@lRh+O*(La=GFj3r453OjjOJhs9J4MOjvcQ%(v7hH^)W4CSvNeSuQU9&!cX3 zoS4j4z;Cko)tuY2Y?d7^?lG$2wF!&;f5h|7x&D-M{7c{UKRu_fwr+jeIsN5_zJC4V b-@wQa#&zlz-*&kMPyy%Z>gTe~DWM4f>aynW literal 0 HcmV?d00001 diff --git a/web/app/assets/images/content/icon-edit.png b/web/app/assets/images/content/icon-edit.png new file mode 100644 index 0000000000000000000000000000000000000000..26ed5892f491be2401f0df5daa0ea05e38c8e7ff GIT binary patch literal 642 zcmV-|0)737P)DX?S5j_HRFAHLex*BAKlM+A|NZ-S6~pV-ucz(bzrT)&2}2JG5D^h!0NDrw#l^)YKm#^2 z0G)Hq+1XhhpXR{8K+$jCzOA}+=~APFgalX&=o*H5_wFqP8F0(Q#6$#_W^;3M*3+j? z$Nz_dix)4}16>1l?zwa4W`YDeo;-Q70VJlUrn7J$v0AOldsAT%`8;NQQ0oM~xkUT@yKc@Gln>FJ5Cs;b(6 zE37~%gArs6hyzT{Kg7kwV{&tI?*bFejmeWIca)ZvuECQ;{)3Yi&{nWZZr{HB;LxE% zrN9tppF4N%oRX4~t@sicBO@a#C=7OXcXtOekO-`-tayQXcQ8~`RG0%9`wI#R%(=L@ zh|&y%%WX;U5}NT9N^(h?Zb<-oN4_51hlAF<^|DEJ4&oSK@NQd_rf-JYGD cJpmxV0Ji)+BC2CZ?f?J)07*qoM6N<$g0cZL5&!@I literal 0 HcmV?d00001 diff --git a/web/app/assets/images/content/icon-edit@2X.png b/web/app/assets/images/content/icon-edit@2X.png new file mode 100644 index 0000000000000000000000000000000000000000..7cd9052d5a424a085700871a3b125075001bf9de GIT binary patch literal 1395 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz$r9IylHmNblJdl&R0hYC{G?O` z&)mfH)S%SFl*+=BsWuD@%o>>?5hW46K32*3xq68pHF_1f1wh>l3^w)^1&PVosU-?Y zsp*+{wo31J?^jaDOtDo8H}y5}EpSfF$n>ZxN)4{^3rViZPPR-@vbR&PsjvbXkegbP zs8ErclUHn2VXFi-*9yo63F|8hm3bwJ6}oxF$}kgLQj3#|G7CyF^YauyCMG83 zmzLNn0bL65LT&-v*t}wBFaZNhzap_f-%!s0*lEl2^R8JRMC7?NanVBh87LEokrYzrSmD_FJeXFu|?7Ic1k{cX}Kh9kW9zD3&? zmO4C2uj9U)*tH#Kir+PT&3Di9cbs^1MU$5~IOvW)Q}BaHO}Eb#2YcOl?{2|aXcggh zP|(A>^S5t<)Tvql_E-f))qj^4@6d_dY1=3z@j1et!S(tb-XrT-ws>w{_9}N`B2SZ~ z`+B*8WUkc>)9WuzX}dkGz0Q#Hcb3?%ry;Giw*LhZ7&Z%hUd=O;VKb;0^mO%eS?83{ F1OT~p^veJM literal 0 HcmV?d00001 diff --git a/web/app/assets/images/content/icon-mix-fail@2X.png b/web/app/assets/images/content/icon-mix-fail@2X.png new file mode 100644 index 0000000000000000000000000000000000000000..265022204724ec7fc103f833623f5a4b9a9c0184 GIT binary patch literal 806 zcmV+>1KIqEP)O^Dr6{v^d`AN zi%VTf$dsTJ9Evy?j3sH8A~+NwQxz%%DK8R{$sg&cYw+0Db`Y4B)E}f{mTR5&R~A7XZ%NO%(t&p9IbU2;0xU03?JE zuhC@X7XM<4eRe}uVxHc)xw*&reE$8) z%E~hU*Ck10?U;Rys~_+>!P?r|OE%!(;GhiPvIK#Fe=3mNA-U)EV?|M>*#wqlWh7PD zBe?^THIl0yzYFZ{?tWwo02r4buu8H9ndBQC9lgso*xcMK$|j=>f#ClB{ujDHQIu&J z0%yE3j>qHoHBB2neAjh-K?o7kb$y|6TrQVe1`v=*1}_SPs?}=MvEP$+-=tH8iHV7O z9aUg{e*RG~7z}z{R5qJ^2q4%g1+LcX^;(mRZ>a!D&dki19Z|rtEVHMlr&qS*uCK2@ z05I%Vfw4CKd|L%bGMP+1_4ONWY;3&sLxAM(R_BE47AT4`ok%2fpNC%{l}at2WD5d? zLg7QZf@7Ly+AxeI(=^j=#RKW3kv+rBeAG4u|{QWXR|9Ph=G6 z^-^+e&js*}OX<#Gi;q?sWRBzFK@ z5JDUoFgQ4PYieq0T+_5mehNXcSlrv*-hK|?o70j3_<15z+5u~R#FU=Imir~}-OVq3h*MLHVgaU(L zc(BH7yS5{WOD<~-7dV71niu6TZUS1}Zm6dnq=H`T8WK*V@j@Q@M$Am)R{m95j zmd=LF&CO9!Q4#m(=twNno&N3Y?7Zjc>1lk~Thj&y2a9ETqV4VN$FZ@o9IcE82M5zZ z;AagwpC23?%);u2)dy<~+tDC1pP!%q=7q-H+}vopySuIGG{9MZb8~Y_e0;nso6V-f zLN6CBBqW4cT3Q-0J3CtotOsgTEN5nBx=|W8H8n+$;gn%NJw1I|R#q0{;^Lw)f(!-& zfsr>B7Z-n_(zUa*^TglZ-%LfYxVSh3&ZVTl!oq?uE-vmh%tNEmkcEYXR!`~d?2Nj) zx&%~COiWBXz>X@0-b5miJW+|)^0E*VU6Yq1g!kLk)zx1M3kyH6v9ZzUgn@wpYHV!$ zjKT*=c6Rn#=ZsZVRiCI7^YQVsn3$MzE2*ieE(*ue>FMd3q@*NAO-`^_EHpDS^N`fP zw6xT87EQQ4g5>HT87e9&q7@Y-CnvirAm4eNx3#tLeSLkeUE~BL=ye=WLiP|}3M;-T zl}dMsL>a(uNI2kz%a7_!_xJZlN=iy19336iK1FwT_dQaswzf74jgF40BuYz5OERhC zA_wHOwzk#?d^b!?OjMkJ(q-dHR^-R|gy6GHaTB8P9GG2naX=nGkx2AaU|^s*+039v zhzQ;(?7+0*4xri$0Lv>^a6nFf0(RVSa&j_|>*(m{C?k0OGsGoJ*!l+!j`Yw-Ju)&f ze(dS#`38gBOIjd!u44y`h)pR@8hw$Sot>wV(Yc9J;@A*7u%fsFw>Jk+qUpo<)YQ}u zFvMg)nVjzGeTJZPz9$tx zSNlju2UY=??)v!ns0<)b6=rb$7m@J&C@(LM3#F%oaP#x?bxO@bvjdXJ$;pbu#6-5b z=YX;9V^XNSy}c0m`T5b|Z6=7bgY@)t4{aQP2zCnuf_JIFM@}3L#{h2oJ1Lu+oBJNZ zg1x=HUs5!SM~eNQ$K$yZEOV``t%(i}4i_dOu7IYdCLXE(@bFL!v*L?n4+{%p!rnT` z)YsQ1ke6h5c(^5OjpE68e0;npA|k?Cn@WJT`!)%|m}+Wj;*}VoS$t0Qxb^k*e0zKQ zGoGNJAVySF6wBDySgTop&*#4bRkchpq>5Z!U6o7S-QC@2Yip~I$_eP_)m$#uR%;0K z@IVa>4Lmi2>K-7ZH>Ds9AP#s6Q?nPN$>L&`^u=^70sH&mwho zT31)66nYiRh0*zTPEO7vjG}5TxVO2vIVKj1$Km@thHhBaFhAUaq`d{-?bStQU|=9K zIy$;+V`Jmu5N2g%-2gNjk?D`NVfaX}w6xS7#{rHRBO@aZD8{f$5#-fl7}Ia-)J)9G z%y1d`0W$uCljkyqArA(ttE)eSgfzTzSF^IRLTPDfuLE@61%SeT+y21A1pdz=??14f p!n28nhljrgdLKYWE5rW_FaY)Dm;4CNeYK_O8g9&u8hhUEk;^24=N;5e$t5ddw+?vfbF5z%+aT5ws zGPo2WOO`4pF49Q{J(3KbnR*EYQ@FoFPY59?nyB|p@7{a&|KEH3|M&i%;Sq?40f+$j z0N_0U4zI%>0KWhj08GXhJzQjuoPP%J4Zu5RY64*TN&;^Ih|c&ofG>=(?|9w2qO9k!14e{0yxL|aSkBq0pJ60;{t&j06x!aEN~ph?d|RD5fPQk5x zyjk>zU@#bzBuR?L<8eunBt=ma05~`}IIwNoJ~}!&S~M8u&VNagBu&$_;cz&lXHYB_ zi;L*N+yo+#NTgn`*NKR1+qMUT!Qkxd?5y2xw`a~Pl}cq%L7IyI$8nshs_Jw)o!;Kw z-j>Vdawe0>h@vRYVxU^BR$rosa}kKeVzFMY*CQgT)oL}K=lR*1APBFKH0>-DaT z!0ztuu5H`4X_{s}pU*$Hk1@tVp-@N^MbYQ;`Q{-oR~ia}ATY+5X_}^C7{>qNo6Rngh6}v~&1SP{7>1EbrBdm1I-N`=lK{}|cDt=stEKC@ z-tYJOD|-u8Q5$*y+yf9?s0Rc9_pX;@t{0{5m*uWECT_Q8t~aO5?e_dHruLP|(DK1# P00000NkvXXu0mjfE9QeK literal 0 HcmV?d00001 diff --git a/web/app/assets/images/content/icon-save@2X.png b/web/app/assets/images/content/icon-save@2X.png new file mode 100644 index 0000000000000000000000000000000000000000..ac80ef9c409602a84301ee40f0b7f3aa2766ed0e GIT binary patch literal 1405 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz$r9IylHmNblJdl&R0hYC{G?O` z&)mfH)S%SFl*+=BsWuD@%o>>?5hW46K32*3xq68pHF_1f1wh>l3^w)^1&PVosU-?Y zsp*+{wo31J?^jaDOtDo8H}y5}EpSfF$n>ZxN)4{^3rViZPPR-@vbR&PsjvbXkegbP zs8ErclUHn2VXFi-*9yo63F|8hm3bwJ6}oxF$}kgLQj3#|G7CyF^YauyCMG83 zmzLNn0bL65LT&-v*t}wBFaZNhzap_f-%!s0*lEl2^R8JRMC7?NanVBh87LEokrYH(P&lWl zKOvMOaYpN*lo_*PKS=l#mGi#&Rg%CI^XNI>4*q$ECN{3??rTqF+`$(8*gN~+yxyAb z)FaOCPB)4C-Ft|0)65HZpNPEt<8*n0V5rBjPwelsn_oX@=TCj>UeI-uAWhO{=>gd?1ucu?&bbEr}Ms4t6+AL@|g>1PmVWg`(+k& z_}$z!ZH2&RgXLH5u*PaH6is{}`9SNO*0KXV>M3jGW-uR6t9V&>xs(0n0jBLGyE3&) z$`1&eAK!U{t?=;IIW2|<@8OM3~ump!T;R4z3q*zy9ixauiKj%4YFn6VLGJKUgbsze=V1OMukhjW(aUJ+FjsKB2!{>DB9>{S3?unGX(H UD{MKs2~;3@y85}Sb4q9e0O`jJI{*Lx literal 0 HcmV?d00001 diff --git a/web/app/assets/images/content/icon_open@2X.png b/web/app/assets/images/content/icon_open@2X.png new file mode 100644 index 0000000000000000000000000000000000000000..1d188a2ea327c52e2d642dabcc1fd244209afa8e GIT binary patch literal 1237 zcmV;`1S z-NDYz&c7ed%*8g+t#G@RB0_ z0kG56)g{ONczr1{GSUUXNOE#Ao0*x>K<^sk_hX^Fy!@o6$s&=6Vk8=Y*>G@haEpNF zEA8#=r-CtJczF0C^je%Wv_wQinB6C#k3~mEv*zaJZi~h8oy}&`61AjK>E)J|mg|Ry zhl>9G{tw=ifNl)$cpM824c&9ln_q$7D=RBq0-&gX*POsmI613sX{3JQ^@EGNQoyR%F}_ zMgUtFwoDC5^<%1x@)&UT3&^l_`7m?{+|Tf}6%rr3l|#|9ii(ORc*MXImvICU2Ltcn z@t&6@W@fYbH&8qc&DQ}N01qrytCb^1OBOIx^xVZR)R;#^&;JXE5q5=qZX?}kGQ^1CE<}D&$RZ~;* zx8DX}{~iB`cqLZg&Q~%noPJ>Vw_ueLk1hs?eZ2{qaXC3T#E9<&NPsr1px(Vx>|^4yFiTHQHz5cw^5=O_5*8LVWH1;wzYV}4#mdUcY(jVs zc@OVriB0Zw_C-VDOAe4CfC2N!8w;Sd|9OBf$%-kb1gjnVe~#+PM8o&4dC2!Ix?Su>IA^f$3q~n^Y7|AJ3Dn= zDlM8Pq9FVTM1k*Ajed5H>h=1E=v&}(fIUFRN#kb(-XGoF-BvGo=Cq^aN4~#Fg#n6( zOYE2=D=Ukwudmb1Yl*I6i8vzPp)p!mSa>W3^!4?ffj;Bf+S&t0$MW*>4XVpf;ao!n zbEL{~vPz)p1?fYLAafi#)z#HuXltnxwcKsABXCUdLWH-19MO^)X5`dCvB_$;laHvw*s_6k#OaC*EsDjHPTU}lK z5!Q`SB@EaoKm~w_zX?4;aeFR4KK?(w{|GPu=E3yUA#Qed00000NkvXXu0mjf@X1ts literal 0 HcmV?d00001 diff --git a/web/app/controllers/api_users_controller.rb b/web/app/controllers/api_users_controller.rb index a9640ba45..17d96fd5d 100644 --- a/web/app/controllers/api_users_controller.rb +++ b/web/app/controllers/api_users_controller.rb @@ -612,7 +612,7 @@ ApiUsersController < ApiController logger.debug("crash_dump can read from url #{read_url}") - redirect_to write_url + redirect_to write_url, status: 307 else # we should store it here to aid in development, but we don't have to until someone wants the feature # so... just return 200 From 032d30c3f304a1fd470fa07bf372c69df78bbf08 Mon Sep 17 00:00:00 2001 From: Seth Call Date: Tue, 22 Sep 2015 06:03:55 -0500 Subject: [PATCH 06/25] * fix query in db cleaner --- ruby/lib/jam_ruby/resque/scheduled/jam_tracks_cleaner.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ruby/lib/jam_ruby/resque/scheduled/jam_tracks_cleaner.rb b/ruby/lib/jam_ruby/resque/scheduled/jam_tracks_cleaner.rb index 7449d6f0e..77061a799 100644 --- a/ruby/lib/jam_ruby/resque/scheduled/jam_tracks_cleaner.rb +++ b/ruby/lib/jam_ruby/resque/scheduled/jam_tracks_cleaner.rb @@ -26,8 +26,8 @@ module JamRuby # 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) + 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| From 1733c8689d1ac8e49b5604826b44e4726969cf48 Mon Sep 17 00:00:00 2001 From: Seth Call Date: Tue, 22 Sep 2015 09:42:53 -0500 Subject: [PATCH 07/25] * fix typo in JamTrackCleaner --- ruby/lib/jam_ruby/resque/scheduled/jam_tracks_cleaner.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ruby/lib/jam_ruby/resque/scheduled/jam_tracks_cleaner.rb b/ruby/lib/jam_ruby/resque/scheduled/jam_tracks_cleaner.rb index 77061a799..bf0942cd6 100644 --- a/ruby/lib/jam_ruby/resque/scheduled/jam_tracks_cleaner.rb +++ b/ruby/lib/jam_ruby/resque/scheduled/jam_tracks_cleaner.rb @@ -27,7 +27,7 @@ module JamRuby # 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) + JamTrackMixdownPackage.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| From 1dce9842470fcfdd3b6ad1a2798c710fd4dd0de5 Mon Sep 17 00:00:00 2001 From: Seth Call Date: Tue, 22 Sep 2015 15:25:48 -0500 Subject: [PATCH 08/25] * VRFS-3519 - 2-player sessions working with custom mix --- .../MediaControls.js.jsx.coffee | 2 +- .../PopupMediaControls.js.jsx.coffee | 451 +++++++++--------- .../helpers/MixerHelper.js.coffee | 9 +- websocket-gateway/bin/websocket_gateway | 2 + .../lib/jam_websockets/router.rb | 33 +- .../lib/jam_websockets/server.rb | 40 +- 6 files changed, 310 insertions(+), 227 deletions(-) diff --git a/web/app/assets/javascripts/react-components/MediaControls.js.jsx.coffee b/web/app/assets/javascripts/react-components/MediaControls.js.jsx.coffee index fed32bea6..e4a1d9d42 100644 --- a/web/app/assets/javascripts/react-components/MediaControls.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/MediaControls.js.jsx.coffee @@ -72,7 +72,7 @@ mixins.push(Reflux.listenTo(MediaPlaybackStore, 'onMediaStateChanged')) monitorControls: (controls, mediaSummary) -> if mediaSummary.mediaOpen || mediaSummary.jamTrack? - if mediaSummary.jamTrack? + if mediaSummary.jamTrackOpen? controls.startMonitor(PLAYBACK_MONITOR_MODE.JAMTRACK) else if mediaSummary.backingTrackOpen controls.startMonitor(PLAYBACK_MONITOR_MODE.MEDIA_FILE) diff --git a/web/app/assets/javascripts/react-components/PopupMediaControls.js.jsx.coffee b/web/app/assets/javascripts/react-components/PopupMediaControls.js.jsx.coffee index a15428170..a475ffecd 100644 --- a/web/app/assets/javascripts/react-components/PopupMediaControls.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/PopupMediaControls.js.jsx.coffee @@ -49,6 +49,7 @@ mixins.push(Reflux.listenTo(JamTrackStore, 'onJamTrackChanged')) metronome: mixers.metronome recordingName: mixers.recordingName() jamTrackName: mixers.jamTrackName() + jamTrackMixdown: session.jamTrackMixdown() @setState(media: state, downloadingJamTrack: session.downloadingJamTrack) @@ -76,7 +77,8 @@ mixins.push(Reflux.listenTo(JamTrackStore, 'onJamTrackChanged')) creatingMixdown: false, createMixdownErrors: null, editingMixdownId: null, - downloadingJamTrack: @props.downloadingJamTrack + downloadingJamTrack: @props.downloadingJamTrack, + jamTrackMixdown: {} } close: () -> @@ -95,234 +97,255 @@ mixins.push(Reflux.listenTo(JamTrackStore, 'onJamTrackChanged')) mediaName = @state.media.recordedTracks[0].recordingName closeLinkText = 'close recording' header = `

{mediaType}: {mediaName}

` - else if @state.jamTrackState.jamTrack? - jamTrack = @state.jamTrackState.jamTrack - mediaType = "JamTrack" - mediaName = jamTrack.name - closeLinkText = 'CLOSE JAMTRACK' + else if @state.media.mediaSummary.jamTrackOpen || @state.jamTrackState.jamTrack? + if @state.media.mediaSummary.isOpener || @state.jamTrackState.jamTrack? + # if you opened the JamTrack, then you get all the good info + jamTrack = @state.jamTrackState.jamTrack + mediaType = "JamTrack" + mediaName = jamTrack.name + closeLinkText = 'CLOSE JAMTRACK' + + selectedMixdown = jamTrack.activeMixdown - selectedMixdown = jamTrack.activeMixdown + if selectedMixdown? + jamTrackTypeHeader = 'Custom Mix' + disabled = true + if selectedMixdown.client_state? + switch selectedMixdown.client_state + when 'cant_open' + customMixName = `
{selectedMixdown.name}
` + when 'keying_timeout' + customMixName = `
{selectedMixdown.name}
` + when 'download_fail' + customMixName = `
{selectedMixdown.name}
` + when 'keying' + customMixName = `
Loading selected mix...
` + when 'downloading' + customMixName = `
Loading selected mix...
` + when 'ready' + customMixName = `
{selectedMixdown.name}
` + disabled = false + else + customMixName = `
Creating mixdown...
` - if selectedMixdown? - jamTrackTypeHeader = 'Custom Mix' - - disabled = true - if selectedMixdown.client_state? - switch selectedMixdown.client_state - when 'cant_open' - customMixName = `
{selectedMixdown.name}
` - when 'keying_timeout' - customMixName = `
{selectedMixdown.name}
` - when 'download_fail' - customMixName = `
{selectedMixdown.name}
` - when 'keying' - customMixName = `
Loading selected mix...
` - when 'downloading' - customMixName = `
Loading selected mix...
` - when 'ready' - customMixName = `
{selectedMixdown.name}
` - disabled = false else - customMixName = `
Creating mixdown...
` + if SessionStore.downloadingJamTrack + downloader = `` - else - if SessionStore.downloadingJamTrack - downloader = `` + jamTrackTypeHeader = `Full JamTrack {downloader}` - jamTrackTypeHeader = `Full JamTrack {downloader}` + header = ` +
+

{mediaType}: {mediaName}

+

{jamTrackTypeHeader}

+ {customMixName} +
` - header = ` -
-

{mediaType}: {mediaName}

-

{jamTrackTypeHeader}

- {customMixName} -
` + myMixes = null + if @state.showMyMixes + myMixdowns = [] - myMixes = null - if @state.showMyMixes - myMixdowns = [] + boundPlayClick = this.jamTrackPlay.bind(this, jamTrack); - boundPlayClick = this.jamTrackPlay.bind(this, jamTrack); - - active = jamTrack.last_mixdown_id == null - - myMixdowns.push ` -
-
- Full JamTrack -
-
- -
-
` - - 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 = `` - when 'QUIET' - action = `` - when 'QUEUED' - action = `` - when 'QUEUED_TIMEOUT' - action = `` - when 'SIGNING' - action = `` - when 'SIGNING_TIMEOUT' - action = `` - when 'SIGNED' - action = `` - when 'ERROR' - action = `` - else - action = `` - - if editing - mixdownName = `` - editIcon = `` - else - mixdownName = mixdown.name - editIcon = `` + active = jamTrack.last_mixdown_id == null myMixdowns.push ` -
-
- {mixdownName} -
-
- {action} +
+
+ Full JamTrack +
+
+ +
+
` - {editIcon} + 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 = `` + when 'QUIET' + action = `` + when 'QUEUED' + action = `` + when 'QUEUED_TIMEOUT' + action = `` + when 'SIGNING' + action = `` + when 'SIGNING_TIMEOUT' + action = `` + when 'SIGNED' + action = `` + when 'ERROR' + action = `` + else + action = `` + + if editing + mixdownName = `` + editIcon = `` + else + mixdownName = mixdown.name + editIcon = `` + + myMixdowns.push ` +
+
+ {mixdownName} +
+
+ {action} + + {editIcon} + + +
+
` + + myMixes = `
{myMixdowns}
` + + 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 = ` +
+

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.

+
+ + +
+
+ + +
+
+ + +
+
+ CREATE MIX + {errorHtml} +
+
+ +
` + + if @state.showMyMixes + showMyMixesText = `hide my mixes
` + else + showMyMixesText = `show my mixes
` + + if @state.showCustomMixes + showMixControlsText = `hide mix controls
` + else + showMixControlsText = `show mix controls
` + + + extraControls = ` +
+

My Mixes {showMyMixesText}

+ + {myMixes} + +

Create Custom Mix {showMixControlsText}

+ + {mixControls} + +
` + else + + mediaType = "JamTrack" + mediaName = @state.media.jamTrackName + closeLinkText = 'CLOSE JAMTRACK' + + # implies we have a mixdown + if @state.media.jamTrackMixdown.id? + jamTrackTypeHeader = 'Custom Mix' + else + jamTrackTypeHeader = 'Full JamTrack' + + header = ` +
+

{mediaType}: {mediaName}

+

{jamTrackTypeHeader}

+ {customMixName}
` - myMixes = `
{myMixdowns}
` - 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 = ` -
-

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.

-
- - -
-
- - -
-
- - -
-
- CREATE MIX - {errorHtml} -
-
- -
` - - if @state.showMyMixes - showMyMixesText = `hide my mixes
` - else - showMyMixesText = `show my mixes
` - - if @state.showCustomMixes - showMixControlsText = `hide mix controls
` - else - showMixControlsText = `show mix controls
` - - - extraControls = ` -
-

My Mixes {showMyMixesText}

- - {myMixes} - -

Create Custom Mix {showMixControlsText}

- - {mixControls} - -
` else if @state.media.mediaSummary.backingTrackOpen mediaType = "Audio File" @@ -444,14 +467,14 @@ mixins.push(Reflux.listenTo(JamTrackStore, 'onJamTrackChanged')) time = enqueued.queue_time if time == 0 - alert("It will take approximately 1 minute to create your custom mix.") + alert("Your custom mix will take about 1 minute to be created.") 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.") + alert("Your custom mix will take about #{msg} to be created.") createMix: (e) -> diff --git a/web/app/assets/javascripts/react-components/helpers/MixerHelper.js.coffee b/web/app/assets/javascripts/react-components/helpers/MixerHelper.js.coffee index c1fe26514..ddd8153a8 100644 --- a/web/app/assets/javascripts/react-components/helpers/MixerHelper.js.coffee +++ b/web/app/assets/javascripts/react-components/helpers/MixerHelper.js.coffee @@ -69,6 +69,7 @@ MIX_MODES = context.JK.MIX_MODES; backingTracks = @session.backingTracks() recordedJamTracks = @session.recordedJamTracks() jamTracks = @session.jamTracks() + jamTrackMixdown = @session.jamTrackMixdown() ### with mixer info, we use these to decide what kind of tracks are open in the backend @@ -92,6 +93,7 @@ MIX_MODES = context.JK.MIX_MODES; @metronomeTrackMixers = [] @adhocTrackMixers = [] + groupByType = (mixers, isLocalMixer) => for mixer in mixers mediaType = mixer.media_type @@ -106,7 +108,10 @@ MIX_MODES = context.JK.MIX_MODES; isJamTrack = false; - if jamTracks + if mixer.id == jamTrackMixdown.id + isJamTrack = true; + + if !isJamTrack && jamTracks # check if the ID matches that of an open jam track for jamTrack in jamTracks if mixer.id == jamTrack.id @@ -186,6 +191,8 @@ MIX_MODES = context.JK.MIX_MODES; backingTrackOpen: @backingTracks.length > 0 metronomeOpen: @session.isMetronomeOpen() + + # figure out if any media is open mediaOpenSummary = false for mediaType, mediaOpen of @mediaSummary diff --git a/websocket-gateway/bin/websocket_gateway b/websocket-gateway/bin/websocket_gateway index 456592701..0045cd96a 100755 --- a/websocket-gateway/bin/websocket_gateway +++ b/websocket-gateway/bin/websocket_gateway @@ -22,6 +22,8 @@ if jam_instance == 0 exit 1 end +#Resque.redis = "localhost:6380" + # now bring in the Jam code require 'jam_websockets' diff --git a/websocket-gateway/lib/jam_websockets/router.rb b/websocket-gateway/lib/jam_websockets/router.rb index 4d79c3647..223685460 100644 --- a/websocket-gateway/lib/jam_websockets/router.rb +++ b/websocket-gateway/lib/jam_websockets/router.rb @@ -147,6 +147,7 @@ module JamWebsockets # subscribe for any messages to users @user_topic.subscribe(:ack => false) do |headers, msg| + time_it('user_topic') { begin routing_key = headers.routing_key user_id = routing_key["user.".length..-1] @@ -175,6 +176,7 @@ module JamWebsockets @log.error "unhandled error in messaging to client" @log.error e end + } end MQRouter.user_exchange = @users_exchange @@ -189,6 +191,7 @@ module JamWebsockets # subscribe for any p2p messages to a client @client_topic.subscribe(:ack => false) do |headers, msg| + time_it('p2p_topic') { begin routing_key = headers.routing_key client_id = routing_key["client.".length..-1] @@ -240,6 +243,7 @@ module JamWebsockets @log.error "unhandled error in messaging to client" @log.error e end + } end MQRouter.client_exchange = @clients_exchange @@ -252,6 +256,7 @@ module JamWebsockets # subscribe for any p2p messages to a client @subscription_topic.subscribe(:ack => false) do |headers, msg| + time_it('subscribe_topic') { begin routing_key = headers.routing_key type_and_id = routing_key["subscription.".length..-1] @@ -276,6 +281,7 @@ module JamWebsockets @log.error "unhandled error in messaging to client for mount" @log.error e end + } end MQRouter.subscription_exchange = @subscriptions_exchange @@ -397,8 +403,10 @@ module JamWebsockets } client.onclose { - @log.debug "connection closed. marking stale: #{client.context}" - cleanup_client(client) + time_it('ws_close') { + @log.debug "connection closed. marking stale: #{client.context}" + cleanup_client(client) + } } client.onerror { |error| @@ -478,6 +486,7 @@ module JamWebsockets end def route(client_msg, client) + time_it('route') { message_type = @message_factory.get_message_type(client_msg) if message_type.nil? Diagnostic.unknown_message_type(client.user_id, client_msg) @@ -516,7 +525,7 @@ module JamWebsockets else raise SessionError, "client_msg.route_to is unknown type: #{client_msg.route_to}" end - + } end def handle_server_directed(client_msg, client) @@ -529,11 +538,11 @@ module JamWebsockets elsif client_msg.type == ClientMessage::Type::HEARTBEAT sane_logging { handle_heartbeat(client_msg.heartbeat, client_msg.message_id, client) } elsif client_msg.type == ClientMessage::Type::SUBSCRIBE_BULK - sane_logging { handle_bulk_subscribe(client_msg.subscribe_bulk, client) } + time_it('subscribe_bulk') { sane_logging { handle_bulk_subscribe(client_msg.subscribe_bulk, client) } } elsif client_msg.type == ClientMessage::Type::SUBSCRIBE - sane_logging { handle_subscribe(client_msg.subscribe, client) } + time_it('subscribe') { sane_logging { handle_subscribe(client_msg.subscribe, client) } } elsif client_msg.type == ClientMessage::Type::UNSUBSCRIBE - sane_logging { handle_unsubscribe(client_msg.unsubscribe, client) } + time_it('unsubscribe') { sane_logging { handle_unsubscribe(client_msg.unsubscribe, client) } } else raise SessionError, "unknown message type '#{client_msg.type}' for #{client_msg.route_to}-directed message" end @@ -1198,6 +1207,7 @@ module JamWebsockets # removes all resources associated with a client def cleanup_client(client) + time_it('cleanup_client') { client.close # unregister any subscriptions @@ -1225,6 +1235,7 @@ module JamWebsockets end end end + } end def stats_logged_in @@ -1245,6 +1256,16 @@ module JamWebsockets private + def time_it(cat, &blk) + start = Time.now + + blk.call + + time = Time.now - start + + @log.warn("LONG TIME: #{cat}: #{time}") if time > 1 + end + def sane_logging(&blk) # used around repeated transactions that cause too much ActiveRecord::Base logging begin diff --git a/websocket-gateway/lib/jam_websockets/server.rb b/websocket-gateway/lib/jam_websockets/server.rb index fd45f7c42..42cc42038 100644 --- a/websocket-gateway/lib/jam_websockets/server.rb +++ b/websocket-gateway/lib/jam_websockets/server.rb @@ -10,6 +10,8 @@ module JamWebsockets @count=0 @router = Router.new @ar_base_logger = ::Logging::Repository.instance[ActiveRecord::Base] + + @last_conn_check = nil end def run(options={}) @@ -26,6 +28,7 @@ module JamWebsockets rabbitmq_port = options[:rabbitmq_port].to_i allow_dynamic_registration = options[:allow_dynamic_registration].nil? ? true : options[:allow_dynamic_registration] + Stats::init(options) calling_thread = options[:calling_thread] @@ -61,6 +64,18 @@ module JamWebsockets EventMachine::stop_event_loop end + + def check_for_em_drift(timer) + # if our timer check is a full second off, say what's up + if Time.now - @last_conn_check > timer + 1 + @log.error("significant drift! Should be 2 seconds. Instead was: #{Time.now - @last_conn_check}") + end + + @last_conn_check = Time.now + end + + + def start_websocket_listener(listen_ip, port, trust_port, trust_check, emwebsocket_debug) EventMachine::WebSocket.run(:host => listen_ip, :port => port, :debug => emwebsocket_debug) do |ws| #@log.info "new client #{ws}" @@ -84,8 +99,11 @@ module JamWebsockets # one cleanup on startup @router.periodical_check_connections - EventMachine::PeriodicTimer.new(2) do - safety_net { sane_logging { @router.periodical_check_connections } } + @last_conn_check = Time.now + timer = 2 + EventMachine::PeriodicTimer.new(timer) do + check_for_em_drift(timer) + time_it('conn_expire') { safety_net { sane_logging { @router.periodical_check_connections } } } end end @@ -94,7 +112,7 @@ module JamWebsockets @router.periodical_check_clients EventMachine::PeriodicTimer.new(30) do - safety_net { sane_logging { @router.periodical_check_clients } } + time_it('client_expire') { safety_net { sane_logging { @router.periodical_check_clients } } } end end @@ -103,13 +121,13 @@ module JamWebsockets @router.periodical_flag_connections EventMachine::PeriodicTimer.new(2) do - safety_net { sane_logging { @router.periodical_flag_connections } } + time_it('conn_flagger') { safety_net { sane_logging { @router.periodical_flag_connections } } } end end def start_stats_dump EventMachine::PeriodicTimer.new(60) do - safety_net { @router.periodical_stats_dump } + time_it('stats_dump') { safety_net { @router.periodical_stats_dump } } end end @@ -126,6 +144,18 @@ module JamWebsockets @log.error("unhandled exception in EM Timer #{e}") end end + + def time_it(cat, &blk) + start = Time.now + + blk.call + + time = Time.now - start + + @log.warn("LONG TIME #{cat}: #{time}") if time > 1 + end + + def sane_logging(&blk) # used around repeated transactions that cause too much ActiveRecord::Base logging # example is handling heartbeats From e4e65f8c04538aa0a298ce832de7efd8d8600474 Mon Sep 17 00:00:00 2001 From: Seth Call Date: Tue, 22 Sep 2015 20:57:01 -0500 Subject: [PATCH 09/25] * add timings for websocket-gateway health issues --- db/up/crash_dumps_2.sql | 1 + .../lib/jam_websockets/router.rb | 179 +++++++++++++----- .../lib/jam_websockets/server.rb | 3 + 3 files changed, 131 insertions(+), 52 deletions(-) diff --git a/db/up/crash_dumps_2.sql b/db/up/crash_dumps_2.sql index 20603091c..96a5374b1 100644 --- a/db/up/crash_dumps_2.sql +++ b/db/up/crash_dumps_2.sql @@ -2,4 +2,5 @@ ALTER TABLE crash_dumps ADD COLUMN email VARCHAR(255); ALTER TABLE crash_dumps ADD COLUMN description VARCHAR(10000); ALTER TABLE crash_dumps ADD COLUMN os VARCHAR(100); ALTER TABLE crash_dumps ADD COLUMN os_version VARCHAR(100); +ALTER TABLE crash_dumps DROP CONSTRAINT crash_dumps_user_id_fkey; diff --git a/websocket-gateway/lib/jam_websockets/router.rb b/websocket-gateway/lib/jam_websockets/router.rb index 223685460..96d20c822 100644 --- a/websocket-gateway/lib/jam_websockets/router.rb +++ b/websocket-gateway/lib/jam_websockets/router.rb @@ -29,7 +29,9 @@ module JamWebsockets :connect_time_stale_browser, :max_connections_per_user, :gateway_name, - :client_lookup + :client_lookup, + :time_it_sums, + :profile_it_sums def initialize() @log = Logging.logger[self] @@ -54,11 +56,16 @@ module JamWebsockets @gateway_name = nil @ar_base_logger = ::Logging::Repository.instance[ActiveRecord::Base] @message_stats = {} + @time_it_sums = {} + @profile_it_sums = {} @login_success_count = 0 @login_fail_count = 0 @connected_count = 0 @disconnected_count = 0 + @user_message_counts = {} + @largest_message = nil + @largest_message_user = nil end def start(connect_time_stale_client, connect_time_expire_client, connect_time_stale_browser, connect_time_expire_browser, options={:host => "localhost", :port => 5672, :max_connections_per_user => 10, :gateway => 'default', :allow_dynamic_registration => true}, &block) @@ -382,28 +389,30 @@ module JamWebsockets client.onopen { |handshake| - stats_connected + time_it('onopen') { + stats_connected - # a unique ID for this TCP connection, to aid in debugging - client.channel_id = handshake.query["channel_id"] + # a unique ID for this TCP connection, to aid in debugging + client.channel_id = handshake.query["channel_id"] - @log.debug "client connected #{client} with channel_id: #{client.channel_id}" + @log.debug "client connected #{client} with channel_id: #{client.channel_id}" - # check for '?pb' or '?pb=true' in url query parameters - query_pb = handshake.query["pb"] + # check for '?pb' or '?pb=true' in url query parameters + query_pb = handshake.query["pb"] - if !query_pb.nil? && (query_pb == "" || query_pb == "true") - client.encode_json = false - end + if !query_pb.nil? && (query_pb == "" || query_pb == "true") + client.encode_json = false + end - websocket_comm(client, nil) do - handle_login(client, handshake.query, handshake.headers["X-Forwarded-For"]) - end + websocket_comm(client, nil) do + handle_login(client, handshake.query, handshake.headers["X-Forwarded-For"]) + end + } } client.onclose { - time_it('ws_close') { + time_it('onclose') { @log.debug "connection closed. marking stale: #{client.context}" cleanup_client(client) } @@ -424,6 +433,12 @@ module JamWebsockets msg = nil + if @largest_message.nil? || data.length > @largest_message.length + @largest_message = data + @largest_message_user = client.user_id + end + + # extract the message safely websocket_comm(client, nil) do if client.encode_json @@ -486,7 +501,6 @@ module JamWebsockets end def route(client_msg, client) - time_it('route') { message_type = @message_factory.get_message_type(client_msg) if message_type.nil? Diagnostic.unknown_message_type(client.user_id, client_msg) @@ -512,20 +526,20 @@ module JamWebsockets elsif @message_factory.client_directed? client_msg to_client_id = client_msg.route_to[MessageFactory::CLIENT_TARGET_PREFIX.length..-1] - handle_client_directed(to_client_id, client_msg, client) + time_it('client_directed') { handle_client_directed(to_client_id, client_msg, client) } elsif @message_factory.session_directed? client_msg session_id = client_msg.target[MessageFactory::SESSION_TARGET_PREFIX.length..-1] - handle_session_directed(session_id, client_msg, client) + time_it('session_directed') { handle_session_directed(session_id, client_msg, client) } elsif @message_factory.user_directed? client_msg user_id = client_msg.target[MessageFactory::USER_PREFIX_TARGET.length..-1] - handle_user_directed(user_id, client_msg, client) + time_it('user_directed') { handle_user_directed(user_id, client_msg, client) } else raise SessionError, "client_msg.route_to is unknown type: #{client_msg.route_to}" end - } + end def handle_server_directed(client_msg, client) @@ -533,10 +547,10 @@ module JamWebsockets if client_msg.type == ClientMessage::Type::LOGIN - handle_login(client_msg.login, client) + time_it('login') { handle_login(client_msg.login, client) } elsif client_msg.type == ClientMessage::Type::HEARTBEAT - sane_logging { handle_heartbeat(client_msg.heartbeat, client_msg.message_id, client) } + time_it('heartbeat') { sane_logging { handle_heartbeat(client_msg.heartbeat, client_msg.message_id, client) } } elsif client_msg.type == ClientMessage::Type::SUBSCRIBE_BULK time_it('subscribe_bulk') { sane_logging { handle_bulk_subscribe(client_msg.subscribe_bulk, client) } } elsif client_msg.type == ClientMessage::Type::SUBSCRIBE @@ -838,39 +852,56 @@ module JamWebsockets def handle_heartbeat(heartbeat, heartbeat_message_id, client) unless context = @clients[client] - @log.warn "*** WARNING: unable to find context when handling heartbeat. client_id=#{client.client_id}; killing session" - #Diagnostic.missing_client_state(client.user_id, client.context) - raise SessionError, 'context state is gone. please reconnect.' + profile_it('heartbeat_context_gone') { + @log.warn "*** WARNING: unable to find context when handling heartbeat. client_id=#{client.client_id}; killing session" + #Diagnostic.missing_client_state(client.user_id, client.context) + raise SessionError, 'context state is gone. please reconnect.' + } else - connection = Connection.find_by_client_id(context.client.client_id) + connection = nil + profile_it('heartbeat_find_conn') { + connection = Connection.find_by_client_id(context.client.client_id) + } track_changes_counter = nil if connection.nil? - @log.warn "*** WARNING: unable to find connection when handling heartbeat. context= #{context}; killing session" - Diagnostic.missing_connection(client.user_id, client.context) - raise SessionError, 'connection state is gone. please reconnect.' + profile_it('heartbeat_diag_missing') { + @log.warn "*** WARNING: unable to find connection when handling heartbeat. context= #{context}; killing session" + Diagnostic.missing_connection(client.user_id, client.context) + raise SessionError, 'connection state is gone. please reconnect.' + } else - Connection.transaction do - # send back track_changes_counter if in a session - if connection.music_session_id - music_session = ActiveMusicSession.select(:track_changes_counter).find_by_id(connection.music_session_id) - track_changes_counter = music_session.track_changes_counter if music_session + #profile_it('heartbeat_transaction') { + #Connection.transaction do + # send back track_changes_counter if in a session + profile_it('heartbeat_session') { + if connection.music_session_id + music_session = ActiveMusicSession.select(:track_changes_counter).find_by_id(connection.music_session_id) + track_changes_counter = music_session.track_changes_counter if music_session + end + } + + profile_it('heartbeat_touch') { + # update connection updated_at + connection.touch + } + + profile_it('heartbeat_notification') { + # update user's notification_seen_at field if the heartbeat indicates it saw one + # first we try to use the notification id, which should usually exist. + # if not, then fallback to notification_seen_at, which is approximately the last time we saw a notification + update_notification_seen_at(connection, context, heartbeat) if client.context.client_type != Connection::TYPE_LATENCY_TESTER + } + #end + #} + + profile_it('heartbeat_stale') { + if connection.stale? + ConnectionManager.active_record_transaction do |connection_manager| + heartbeat_interval, connection_stale_time, connection_expire_time = determine_connection_times(context.user, context.client_type) + connection_manager.reconnect(connection, client.channel_id, connection.music_session_id, nil, connection_stale_time, connection_expire_time, nil, @gateway_name) + end end - - # update connection updated_at - connection.touch - - # update user's notification_seen_at field if the heartbeat indicates it saw one - # first we try to use the notification id, which should usually exist. - # if not, then fallback to notification_seen_at, which is approximately the last time we saw a notification - update_notification_seen_at(connection, context, heartbeat) if client.context.client_type != Connection::TYPE_LATENCY_TESTER - end - - if connection.stale? - ConnectionManager.active_record_transaction do |connection_manager| - heartbeat_interval, connection_stale_time, connection_expire_time = determine_connection_times(context.user, context.client_type) - connection_manager.reconnect(connection, client.channel_id, connection.music_session_id, nil, connection_stale_time, connection_expire_time, nil, @gateway_name) - end - end + } end heartbeat_ack = @message_factory.heartbeat_ack(track_changes_counter) @@ -1130,7 +1161,33 @@ module JamWebsockets stats = @message_stats.sort_by{|k,v| -v} stats.map { |i| i[1] = (i[1] / 60.0).round(2) } - @log.info("msg/s: " + stats.map { |i| i.join('=>') }.join(', ')); + @log.info("msg/s: " + stats.map { |i| i.join('=>') }.join(', ')) + @log.info("largest msg from #{@largest_message_user}: #{@largest_message.length}b") + + + total_time = 0 + time_sums = @time_it_sums.sort_by{|k,v| -v} + + log_num = 3 + count = 0 + time_sums.each do | cat, cat_time | + count += 1 + if count <= log_num + @log.info("timed #{cat} used time: #{cat_time}") + end + + total_time += cat_time + end + + @log.info("total used time: #{total_time}") + + profile_sums = @profile_it_sums.sort_by{|k,v| -v} + profile_sums.each do | cat, cat_time | + @log.info("profiled #{cat} used time: #{cat_time}") + + end + + # stuff in extra stats into the @message_stats and send it all off @@ -1139,6 +1196,7 @@ module JamWebsockets @message_stats['login_fail'] = @login_fail_count @message_stats['connected'] = @connected_count @message_stats['disconnected'] = @disconnected_count + @message_stats['largest_msg'] = @largest_message ? @largest_message.length : 0 Stats.write('gateway.stats', @message_stats) @@ -1148,6 +1206,11 @@ module JamWebsockets @login_fail_count = 0 @connected_count = 0 @disconnected_count = 0 + @user_message_counts = {} + @largest_message = nil + @largest_message_user = nil + @time_it_sums = {} + end def cleanup_clients_with_ids(expired_connections) @@ -1207,7 +1270,6 @@ module JamWebsockets # removes all resources associated with a client def cleanup_client(client) - time_it('cleanup_client') { client.close # unregister any subscriptions @@ -1235,7 +1297,6 @@ module JamWebsockets end end end - } end def stats_logged_in @@ -1263,6 +1324,20 @@ module JamWebsockets time = Time.now - start + @time_it_sums[cat] = (@time_it_sums[cat] || 0 )+ time + + @log.warn("LONG TIME: #{cat}: #{time}") if time > 1 + end + + def profile_it(cat, &blk) + start = Time.now + + blk.call + + time = Time.now - start + + @profile_it_sums[cat] = (@profile_it_sums[cat] || 0 )+ time + @log.warn("LONG TIME: #{cat}: #{time}") if time > 1 end diff --git a/websocket-gateway/lib/jam_websockets/server.rb b/websocket-gateway/lib/jam_websockets/server.rb index 42cc42038..0d194a4b2 100644 --- a/websocket-gateway/lib/jam_websockets/server.rb +++ b/websocket-gateway/lib/jam_websockets/server.rb @@ -152,6 +152,9 @@ module JamWebsockets time = Time.now - start + + @router.time_it_sums[cat] = (@router.time_it_sums[cat] || 0) + time + @log.warn("LONG TIME #{cat}: #{time}") if time > 1 end From 0f9b1c3f0778d798920058235ad0d705d01dbb2a Mon Sep 17 00:00:00 2001 From: Seth Call Date: Tue, 22 Sep 2015 21:34:34 -0500 Subject: [PATCH 10/25] * don't find against notifications for a blank notify --- web/config/application.rb | 8 ++++---- web/config/environments/development.rb | 8 ++++---- websocket-gateway/lib/jam_websockets/router.rb | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/web/config/application.rb b/web/config/application.rb index f87e02c44..febfd4114 100644 --- a/web/config/application.rb +++ b/web/config/application.rb @@ -109,10 +109,10 @@ if defined?(Bundler) # Websocket-gateway embedded configs config.websocket_gateway_enable = false - config.websocket_gateway_connect_time_stale_client = 40 # 40 matches production - config.websocket_gateway_connect_time_expire_client = 60 # 60 matches production - config.websocket_gateway_connect_time_stale_browser = 40 # 40 matches production - config.websocket_gateway_connect_time_expire_browser = 60 # 60 matches production + config.websocket_gateway_connect_time_stale_client = 80 + config.websocket_gateway_connect_time_expire_client = 120 + config.websocket_gateway_connect_time_stale_browser = 80 + config.websocket_gateway_connect_time_expire_browser = 120 config.websocket_gateway_cidr = ['0.0.0.0/0'] config.websocket_gateway_internal_debug = false config.websocket_gateway_port = 6767 + ENV['JAM_INSTANCE'].to_i diff --git a/web/config/environments/development.rb b/web/config/environments/development.rb index 032b3a33e..d2143a987 100644 --- a/web/config/environments/development.rb +++ b/web/config/environments/development.rb @@ -69,10 +69,10 @@ SampleApp::Application.configure do # it's nice to have even admin accounts (which all the default ones are) generate GA data for testing config.ga_suppress_admin = false - config.websocket_gateway_connect_time_stale_client = 40 # 40 matches production - config.websocket_gateway_connect_time_expire_client = 60 # 60 matches production - config.websocket_gateway_connect_time_stale_browser = 40 # 40 matches production - config.websocket_gateway_connect_time_expire_browser = 60 # 60 matches production + config.websocket_gateway_connect_time_stale_client = 80 + config.websocket_gateway_connect_time_expire_client = 120 + config.websocket_gateway_connect_time_stale_browser = 80 + config.websocket_gateway_connect_time_expire_browser = 120 config.audiomixer_path = ENV['AUDIOMIXER_PATH'] || audiomixer_workspace_path || "/var/lib/audiomixer/audiomixer/audiomixerapp" diff --git a/websocket-gateway/lib/jam_websockets/router.rb b/websocket-gateway/lib/jam_websockets/router.rb index 96d20c822..0757db0b8 100644 --- a/websocket-gateway/lib/jam_websockets/router.rb +++ b/websocket-gateway/lib/jam_websockets/router.rb @@ -924,7 +924,7 @@ module JamWebsockets def update_notification_seen_at(connection, context, heartbeat) notification_id_field = heartbeat.notification_seen if heartbeat.value_for_tag(1) - if notification_id_field + if notification_id_field && notification_id_field != '' notification = Notification.find_by_id(notification_id_field) if notification connection.user.notification_seen_at = notification.created_at From 284e671b01d6991aa55b8cba11dcfa65a6743255 Mon Sep 17 00:00:00 2001 From: Seth Call Date: Wed, 23 Sep 2015 09:18:00 -0500 Subject: [PATCH 11/25] * VRFS-3571 - fixes to temp ban user if they are spamming server --- web/app/assets/javascripts/JamServer.js | 52 ++++++++++- .../lib/jam_websockets/router.rb | 87 +++++++++++++++++-- .../lib/jam_websockets/server.rb | 8 +- 3 files changed, 135 insertions(+), 12 deletions(-) diff --git a/web/app/assets/javascripts/JamServer.js b/web/app/assets/javascripts/JamServer.js index ee570a513..55dd7cc00 100644 --- a/web/app/assets/javascripts/JamServer.js +++ b/web/app/assets/javascripts/JamServer.js @@ -23,6 +23,7 @@ var mode = null; // heartbeat + var startHeartbeatTimeout = null; var heartbeatInterval = null; var heartbeatMS = null; var connection_expire_time = null; @@ -104,6 +105,12 @@ heartbeatInterval = null; } + // stop the heartbeat start delay from happening + if (startHeartbeatTimeout != null) { + clearTimeout(startHeartbeatTimeout); + startHeartbeatTimeout = null; + } + // stop checking for heartbeat acks if (heartbeatAckCheckInterval != null) { clearTimeout(heartbeatAckCheckInterval); @@ -236,9 +243,41 @@ heartbeatMS = payload.heartbeat_interval * 1000; connection_expire_time = payload.connection_expire_time * 1000; logger.info("loggedIn(): clientId=" + app.clientId + " heartbeat=" + payload.heartbeat_interval + "s expire_time=" + payload.connection_expire_time + 's'); - heartbeatInterval = context.setInterval(_heartbeat, heartbeatMS); - heartbeatAckCheckInterval = context.setInterval(_heartbeatAckCheck, 1000); - lastHeartbeatAckTime = new Date(new Date().getTime() + heartbeatMS); // add a little forgiveness to server for initial heartbeat + + // add some randomness to help move heartbeats apart from each other + + // send 1st heartbeat somewhere between 0 - 0.5 of the connection expire time + var randomStartTime = connection_expire_time * (Math.random() / 2) + + if (startHeartbeatTimeout) { + logger.warn("start heartbeat timeout is active; should be null") + clearTimeout(startHeartbeatTimeout) + } + + if (heartbeatInterval != null) { + logger.warn("heartbeatInterval is active; should be null") + clearInterval(heartbeatInterval); + heartbeatInterval = null; + } + + if (heartbeatAckCheckInterval != null) { + logger.warn("heartbeatAckCheckInterval is active; should be null") + clearInterval(heartbeatAckCheckInterval); + heartbeatAckCheckInterval = null; + } + + startHeartbeatTimeout = setTimeout(function() { + if(server.connected) { + heartbeatInterval = context.setInterval(_heartbeat, heartbeatMS); + heartbeatAckCheckInterval = context.setInterval(_heartbeatAckCheck, 1000); + lastHeartbeatAckTime = new Date(new Date().getTime() + heartbeatMS); // add a little forgiveness to server for initial heartbeat + } + }, randomStartTime) + + logger.info("starting heartbeat timer in " + randomStartTime/1000 + 's') + + + connectDeferred.resolve(); $self.triggerHandler(EVENTS.CONNECTION_UP) @@ -295,6 +334,11 @@ logger.debug(payload.error_code + ": no longer reconnecting") server.noReconnect = true; // stop trying to log in!! } + else if (payload.error_code == 'no_reconnect') { + logger.debug(payload.error_code + ": no longer reconnecting") + server.noReconnect = true; // stop trying to log in!! + context.JK.Banner.showAlert("Misbehaved Client", "Please restart your application in order to continue using JamKazam.") + } } /////////////////// @@ -384,7 +428,7 @@ } function formatDelaySecs(secs) { - return $('' + secs + ' ' + (secs == 1 ? ' second.s' : 'seconds.') + ''); + return $('' + secs + ' ' + (secs == 1 ? ' second.s' : 'seconds.') + ''); } function setCountdown($parent) { diff --git a/websocket-gateway/lib/jam_websockets/router.rb b/websocket-gateway/lib/jam_websockets/router.rb index 0757db0b8..fd5202450 100644 --- a/websocket-gateway/lib/jam_websockets/router.rb +++ b/websocket-gateway/lib/jam_websockets/router.rb @@ -27,11 +27,17 @@ module JamWebsockets :heartbeat_interval_browser, :connect_time_expire_browser, :connect_time_stale_browser, + :maximum_minutely_heartbeat_rate_browser, + :maximum_minutely_heartbeat_rate_client, :max_connections_per_user, :gateway_name, :client_lookup, :time_it_sums, - :profile_it_sums + :profile_it_sums, + :highest_drift, + :heartbeat_tracker + :temp_ban + def initialize() @log = Logging.logger[self] @@ -53,11 +59,15 @@ module JamWebsockets @heartbeat_interval_browser= nil @connect_time_expire_browser= nil @connect_time_stale_browser= nil + @maximum_minutely_heartbeat_rate_browser = nil + @maximum_minutely_heartbeat_rate_client = nil @gateway_name = nil @ar_base_logger = ::Logging::Repository.instance[ActiveRecord::Base] @message_stats = {} @time_it_sums = {} @profile_it_sums = {} + @heartbeat_tracker = {} + @temp_ban = {} @login_success_count = 0 @login_fail_count = 0 @@ -66,6 +76,8 @@ module JamWebsockets @user_message_counts = {} @largest_message = nil @largest_message_user = nil + @highest_drift = 0 + end def start(connect_time_stale_client, connect_time_expire_client, connect_time_stale_browser, connect_time_expire_browser, options={:host => "localhost", :port => 5672, :max_connections_per_user => 10, :gateway => 'default', :allow_dynamic_registration => true}, &block) @@ -82,6 +94,11 @@ module JamWebsockets @gateway_name = options[:gateway] @allow_dynamic_registration = options[:allow_dynamic_registration] + # determine the maximum amount of heartbeats we should get per user + @maximum_minutely_heartbeat_rate_client = ((@heartbeat_interval_client / 60.0) * 2).ceil + 3 + @maximum_minutely_heartbeat_rate_browser = ((@heartbeat_interval_browser / 60.0) * 2).ceil + 3 + + @log.info("maxmium minutely timer #{maximum_minutely_heartbeat_rate_client}") begin @amqp_connection_manager = AmqpConnectionManager.new(true, 4, :host => options[:host], :port => options[:port]) @amqp_connection_manager.connect do |channel| @@ -850,6 +867,36 @@ module JamWebsockets end end + def add_to_ban(user, reason) + user_ban = @temp_ban[user.id] + + if user_ban.nil? + user_ban = {} + @temp_ban[user.id] = user_ban + end + + # allow user back in, after 10 minutes + user_ban[:allow] = Time.now + 600 + + @log.info("user #{user} banned for 10 minutes. reason #{reason}") + end + + def runaway_heartbeat(heartbeat, context) + heartbeat_count = @heartbeat_tracker[context.user.id] || 0 + heartbeat_count += 1 + @heartbeat_tracker[context.user.id] = heartbeat_count + + if heartbeat_count > (context.client_type == 'browser' ? @maximum_minutely_heartbeat_rate_browser : @maximum_minutely_heartbeat_rate_client) + @log.warn("user #{context.user} sending too many heartbeats: #{heartbeat_count}") if heartbeat_count % 100 == 0 + + add_to_ban(context.user, 'too many heartbeats') + raise SessionError.new('too many heartbeats', 'empty_login') + else + false + end + + end + def handle_heartbeat(heartbeat, heartbeat_message_id, client) unless context = @clients[client] profile_it('heartbeat_context_gone') { @@ -858,6 +905,10 @@ module JamWebsockets raise SessionError, 'context state is gone. please reconnect.' } else + if runaway_heartbeat(heartbeat, context) + return + end + connection = nil profile_it('heartbeat_find_conn') { connection = Connection.find_by_client_id(context.client.client_id) @@ -961,6 +1012,13 @@ module JamWebsockets @log.debug "no user found with token #{token}" return nil else + + # check against temp ban list + if @temp_ban[user.id] + @log.debug("user #{user} is still banned; rejecting login") + raise SessionError.new('login rejected temporarily', 'empty_login') + end + @log.debug "#{user} login via token" return user end @@ -971,6 +1029,12 @@ module JamWebsockets # attempt login with username and password user = User.find_by_email(username) + # check against temp ban list + if !user.nil? && @temp_ban[user.id] + @log.debug("user #{user} is still banned; rejecting login") + raise SessionError.new('login rejected temporarily', 'empty_login') + end + if !user.nil? && user.valid_password?(password) @log.debug "#{user} login via password" return user @@ -1157,13 +1221,17 @@ module JamWebsockets end def periodical_stats_dump + # assume 60 seconds per status dump stats = @message_stats.sort_by{|k,v| -v} stats.map { |i| i[1] = (i[1] / 60.0).round(2) } @log.info("msg/s: " + stats.map { |i| i.join('=>') }.join(', ')) - @log.info("largest msg from #{@largest_message_user}: #{@largest_message.length}b") + @log.info("largest msg from #{@largest_message_user}: #{@largest_message ? @largest_message.length : 0}b") + if @highest_drift > 1 + @log.info("highest drift: #{@highest_drift - 2}") + end total_time = 0 time_sums = @time_it_sums.sort_by{|k,v| -v} @@ -1184,11 +1252,15 @@ module JamWebsockets profile_sums = @profile_it_sums.sort_by{|k,v| -v} profile_sums.each do | cat, cat_time | @log.info("profiled #{cat} used time: #{cat_time}") - end - + @temp_ban.each do |user_id, data| + if Time.now > data[:allow] + @log.info("user #{user_id} allowed back in") + @temp_ban.delete(user_id) + end + end # stuff in extra stats into the @message_stats and send it all off @message_stats['gateway_name'] = @gateway_name @@ -1197,6 +1269,10 @@ module JamWebsockets @message_stats['connected'] = @connected_count @message_stats['disconnected'] = @disconnected_count @message_stats['largest_msg'] = @largest_message ? @largest_message.length : 0 + @message_stats['highest_drift'] = @highest_drift - 2 # 2 comes from the server's 2 second timer for the drift check + @message_stats['total_time'] = total_time + @message_stats['banned_users'] = @temp_ban.length + Stats.write('gateway.stats', @message_stats) @@ -1210,7 +1286,8 @@ module JamWebsockets @largest_message = nil @largest_message_user = nil @time_it_sums = {} - + @highest_drift = 0 + @heartbeat_tracker = {} end def cleanup_clients_with_ids(expired_connections) diff --git a/websocket-gateway/lib/jam_websockets/server.rb b/websocket-gateway/lib/jam_websockets/server.rb index 0d194a4b2..25322dc98 100644 --- a/websocket-gateway/lib/jam_websockets/server.rb +++ b/websocket-gateway/lib/jam_websockets/server.rb @@ -67,9 +67,8 @@ module JamWebsockets def check_for_em_drift(timer) # if our timer check is a full second off, say what's up - if Time.now - @last_conn_check > timer + 1 - @log.error("significant drift! Should be 2 seconds. Instead was: #{Time.now - @last_conn_check}") - end + drift = Time.now - @last_conn_check + @router.highest_drift = drift if drift > @router.highest_drift @last_conn_check = Time.now end @@ -142,6 +141,9 @@ module JamWebsockets rescue => e Bugsnag.notify(e) @log.error("unhandled exception in EM Timer #{e}") + puts "Error during processing: #{$!}" + puts "Backtrace:\n\t#{e.backtrace.join("\n\t")}" + end end From c379c6dc441b526a402a4e4ea5f6ea40108e68f0 Mon Sep 17 00:00:00 2001 From: Seth Call Date: Wed, 23 Sep 2015 10:38:38 -0500 Subject: [PATCH 12/25] * VRFS-3576 - deal with case that mixdown is not yet created --- .../MediaControls.js.jsx.coffee | 19 +++++++++++++------ .../PopupMediaControls.js.jsx.coffee | 5 ++++- .../SessionMediaTracks.js.jsx.coffee | 9 ++++++--- 3 files changed, 23 insertions(+), 10 deletions(-) diff --git a/web/app/assets/javascripts/react-components/MediaControls.js.jsx.coffee b/web/app/assets/javascripts/react-components/MediaControls.js.jsx.coffee index e4a1d9d42..ae20ed587 100644 --- a/web/app/assets/javascripts/react-components/MediaControls.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/MediaControls.js.jsx.coffee @@ -19,9 +19,11 @@ MixerActions = reactContext.MixerActions MediaPlaybackStore = reactContext.MediaPlaybackStore SessionActions = reactContext.SessionActions MediaPlaybackActions = reactContext.MediaPlaybackActions +JamTrackStore = reactContext.JamTrackStore mixins.push(Reflux.listenTo(MixerStore,"onInputsChanged")) mixins.push(Reflux.listenTo(MediaPlaybackStore, 'onMediaStateChanged')) +mixins.push(Reflux.listenTo(JamTrackStore, 'onJamTrackStateChanged')) @MediaControls = React.createClass({ @@ -29,6 +31,10 @@ mixins.push(Reflux.listenTo(MediaPlaybackStore, 'onMediaStateChanged')) mixins: mixins tempos : [ 40, 42, 44, 46, 48, 50, 52, 54, 56, 58, 60, 63, 66, 69, 72, 76, 80, 84, 88, 92, 96, 100, 104, 108, 112, 116, 120, 126, 132, 138, 144, 152, 160, 168, 176, 184, 192, 200, 208 ] + onJamTrackStateChanged: (jamTrackState) -> + @monitorControls(@state.controls, @state.mediaSummary, jamTrackState) + @setState({jamTrackState: jamTrackState}) + onMediaStateChanged: (changes) -> if changes.playbackStateChanged if @state.controls? @@ -53,7 +59,7 @@ mixins.push(Reflux.listenTo(MediaPlaybackStore, 'onMediaStateChanged')) mediaSummary = mixers.mediaSummary metro = mixers.metro - @monitorControls(@state.controls, mediaSummary) + @monitorControls(@state.controls, mediaSummary, @state.jamTrackState) @setState({mediaSummary: mediaSummary, metro: metro}) @updateMetronomeDetails(metro, @state.initializedMetronomeControls) @@ -69,10 +75,10 @@ mixins.push(Reflux.listenTo(MediaPlaybackStore, 'onMediaStateChanged')) logger.debug("settingcricket", mode) $root.find('#metronome-playback-select').metronomeSetPlaybackMode(mode) - monitorControls: (controls, mediaSummary) -> + monitorControls: (controls, mediaSummary, jamTrackState) -> - if mediaSummary.mediaOpen || mediaSummary.jamTrack? - if mediaSummary.jamTrackOpen? + if mediaSummary.mediaOpen || mediaSummary.jamTrack? || jamTrackState?.jamTrack? + if mediaSummary.jamTrackOpen? || mediaSummary.jamTrack? || jamTrackState?.jamTrack? controls.startMonitor(PLAYBACK_MONITOR_MODE.JAMTRACK) else if mediaSummary.backingTrackOpen controls.startMonitor(PLAYBACK_MONITOR_MODE.MEDIA_FILE) @@ -203,10 +209,11 @@ mixins.push(Reflux.listenTo(MediaPlaybackStore, 'onMediaStateChanged')) mediaSummary = MixerStore.mixers.mediaSummary metro = MixerStore.mixers.metro + jamTrackState = JamTrackStore.getState() - @monitorControls(controls, mediaSummary) + @monitorControls(controls, mediaSummary, jamTrackState) @tryPrepareMetronome(metro) - @setState({mediaSummary: mediaSummary, controls: controls, metro: metro}) + @setState({mediaSummary: mediaSummary, controls: controls, metro: metro, jamTrackState: jamTrackState}) }) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/PopupMediaControls.js.jsx.coffee b/web/app/assets/javascripts/react-components/PopupMediaControls.js.jsx.coffee index a475ffecd..22f2a8034 100644 --- a/web/app/assets/javascripts/react-components/PopupMediaControls.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/PopupMediaControls.js.jsx.coffee @@ -128,7 +128,10 @@ mixins.push(Reflux.listenTo(JamTrackStore, 'onJamTrackChanged')) customMixName = `
{selectedMixdown.name}
` disabled = false else - customMixName = `
Creating mixdown...
` + if selectedMixdown.myPackage + customMixName = `
Creating mixdown...
` + else + customMixName = `
{selectedMixdown.name}
` else if SessionStore.downloadingJamTrack diff --git a/web/app/assets/javascripts/react-components/SessionMediaTracks.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionMediaTracks.js.jsx.coffee index afb9a13f2..d8c2210bd 100644 --- a/web/app/assets/javascripts/react-components/SessionMediaTracks.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/SessionMediaTracks.js.jsx.coffee @@ -15,6 +15,8 @@ ChannelGroupIds = context.JK.ChannelGroupIds Reflux.listenTo(@JamTrackStore, "onJamTrackStateChanged")] onJamTrackStateChanged: (jamTrackState) -> + @setState({jamTrackState: jamTrackState}) + if jamTrackState.fullTrackActivated || jamTrackState.opened && jamTrackState.jamTrack.activeMixdown == null @loadJamTrack(jamTrackState.jamTrack) else if jamTrackState.closed @@ -29,6 +31,7 @@ ChannelGroupIds = context.JK.ChannelGroupIds SessionActions.closeMedia(true) + #inputsChangedProcessed: (state) -> @@ -230,7 +233,7 @@ ChannelGroupIds = context.JK.ChannelGroupIds # give the users options to close it if this.state.mediaSummary.recordingOpen mediaType = "Recording" - else if this.state.mediaSummary.jamTrackOpen + else if this.state.mediaSummary.jamTrackOpen || @state.jamTrackState?.jamTrack? mediaType = "JamTrack" else if this.state.mediaSummary.backingTrackOpen mediaType = "Audio File" @@ -308,7 +311,7 @@ ChannelGroupIds = context.JK.ChannelGroupIds getInitialState:() -> - {mediaSummary:{mediaOpen: false}, isRecording: false, backingTracks: [], jamTracks: [], recordedTracks: [], metronome: null} + {mediaSummary:{mediaOpen: false}, isRecording: false, backingTracks: [], jamTracks: [], recordedTracks: [], metronome: null, jamTrackState: {}} onAppInit: (app) -> @app = app @@ -343,7 +346,7 @@ ChannelGroupIds = context.JK.ChannelGroupIds @handlePopup() handlePopup: () -> - if @state.mediaSummary.userNeedsMediaControls + if @state.mediaSummary.userNeedsMediaControls || @state.jamTrackState?.jamTrack? 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') From 6c5c4731eda36224f001d697a3b408772a1d5c00 Mon Sep 17 00:00:00 2001 From: Seth Call Date: Wed, 23 Sep 2015 12:03:40 -0500 Subject: [PATCH 13/25] * VRFS-3577 - hide open media controls while JamTrack mixdown is downloading --- .../react-components/SessionMediaTracks.js.jsx.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/app/assets/javascripts/react-components/SessionMediaTracks.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionMediaTracks.js.jsx.coffee index d8c2210bd..ca7012871 100644 --- a/web/app/assets/javascripts/react-components/SessionMediaTracks.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/SessionMediaTracks.js.jsx.coffee @@ -228,7 +228,7 @@ ChannelGroupIds = context.JK.ChannelGroupIds contents = closeOptions - else if this.state.mediaSummary.mediaOpen + else if this.state.mediaSummary.mediaOpen || @state.jamTrackState?.jamTrack? # give the users options to close it if this.state.mediaSummary.recordingOpen From 0aa2f215b62982d5476a0bf91408b9e3b135dd16 Mon Sep 17 00:00:00 2001 From: Seth Call Date: Wed, 23 Sep 2015 13:57:14 -0500 Subject: [PATCH 14/25] * VRFS-3580 - empty windows shouldn't show --- .../PopupMediaControls.js.jsx.coffee | 27 +++++++++++++------ .../helpers/MixerHelper.js.coffee | 4 +++ .../stores/JamTrackStore.js.coffee | 2 ++ 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/web/app/assets/javascripts/react-components/PopupMediaControls.js.jsx.coffee b/web/app/assets/javascripts/react-components/PopupMediaControls.js.jsx.coffee index 22f2a8034..0d587d39e 100644 --- a/web/app/assets/javascripts/react-components/PopupMediaControls.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/PopupMediaControls.js.jsx.coffee @@ -18,6 +18,7 @@ if window.opener? if accessOpener SessionActions = window.opener.SessionActions MixerActions = window.opener.MixerActions + MixerStore = window.opener.MixerStore JamTrackActions = window.opener.JamTrackActions JamTrackMixdownActions = window.opener.JamTrackMixdownActions #JamTrackMixdownStore = window.opener.JamTrackMixdownStore @@ -33,10 +34,8 @@ mixins.push(Reflux.listenTo(JamTrackStore, 'onJamTrackChanged')) mixins: mixins - onMixersChanged: (sessionMixers) -> + updateFromMixerHelper: (mixers, session) -> - 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 @@ -51,15 +50,19 @@ mixins.push(Reflux.listenTo(JamTrackStore, 'onJamTrackChanged')) jamTrackName: mixers.jamTrackName() jamTrackMixdown: session.jamTrackMixdown() - @setState(media: state, downloadingJamTrack: session.downloadingJamTrack) + return {media: state, downloadingJamTrack: session.downloadingJamTrack} + + onMixersChanged: (sessionMixers) -> + session = sessionMixers.session + mixers = sessionMixers.mixers + + @setState(@updateFromMixerHelper(mixers, session)) + 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}) @@ -70,7 +73,15 @@ mixins.push(Reflux.listenTo(JamTrackStore, 'onJamTrackChanged')) SessionActions.showNativeMetronomeGui() getInitialState: () -> - { + + if accessOpener + + state = @updateFromMixerHelper(MixerStore.mixers, MixerStore.session) + state.jamTrackState = JamTrackStore.getState() + return state + else + + return { media: @props.media, mixdown: @props.mixdown, jamTrackState: @props.jamTrackState, diff --git a/web/app/assets/javascripts/react-components/helpers/MixerHelper.js.coffee b/web/app/assets/javascripts/react-components/helpers/MixerHelper.js.coffee index ddd8153a8..5e6fe8618 100644 --- a/web/app/assets/javascripts/react-components/helpers/MixerHelper.js.coffee +++ b/web/app/assets/javascripts/react-components/helpers/MixerHelper.js.coffee @@ -359,6 +359,10 @@ MIX_MODES = context.JK.MIX_MODES; else logger.debug("MixerHelper: full jamtrack is active") + if jamTrackMixers.length == 1 + logger.warn("ignoring wrong amount of mixers for JamTrack in Full Track mode") + return _jamTracks + for jamTrack in jamTracks mixer = null preMasteredClass = "" diff --git a/web/app/assets/javascripts/react-components/stores/JamTrackStore.js.coffee b/web/app/assets/javascripts/react-components/stores/JamTrackStore.js.coffee index 1daf1beec..2e97dd4d2 100644 --- a/web/app/assets/javascripts/react-components/stores/JamTrackStore.js.coffee +++ b/web/app/assets/javascripts/react-components/stores/JamTrackStore.js.coffee @@ -501,6 +501,8 @@ JamTrackActions = @JamTrackActions downloadFailureCallback: (errorMsg) -> + logger.debug("mixdown download failed", errorMsg); + if @jamTrack?.activeMixdown? @jamTrack.activeMixdown.client_state = 'download_fail' @reportError(@jamTrack.activeMixdown) From 49dc6890fc922aacd2313a484292196e676c780e Mon Sep 17 00:00:00 2001 From: Seth Call Date: Wed, 23 Sep 2015 14:39:34 -0500 Subject: [PATCH 15/25] * VRFS-3579 - dont try to kick off 2 downloads --- .../react-components/stores/JamTrackStore.js.coffee | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/web/app/assets/javascripts/react-components/stores/JamTrackStore.js.coffee b/web/app/assets/javascripts/react-components/stores/JamTrackStore.js.coffee index 2e97dd4d2..9bea103ce 100644 --- a/web/app/assets/javascripts/react-components/stores/JamTrackStore.js.coffee +++ b/web/app/assets/javascripts/react-components/stores/JamTrackStore.js.coffee @@ -177,7 +177,9 @@ JamTrackActions = @JamTrackActions , null, true) when 'unknown' - if @jamTrack.activeMixdown.client_state != 'downloading' + # we need to check if @keyCheckTimeout exists; because if it does, we don't want to download while keying. + # 'unknown' is tricky here because the file probably is actually on disk, but the bridge API can say unknown until you've tried to key at least once + if @jamTrack.activeMixdown.client_state != 'downloading' && !@keyCheckTimeout? @jamTrack.activeMixdown.client_state = 'downloading' logger.debug("JamTrackStore: initiating download of mixdown") context.jamClient.JamTrackDownload(@jamTrack.id, @jamTrack.activeMixdown.id, context.JK.currentUserId, From a5e4c2dfce0b2bbc231d06fda3568e5aa1e867fc Mon Sep 17 00:00:00 2001 From: Seth Call Date: Wed, 23 Sep 2015 15:27:41 -0500 Subject: [PATCH 16/25] * VRFS-3519 - prevent play click when JamTrack is loading --- web/app/assets/javascripts/playbackControls.js | 11 +++++++++++ .../react-components/MediaControls.js.jsx.coffee | 5 +++++ .../PopupMediaControls.js.jsx.coffee | 13 +++++++++---- 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/web/app/assets/javascripts/playbackControls.js b/web/app/assets/javascripts/playbackControls.js index 882c847c5..ec36d0a03 100644 --- a/web/app/assets/javascripts/playbackControls.js +++ b/web/app/assets/javascripts/playbackControls.js @@ -39,6 +39,7 @@ var $self = $(this); + var disabled = false; var playbackPlaying = false; var playbackDurationMs = 0; var playbackPositionMs = 0; @@ -173,6 +174,11 @@ } $playButton.on('click', function (e) { + + if (disabled) { + logger.debug("PlaybackControls are disabled; ignoring start button") + return; + } startPlay(); return false; }); @@ -484,6 +490,10 @@ playbackPlaying = false; } + function setDisabled(_disabled) { + disabled = _disabled; + } + this.update = update; this.setPlaybackMode = setPlaybackMode; this.startMonitor = startMonitor; @@ -492,6 +502,7 @@ this.onPlayStopEvent = onPlayStopEvent; this.onPlayStartEvent = onPlayStartEvent; this.onPlayPauseEvent = onPlayPauseEvent; + this.setDisabled = setDisabled; return this; } diff --git a/web/app/assets/javascripts/react-components/MediaControls.js.jsx.coffee b/web/app/assets/javascripts/react-components/MediaControls.js.jsx.coffee index ae20ed587..ed241cf9e 100644 --- a/web/app/assets/javascripts/react-components/MediaControls.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/MediaControls.js.jsx.coffee @@ -206,6 +206,7 @@ mixins.push(Reflux.listenTo(JamTrackStore, 'onJamTrackStateChanged')) $root = jQuery(this.getDOMNode()) controls = context.JK.PlaybackControls($root, {mediaActions: MediaPlaybackActions}) + controls.setDisabled(@props.disabled) mediaSummary = MixerStore.mixers.mediaSummary metro = MixerStore.mixers.metro @@ -216,4 +217,8 @@ mixins.push(Reflux.listenTo(JamTrackStore, 'onJamTrackStateChanged')) @tryPrepareMetronome(metro) @setState({mediaSummary: mediaSummary, controls: controls, metro: metro, jamTrackState: jamTrackState}) + + componentWillUpdate: (nextProps) -> + + @state.controls.setDisabled(nextProps.disabled) if @state.controls? }) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/PopupMediaControls.js.jsx.coffee b/web/app/assets/javascripts/react-components/PopupMediaControls.js.jsx.coffee index 0d587d39e..ff744ab65 100644 --- a/web/app/assets/javascripts/react-components/PopupMediaControls.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/PopupMediaControls.js.jsx.coffee @@ -386,7 +386,7 @@ mixins.push(Reflux.listenTo(JamTrackStore, 'onJamTrackChanged')) `
{header} - + {extraControls} {closeLinkText}
` @@ -588,13 +588,12 @@ mixins.push(Reflux.listenTo(JamTrackStore, 'onJamTrackChanged')) window.resizeTo(width, height + offset) - componentWillUpdate: (nextProps, nextState) -> - + computeDisableLoading: (state) -> @disableLoading = false return unless nextState? - selectedMixdown = nextState?.jamTrackState?.jamTrack?.activeMixdown + selectedMixdown = state?.jamTrackState?.jamTrack?.activeMixdown mixdownDownloading = false if selectedMixdown? @@ -607,5 +606,11 @@ mixins.push(Reflux.listenTo(JamTrackStore, 'onJamTrackChanged')) @disableLoading = SessionStore.downloadingJamTrack || mixdownDownloading + componentWillMount: () -> + @computeDisableLoading(@state) + + componentWillUpdate: (nextProps, nextState) -> + @computeDisableLoading(nextState) + }) \ No newline at end of file From 5018be71ddd5be9b7a5fe380e8ffadeb332f6f91 Mon Sep 17 00:00:00 2001 From: Seth Call Date: Wed, 23 Sep 2015 18:30:38 -0500 Subject: [PATCH 17/25] * fix sample rate issue typo in keys.rabl --- web/app/views/api_jam_tracks/keys.rabl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/app/views/api_jam_tracks/keys.rabl b/web/app/views/api_jam_tracks/keys.rabl index 2b482e44a..3f99828f0 100644 --- a/web/app/views/api_jam_tracks/keys.rabl +++ b/web/app/views/api_jam_tracks/keys.rabl @@ -21,7 +21,7 @@ node do |jam_track| # now include mixdown info mixdowns_44 = [] - mixdown_info = @jamtrack_mixdowns[id] + mixdown_info = @jamtrack_mixdowns[id + '-44'] if mixdown_info mixdown_info.each do |mixdown_id| mixdowns_44 << { From 3fdf835942ed18f4cbf62af536923de19711a761 Mon Sep 17 00:00:00 2001 From: Seth Call Date: Fri, 25 Sep 2015 08:36:25 -0500 Subject: [PATCH 18/25] * hide custom mix controls when custom mix is active --- .../PopupMediaControls.js.jsx.coffee | 154 +++++++++--------- .../minimal/media_controls.css.scss | 4 + 2 files changed, 85 insertions(+), 73 deletions(-) diff --git a/web/app/assets/javascripts/react-components/PopupMediaControls.js.jsx.coffee b/web/app/assets/javascripts/react-components/PopupMediaControls.js.jsx.coffee index ff744ab65..59a068003 100644 --- a/web/app/assets/javascripts/react-components/PopupMediaControls.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/PopupMediaControls.js.jsx.coffee @@ -243,80 +243,88 @@ mixins.push(Reflux.listenTo(JamTrackStore, 'onJamTrackChanged')) 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 = ` -
-

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.

-
- - -
-
- - -
-
- - -
-
- CREATE MIX - {errorHtml} -
-
-
` + if !selectedMixdown? + mixControls = ` +
+

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.

+
+ + +
+
+ + +
+
+ + +
+
+ CREATE MIX + {errorHtml} +
+
+ +
` + else + + mixControls = + `
+

To create a custom mix, you must open the Full JamTrack in the My Mixes section above.

+
` if @state.showMyMixes showMyMixesText = `hide my mixes
` diff --git a/web/app/assets/stylesheets/minimal/media_controls.css.scss b/web/app/assets/stylesheets/minimal/media_controls.css.scss index b8079dd95..8545aea86 100644 --- a/web/app/assets/stylesheets/minimal/media_controls.css.scss +++ b/web/app/assets/stylesheets/minimal/media_controls.css.scss @@ -183,6 +183,10 @@ body.media-controls-popup.popup { border-width:1px 0; padding: 7px 0 20px; + &.not-active { + padding:7px 0 7px; + } + p { line-height:125%; color:$ColorTextTypical; From aaa450f14e6dd4e53cb0216c746687dc13244fa5 Mon Sep 17 00:00:00 2001 From: Seth Call Date: Fri, 25 Sep 2015 10:53:53 -0500 Subject: [PATCH 19/25] * add help article link to popup for JamTracks --- .../PopupMediaControls.js.jsx.coffee | 14 +++++++++++++- .../react-components/actions/AppActions.js.coffee | 1 + .../react-components/stores/AppStore.js.coffee | 4 ++++ .../stylesheets/minimal/media_controls.css.scss | 13 ++++++++++++- 4 files changed, 30 insertions(+), 2 deletions(-) diff --git a/web/app/assets/javascripts/react-components/PopupMediaControls.js.jsx.coffee b/web/app/assets/javascripts/react-components/PopupMediaControls.js.jsx.coffee index 59a068003..c92653bd1 100644 --- a/web/app/assets/javascripts/react-components/PopupMediaControls.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/PopupMediaControls.js.jsx.coffee @@ -16,6 +16,7 @@ if window.opener? if accessOpener + AppActions = window.opener.AppActions SessionActions = window.opener.SessionActions MixerActions = window.opener.MixerActions MixerStore = window.opener.MixerStore @@ -95,6 +96,10 @@ mixins.push(Reflux.listenTo(JamTrackStore, 'onJamTrackChanged')) close: () -> window.close() + help: (e) -> + e.preventDefault() + + AppActions.openExternalUrl($(e.target).attr('href')) render: () -> @@ -115,6 +120,7 @@ mixins.push(Reflux.listenTo(JamTrackStore, 'onJamTrackChanged')) mediaType = "JamTrack" mediaName = jamTrack.name closeLinkText = 'CLOSE JAMTRACK' + helpLink = 'https://jamkazam.desk.com/customer/portal/articles/2138903-using-custom-mixes-to-slow-tempo-change-pitch' selectedMixdown = jamTrack.activeMixdown @@ -392,11 +398,17 @@ mixins.push(Reflux.listenTo(JamTrackStore, 'onJamTrackChanged')) else mediaType = "" + if helpLink? + helpButton = `HELP` + `
{header} {extraControls} - {closeLinkText} +
+ {helpButton} + {closeLinkText} +
` windowUnloaded: () -> diff --git a/web/app/assets/javascripts/react-components/actions/AppActions.js.coffee b/web/app/assets/javascripts/react-components/actions/AppActions.js.coffee index 6c054e3ec..dbe55b7e4 100644 --- a/web/app/assets/javascripts/react-components/actions/AppActions.js.coffee +++ b/web/app/assets/javascripts/react-components/actions/AppActions.js.coffee @@ -2,4 +2,5 @@ context = window @AppActions = Reflux.createActions({ appInit: {} + openExternalUrl: {} }) diff --git a/web/app/assets/javascripts/react-components/stores/AppStore.js.coffee b/web/app/assets/javascripts/react-components/stores/AppStore.js.coffee index bb5a11d98..887a38326 100644 --- a/web/app/assets/javascripts/react-components/stores/AppStore.js.coffee +++ b/web/app/assets/javascripts/react-components/stores/AppStore.js.coffee @@ -8,5 +8,9 @@ logger = context.JK.logger onAppInit: (app) -> @trigger(app) + + onOpenExternalUrl: (href) -> + + context.JK.popExternalLink(href) } ) diff --git a/web/app/assets/stylesheets/minimal/media_controls.css.scss b/web/app/assets/stylesheets/minimal/media_controls.css.scss index 8545aea86..72d065b77 100644 --- a/web/app/assets/stylesheets/minimal/media_controls.css.scss +++ b/web/app/assets/stylesheets/minimal/media_controls.css.scss @@ -33,10 +33,21 @@ body.media-controls-popup.popup { margin-bottom:5px; } - .close-link { + .actions { + position:relative; margin-top:20px; font-size:11px; margin-bottom:10px; + + } + + .help-link { + position:absolute; + left:8px; + top:0; + } + .close-link { + } .display-metronome { From 8e67d6d9b846f04399b205ad906a70826e978447 Mon Sep 17 00:00:00 2001 From: Seth Call Date: Fri, 25 Sep 2015 13:28:48 -0500 Subject: [PATCH 20/25] * fix help alignment --- web/app/assets/stylesheets/minimal/media_controls.css.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/app/assets/stylesheets/minimal/media_controls.css.scss b/web/app/assets/stylesheets/minimal/media_controls.css.scss index 72d065b77..addf80c3d 100644 --- a/web/app/assets/stylesheets/minimal/media_controls.css.scss +++ b/web/app/assets/stylesheets/minimal/media_controls.css.scss @@ -43,7 +43,7 @@ body.media-controls-popup.popup { .help-link { position:absolute; - left:8px; + left:-6px; top:0; } .close-link { From c0d78c9d26505033f5bdf986e2103febd2887bcf Mon Sep 17 00:00:00 2001 From: Seth Call Date: Sat, 26 Sep 2015 14:01:51 -0500 Subject: [PATCH 21/25] * VRFS-3586 up 40 from 20 for the jamtrack account screen --- web/app/assets/javascripts/accounts_jamtracks.js.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/app/assets/javascripts/accounts_jamtracks.js.coffee b/web/app/assets/javascripts/accounts_jamtracks.js.coffee index 08785bc50..5793d7941 100644 --- a/web/app/assets/javascripts/accounts_jamtracks.js.coffee +++ b/web/app/assets/javascripts/accounts_jamtracks.js.coffee @@ -19,7 +19,7 @@ context.JK.AccountJamTracks = class AccountJamTracks @screen = $('#account-jamtracks') beforeShow:() => - rest.getPurchasedJamTracks({}) + rest.getPurchasedJamTracks({limit: 40}) .done(@populateJamTracks) .fail(@app.ajaxError); From 38aa35765de1ce3d92ae1048f99975a72b6e75b0 Mon Sep 17 00:00:00 2001 From: Seth Call Date: Sat, 26 Sep 2015 14:37:05 -0500 Subject: [PATCH 22/25] * allow someone to make a replica of the master mix if the boolean 'full' is passed with true --- ruby/lib/jam_ruby/models/jam_track_mixdown.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ruby/lib/jam_ruby/models/jam_track_mixdown.rb b/ruby/lib/jam_ruby/models/jam_track_mixdown.rb index 59527b659..71a2ca0a1 100644 --- a/ruby/lib/jam_ruby/models/jam_track_mixdown.rb +++ b/ruby/lib/jam_ruby/models/jam_track_mixdown.rb @@ -86,7 +86,7 @@ module JamRuby if all_quiet errors.add(:settings, 'are all muted') end - if !tweaked + if !tweaked && !parsed['full'] errors.add(:settings, 'have nothing specified') end From cb76a6e28fa6d0af500124d381dcb5c80b6e5c48 Mon Sep 17 00:00:00 2001 From: Seth Call Date: Tue, 29 Sep 2015 19:57:31 -0500 Subject: [PATCH 23/25] * VRFS-3588 generating AAC's for master previews --- db/manifest | 3 +- db/up/aac_master.sql | 3 + ruby/lib/jam_ruby/jam_track_importer.rb | 238 ++++++++++++++++-- ruby/lib/jam_ruby/lib/s3_manager.rb | 4 + ruby/lib/jam_ruby/models/jam_track_track.rb | 19 +- web/app/views/api_jam_tracks/show.rabl | 3 +- .../views/api_jam_tracks/show_for_client.rabl | 10 +- web/lib/tasks/jam_tracks.rake | 9 + 8 files changed, 269 insertions(+), 20 deletions(-) create mode 100644 db/up/aac_master.sql diff --git a/db/manifest b/db/manifest index 9b209f892..4a3b6674b 100755 --- a/db/manifest +++ b/db/manifest @@ -303,4 +303,5 @@ jam_track_name_drop_unique.sql jam_track_searchability.sql harry_fox_agency.sql jam_track_slug.sql -mixdown.sql \ No newline at end of file +mixdown.sql +aac_master.sql \ No newline at end of file diff --git a/db/up/aac_master.sql b/db/up/aac_master.sql new file mode 100644 index 000000000..28f67f81d --- /dev/null +++ b/db/up/aac_master.sql @@ -0,0 +1,3 @@ +ALTER TABLE jam_track_tracks ADD COLUMN preview_aac_url VARCHAR; +ALTER TABLE jam_track_tracks ADD COLUMN preview_aac_md5 VARCHAR; +ALTER TABLE jam_track_tracks ADD COLUMN preview_aac_length bigint; \ No newline at end of file diff --git a/ruby/lib/jam_ruby/jam_track_importer.rb b/ruby/lib/jam_ruby/jam_track_importer.rb index c6ef0b135..36e3772a3 100644 --- a/ruby/lib/jam_ruby/jam_track_importer.rb +++ b/ruby/lib/jam_ruby/jam_track_importer.rb @@ -34,6 +34,70 @@ module JamRuby end + def synchronize_preview_dev(jam_track) + jam_track.jam_track_tracks.each do |track| + next if track.track_type != 'Master' + + + most_recent_aac = nil + most_recent_ogg = nil + most_recent_mp3 = nil + public_jamkazam_s3_manager.list_files(track.preview_directory).each do |s3_preview_item| + + s3_object = public_jamkazam_s3_manager.object(s3_preview_item) + + if s3_preview_item.end_with?('.aac') + if most_recent_aac + if s3_object.last_modified > most_recent_aac.last_modified + most_recent_aac = s3_object + end + else + most_recent_aac = s3_object + end + end + + if s3_preview_item.end_with?('.mp3') + if most_recent_mp3 + if s3_object.last_modified > most_recent_mp3.last_modified + most_recent_mp3 = s3_object + end + else + most_recent_mp3 = s3_object + end + end + + if s3_preview_item.end_with?('.ogg') + if most_recent_ogg + if s3_object.last_modified > most_recent_ogg.last_modified + most_recent_ogg = s3_object + end + else + most_recent_ogg = s3_object + end + end + end + + if most_recent_aac + track['preview_aac_md5'] = 'md5' + track['preview_aac_url'] = most_recent_aac.key + track['preview_aac_length'] = most_recent_aac.content_length + end + + if most_recent_mp3 + track['preview_mp3_md5'] = 'md5' + track['preview_mp3_url'] = most_recent_mp3.key + track['preview_mp3_length'] = most_recent_mp3.content_length + end + + if most_recent_ogg + track['preview_md5'] = 'md5' + track['preview_url'] = most_recent_ogg.key + track['preview_length'] = most_recent_ogg.content_length + end + + track.save + end + end # this method was created due to Tency-sourced data having no master track # it goes through all audio tracks, and creates a master mix from it. (mix + normalize) def create_master(metadata, metalocation) @@ -116,7 +180,6 @@ module JamRuby end - temp_file = File.join(tmp_dir, "temp.wav") output_filename = JamTrackImporter.remove_s3_special_chars("#{self.name} Master Mix.wav") output_file = File.join(tmp_dir, output_filename) @@ -149,7 +212,7 @@ module JamRuby # now we need to upload the output back up s3_target = audio_path + '/' + output_filename @@log.debug("uploading #{output_file} to #{s3_target}") - JamTrackImporter.song_storage_manager.upload(s3_target, output_file ) + JamTrackImporter.song_storage_manager.upload(s3_target, output_file) finish('success', nil) end @@ -195,7 +258,7 @@ module JamRuby meta[:licensor] = vendor - File.open(meta_yml, 'w') {|f| f.write meta.to_yaml } + File.open(meta_yml, 'w') { |f| f.write meta.to_yaml } jamkazam_s3_manager.upload(metalocation, meta_yml) end @@ -338,7 +401,7 @@ module JamRuby genres << Genre.find('asian') else found = Genre.find_by_id(genre) - genres << found if found + genres << found if found end end @@ -1166,7 +1229,7 @@ module JamRuby total_time = `#{total_time_command}`.to_f result_code = -20 - stripped_time = total_time # default to the case where we just start the preview at the beginning + stripped_time = total_time # default to the case where we just start the preview at the beginning burp_gaps.each do |gap| command_strip_lead_silence = "sox \"#{ogg_44100}\" \"#{out_wav}\" silence 1 #{gap} 1%" @@ -1218,6 +1281,53 @@ module JamRuby end + def synchronize_aac_preview(track, tmp_dir, ogg_44100, ogg_digest) + begin + aac_44100 = File.join(tmp_dir, 'output-preview-44100.aac') + convert_aac_cmd = "#{APP_CONFIG.ffmpeg_path} -i \"#{ogg_44100}\" -c:a libfdk_aac -b:a 192k \"#{aac_44100}\"" + @@log.debug("converting to aac using: " + convert_aac_cmd) + + convert_output = `#{convert_aac_cmd}` + + aac_digest = ::Digest::MD5.file(aac_44100) + + track["preview_aac_md5"] = aac_md5 = aac_digest.hexdigest + + # upload 44100 aac to public location + @@log.debug("uploading aac preview to #{track.preview_filename('aac')}") + public_jamkazam_s3_manager.upload(track.preview_filename(aac_digest.hexdigest, 'aac'), aac_44100, content_type: 'audio/aac', content_md5: aac_digest.base64digest) + + + track.skip_uploader = true + + original_aac_preview_url = track["preview_aac_url"] + + # and finally update the JamTrackTrack with the new info + track["preview_aac_url"] = track.preview_filename(aac_md5, 'aac') + track["preview_aac_length"] = File.new(aac_44100).size + track["preview_start_time"] = 0 + + if !track.save + finish("save_master_preview", track.errors.to_s) + return false + end + + # if all that worked, now delete old previews, if present + begin + public_jamkazam_s3_manager.delete(original_aac_preview_url) if original_aac_preview_url && original_aac_preview_url != track["preview_aac_url"] + rescue + puts "UNABLE TO CLEANUP OLD PREVIEW URL" + end + rescue Exception => e + finish("sync_master_preview_exception", e.to_s) + return false + end + + + return true + + end + def synchronize_master_preview(track, tmp_dir, ogg_44100, ogg_digest) begin @@ -1229,20 +1339,33 @@ module JamRuby mp3_digest = ::Digest::MD5.file(mp3_44100) + aac_44100 = File.join(tmp_dir, 'output-preview-44100.aac') + convert_aac_cmd = "#{APP_CONFIG.ffmpeg_path} -i \"#{ogg_44100}\" -c:a libfdk_aac -b:a 192k \"#{aac_44100}\"" + @@log.debug("converting to aac using: " + convert_aac_cmd) + + convert_output = `#{convert_aac_cmd}` + + aac_digest = ::Digest::MD5.file(aac_44100) + + track["preview_md5"] = ogg_md5 = ogg_digest.hexdigest track["preview_mp3_md5"] = mp3_md5 = mp3_digest.hexdigest + track["preview_aac_md5"] = aac_md5 = aac_digest.hexdigest - # upload 44100 ogg and mp3 to public location as well + # upload 44100 ogg, mp3, aac to public location as well @@log.debug("uploading ogg preview to #{track.preview_filename('ogg')}") public_jamkazam_s3_manager.upload(track.preview_filename(ogg_digest.hexdigest, 'ogg'), ogg_44100, content_type: 'audio/ogg', content_md5: ogg_digest.base64digest) @@log.debug("uploading mp3 preview to #{track.preview_filename('mp3')}") public_jamkazam_s3_manager.upload(track.preview_filename(mp3_digest.hexdigest, 'mp3'), mp3_44100, content_type: 'audio/mpeg', content_md5: mp3_digest.base64digest) + @@log.debug("uploading aac preview to #{track.preview_filename('aac')}") + public_jamkazam_s3_manager.upload(track.preview_filename(aac_digest.hexdigest, 'aac'), aac_44100, content_type: 'audio/aac', content_md5: aac_digest.base64digest) track.skip_uploader = true original_ogg_preview_url = track["preview_url"] original_mp3_preview_url = track["preview_mp3_url"] + original_aac_preview_url = track["preview_aac_url"] # and finally update the JamTrackTrack with the new info track["preview_url"] = track.preview_filename(ogg_md5, 'ogg') @@ -1250,6 +1373,8 @@ module JamRuby # and finally update the JamTrackTrack with the new info track["preview_mp3_url"] = track.preview_filename(mp3_md5, 'mp3') track["preview_mp3_length"] = File.new(mp3_44100).size + track["preview_aac_url"] = track.preview_filename(aac_md5, 'mp3') + track["preview_aac_length"] = File.new(aac_44100).size track["preview_start_time"] = 0 if !track.save @@ -1261,6 +1386,7 @@ module JamRuby begin public_jamkazam_s3_manager.delete(original_ogg_preview_url) if original_ogg_preview_url && original_ogg_preview_url != track["preview_url"] public_jamkazam_s3_manager.delete(original_mp3_preview_url) if original_mp3_preview_url && original_mp3_preview_url != track["preview_mp3_url"] + public_jamkazam_s3_manager.delete(original_aac_preview_url) if original_aac_preview_url && original_aac_preview_url != track["preview_aac_url"] rescue puts "UNABLE TO CLEANUP OLD PREVIEW URL" end @@ -1499,13 +1625,13 @@ module JamRuby CSV.open("only_in_s3.csv", "wb") do |csv| only_in_s3.each do |song_id| - csv << [ song_id, in_s3[song_id][:artist], in_s3[song_id][:song] ] + csv << [song_id, in_s3[song_id][:artist], in_s3[song_id][:song]] end end CSV.open("only_in_2k_selection.csv", "wb") do |csv| only_in_mapping.each do |song_id| - csv << [ song_id, in_mapping[song_id][:artist], in_mapping[song_id][:song] ] + csv << [song_id, in_mapping[song_id][:artist], in_mapping[song_id][:song]] end end @@ -1518,6 +1644,7 @@ module JamRuby break end end + def create_masters iterate_song_storage do |metadata, metalocation| next if metadata.nil? @@ -1584,6 +1711,38 @@ module JamRuby importer end + # hunts for the most recent .aac, .mp3, or .ogg file + def synchronize_preview_dev(jam_track) + importer = JamTrackImporter.new + importer.name = jam_track.name + + importer.synchronize_preview_dev(jam_track) + + importer.finish('success', nil) + importer + end + + def synchronize_jamtrack_aac_preview(jam_track) + importer = JamTrackImporter.new + importer.name = jam_track.name + + track = jam_track.master_track + + if track + Dir.mktmpdir do |tmp_dir| + ogg_44100 = File.join(tmp_dir, 'input.ogg') + private_s3_manager.download(track.url_by_sample_rate(44), ogg_44100) + ogg_44100_digest = ::Digest::MD5.file(ogg_44100) + if importer.synchronize_aac_preview(track, tmp_dir, ogg_44100, ogg_44100_digest) + importer.finish("success", nil) + end + end + else + importer.finish('no_master_track', nil) + end + importer + end + def synchronize_jamtrack_master_preview(jam_track) importer = JamTrackImporter.new importer.name = jam_track.name @@ -1606,6 +1765,30 @@ module JamRuby importer end + def synchronize_previews_dev + importers = [] + + JamTrack.all.each do |jam_track| + importers << synchronize_preview_dev(jam_track) + end + + @@log.info("SUMMARY") + @@log.info("-------") + importers.each do |importer| + if importer + if importer.reason == "success" || importer.reason == "no_preview_start_time" + @@log.info("#{importer.name} #{importer.reason}") + else + @@log.error("#{importer.name} failed to import.") + @@log.error("#{importer.name} reason=#{importer.reason}") + @@log.error("#{importer.name} detail=#{importer.detail}") + end + else + @@log.error("NULL IMPORTER") + end + + end + end def synchronize_previews importers = [] @@ -1632,6 +1815,33 @@ module JamRuby end end + def synchronize_jamtrack_aac_previews + + importers = [] + + JamTrack.all.each do |jam_track| + importers << synchronize_jamtrack_aac_preview(jam_track) + end + + @@log.info("SUMMARY") + @@log.info("-------") + importers.each do |importer| + if importer + if importer.reason == "success" || importer.reason == "jam_track_exists" || importer.reason == "other_processing" + @@log.info("#{importer.name} #{importer.reason}") + else + @@log.error("#{importer.name} failed to import.") + @@log.error("#{importer.name} reason=#{importer.reason}") + @@log.error("#{importer.name} detail=#{importer.detail}") + end + else + @@log.error("NULL IMPORTER") + end + + + end + end + def synchronize_jamtrack_master_previews importers = [] @@ -1761,7 +1971,7 @@ module JamRuby end end - def synchronize_all(options) + def synchronize_all(options) importers = [] count = 0 @@ -1883,11 +2093,11 @@ module JamRuby genre4 = value[:genre4] genre5 = value[:genre5] - genres << genre1.downcase.strip if genre1 - genres << genre2.downcase.strip if genre2 - genres << genre3.downcase.strip if genre3 - genres << genre4.downcase.strip if genre4 - genres << genre5.downcase.strip if genre5 + genres << genre1.downcase.strip if genre1 + genres << genre2.downcase.strip if genre2 + genres << genre3.downcase.strip if genre3 + genres << genre4.downcase.strip if genre4 + genres << genre5.downcase.strip if genre5 value[:genres] = genres end diff --git a/ruby/lib/jam_ruby/lib/s3_manager.rb b/ruby/lib/jam_ruby/lib/s3_manager.rb index cf86fdc9b..3398a6a3c 100644 --- a/ruby/lib/jam_ruby/lib/s3_manager.rb +++ b/ruby/lib/jam_ruby/lib/s3_manager.rb @@ -121,6 +121,10 @@ module JamRuby s3_bucket.objects[filename].exists? end + def object(filename) + s3_bucket.objects[filename] + end + def length(filename) s3_bucket.objects[filename].content_length end diff --git a/ruby/lib/jam_ruby/models/jam_track_track.rb b/ruby/lib/jam_ruby/models/jam_track_track.rb index c0548e6fd..16f00cd21 100644 --- a/ruby/lib/jam_ruby/models/jam_track_track.rb +++ b/ruby/lib/jam_ruby/models/jam_track_track.rb @@ -49,7 +49,11 @@ module JamRuby # md5-'ed because we cache forever def preview_filename(md5, ext='ogg') original_name = "#{File.basename(self["url_44"], ".ogg")}-preview-#{md5}.#{ext}" - "jam_track_previews/#{jam_track.original_artist}/#{jam_track.name}/#{original_name}" + "#{preview_directory}/#{original_name}" + end + + def preview_directory + "jam_track_previews/#{jam_track.original_artist}/#{jam_track.name}" end def has_preview? @@ -58,7 +62,16 @@ module JamRuby # generates a URL that points to a public version of the preview def preview_public_url(media_type='ogg') - url = media_type == 'ogg' ? self[:preview_url] : self[:preview_mp3_url] + case media_type + when 'ogg' + url = self[:preview_url] + when 'mp3' + url = self[:preview_mp3_url] + when 'aac' + url = self[:preview_aac_url] + else + raise "unknown media_type #{media_type}" + end if url s3_public_manager.public_url(url,{ :secure => true}) else @@ -154,6 +167,7 @@ module JamRuby # input is the original ogg file for the track. tmp_dir is where this code can safely generate output stuff and have it cleaned up later def process_preview(input, tmp_dir) + raise "Does not include AAC generation. Must be updated before used." uuid = SecureRandom.uuid output = File.join(tmp_dir, "#{uuid}.ogg") output_mp3 = File.join(tmp_dir, "#{uuid}.mp3") @@ -176,7 +190,6 @@ module JamRuby # now create mp3 off of ogg preview convert_mp3_cmd = "#{APP_CONFIG.ffmpeg_path} -i \"#{output}\" -ab 192k \"#{output_mp3}\"" - @@log.debug("converting to mp3 using: " + convert_mp3_cmd) convert_output = `#{convert_mp3_cmd}` diff --git a/web/app/views/api_jam_tracks/show.rabl b/web/app/views/api_jam_tracks/show.rabl index e05ca0cbf..c54341870 100644 --- a/web/app/views/api_jam_tracks/show.rabl +++ b/web/app/views/api_jam_tracks/show.rabl @@ -20,7 +20,8 @@ child(:jam_track_tracks => :tracks) { node do |track| { preview_mp3_url: track.preview_public_url('mp3'), - preview_ogg_url: track.preview_public_url('ogg') + preview_ogg_url: track.preview_public_url('ogg'), + preview_aac_url: track.preview_public_url('aac') } end } diff --git a/web/app/views/api_jam_tracks/show_for_client.rabl b/web/app/views/api_jam_tracks/show_for_client.rabl index 27f4fb616..d3a6ba77d 100644 --- a/web/app/views/api_jam_tracks/show_for_client.rabl +++ b/web/app/views/api_jam_tracks/show_for_client.rabl @@ -15,7 +15,15 @@ node :jam_track_right_id do |jam_track| end child(:jam_track_tracks => :tracks) { - attributes :id, :part, :instrument, :track_type + attributes :id, :part, :instrument, :track_type, :position + + node do |track| + { + preview_mp3_url: track.preview_public_url('mp3'), + preview_ogg_url: track.preview_public_url('ogg'), + preview_aac_url: track.preview_public_url('aac') + } + end } node :last_mixdown_id do |jam_track| diff --git a/web/lib/tasks/jam_tracks.rake b/web/lib/tasks/jam_tracks.rake index f74a6f775..b1e2161fd 100644 --- a/web/lib/tasks/jam_tracks.rake +++ b/web/lib/tasks/jam_tracks.rake @@ -103,6 +103,15 @@ namespace :jam_tracks do importer = JamTrackImporter.synchronize_jamtrack_master_previews end + task sync_master_aac: :environment do |task, args| + JamTrackImporter.synchronize_jamtrack_aac_previews + end + + # popuplate preview info without uploading/processing audio files (use what's in S3) + task sync_previews_dev: :environment do |task, args| + JamTrackImporter.synchronize_previews_dev + end + # syncs just one master track for a give JamTrack task sync_master_preview: :environment do |task, args| plan_code = ENV['PLAN_CODE'] From 9abee035bed94a520f5843c22741fa792cf2b17b Mon Sep 17 00:00:00 2001 From: Seth Call Date: Wed, 30 Sep 2015 11:21:26 -0500 Subject: [PATCH 24/25] * VRFS-3513 - start/stop video possible from frontentd --- db/manifest | 3 +- db/up/video_recording.sql | 1 + ruby/lib/jam_ruby/models/recording.rb | 3 +- web/app/assets/javascripts/JamServer.js | 1 + .../dialog/recordingFinishedDialog.js | 15 +++ .../PopupMediaControls.js.jsx.coffee | 1 + .../PopupRecordingStartStop.js.jsx.coffee | 115 ++++++++++++++++-- .../SessionScreen.js.jsx.coffee | 1 + .../stores/RecordingStore.js.jsx.coffee | 13 +- web/app/assets/javascripts/recordingModel.js | 7 +- .../dialogs/recordingFinishedDialog.css.scss | 4 + .../stylesheets/minimal/minimal.css.scss | 2 + .../minimal/recording_controls.css.scss | 42 ++++++- .../controllers/api_recordings_controller.rb | 2 +- web/app/views/api_recordings/show.rabl | 2 +- web/app/views/clients/_help.html.slim | 7 ++ web/app/views/clients/index.html.erb | 2 +- .../_recordingFinishedDialog.html.haml | 6 +- 18 files changed, 195 insertions(+), 32 deletions(-) create mode 100644 db/up/video_recording.sql diff --git a/db/manifest b/db/manifest index 4a3b6674b..483fbd128 100755 --- a/db/manifest +++ b/db/manifest @@ -304,4 +304,5 @@ jam_track_searchability.sql harry_fox_agency.sql jam_track_slug.sql mixdown.sql -aac_master.sql \ No newline at end of file +aac_master.sql +video_recording.sql \ No newline at end of file diff --git a/db/up/video_recording.sql b/db/up/video_recording.sql new file mode 100644 index 000000000..44a976d30 --- /dev/null +++ b/db/up/video_recording.sql @@ -0,0 +1 @@ +ALTER TABLE recordings ADD video BOOLEAN NOT NULL DEFAULT FALSE; \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/recording.rb b/ruby/lib/jam_ruby/models/recording.rb index 99f21c09e..1ec43b9aa 100644 --- a/ruby/lib/jam_ruby/models/recording.rb +++ b/ruby/lib/jam_ruby/models/recording.rb @@ -205,7 +205,7 @@ module JamRuby end # Start recording a session. - def self.start(music_session, owner) + def self.start(music_session, owner, record_video: false) recording = nil # Use a transaction and lock to avoid races. music_session.with_lock do @@ -213,6 +213,7 @@ module JamRuby recording.music_session = music_session recording.owner = owner recording.band = music_session.band + recording.video = record_video if recording.save GoogleAnalyticsEvent.report_band_recording(recording.band) diff --git a/web/app/assets/javascripts/JamServer.js b/web/app/assets/javascripts/JamServer.js index 55dd7cc00..fb16ffa71 100644 --- a/web/app/assets/javascripts/JamServer.js +++ b/web/app/assets/javascripts/JamServer.js @@ -80,6 +80,7 @@ function initiateReconnect(activeElementVotes, in_error) { var initialConnect = !!activeElementVotes; + console.log("activeElementVotes", activeElementVotes) freezeInteraction = activeElementVotes && ((activeElementVotes.dialog && activeElementVotes.dialog.freezeInteraction === true) || (activeElementVotes.screen && activeElementVotes.screen.freezeInteraction === true)); if (in_error) { diff --git a/web/app/assets/javascripts/dialog/recordingFinishedDialog.js b/web/app/assets/javascripts/dialog/recordingFinishedDialog.js index cceaaedc2..c9440dd80 100644 --- a/web/app/assets/javascripts/dialog/recordingFinishedDialog.js +++ b/web/app/assets/javascripts/dialog/recordingFinishedDialog.js @@ -13,6 +13,13 @@ // remove all display errors $('#recording-finished-dialog form .error-text').remove() $('#recording-finished-dialog form .error').removeClass("error") + console.log("save video?", recording) + if(recording.video) { + $dialog.find('.save-video').show() + } + else { + $dialog.find('.save-video').hide() + } removeGoogleLoginErrors() } @@ -103,6 +110,14 @@ } function afterHide() { + if(recording && recording.video) { + var name = $('#recording-finished-dialog form input[name=name]').val(); + name = name.replace(/[^A-Za-z0-9\-\ ]/g, ''); + var keep = $('#recording-finished-dialog form input[name=save_video]').is(':checked') + logger.debug("VideoDecision rid:" + recording.id + ", name=" + name + ", keep=" + keep) + context.jamClient.VideoDecision(recording.id, name, keep) + } + recording = null; playbackControls.stopMonitor(); context.jamClient.ClosePreviewRecording(); diff --git a/web/app/assets/javascripts/react-components/PopupMediaControls.js.jsx.coffee b/web/app/assets/javascripts/react-components/PopupMediaControls.js.jsx.coffee index c92653bd1..0136f3f98 100644 --- a/web/app/assets/javascripts/react-components/PopupMediaControls.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/PopupMediaControls.js.jsx.coffee @@ -2,6 +2,7 @@ context = window logger = context.JK.logger ReactCSSTransitionGroup = React.addons.CSSTransitionGroup; + mixins = [] diff --git a/web/app/assets/javascripts/react-components/PopupRecordingStartStop.js.jsx.coffee b/web/app/assets/javascripts/react-components/PopupRecordingStartStop.js.jsx.coffee index d13f69dc6..8c9092b1e 100644 --- a/web/app/assets/javascripts/react-components/PopupRecordingStartStop.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/PopupRecordingStartStop.js.jsx.coffee @@ -1,24 +1,40 @@ context = window +logger = context.JK.logger + +NoVideoRecordActive = 0 +WebCamRecordActive = 1 +ScreenRecordActive = 2 mixins = [] # make sure this is actually us opening the window, not someone else (by checking for MixerStore) +# this check ensures we attempt to listen if this component is created in a popup +reactContext = if window.opener? then window.opener else window + accessOpener = false if window.opener? try m = window.opener.MixerStore accessOpener = true catch e + reactContext = window +MixerStore = reactContext.MixerStore +RecordingStore = reactContext.RecordingStore +VideoStore = reactContext.VideoStore if accessOpener - mixins.push(Reflux.listenTo(window.opener.RecordingStore,"onRecordingStateChanged")) + mixins.push(Reflux.listenTo(RecordingStore,"onRecordingStateChanged")) + # mixins.push(Reflux.listenTo(MixerStore,"onMixersChanged")) @PopupRecordingStartStop = React.createClass({ mixins: mixins + #onMixersChanged: (mixers) -> + # this.setState(chatMixer: mixers.chatMixer) + onRecordingStateChanged: (recordingState) -> this.setState(isRecording: recordingState.isRecording, recordedOnce: this.state.recordedOnce || recordingState.isRecording) @@ -26,13 +42,39 @@ if accessOpener if this.state.isRecording window.opener.RecordingActions.stopRecording() else - window.opener.RecordingActions.startRecording() + recordChat = false + recordVideo = NoVideoRecordActive + + $root = $(this.getDOMNode()) + + if @inputType != 'audio-only' + + if $root.find('#recording-selection').val() == 'video-window' + recordVideo = ScreenRecordActive + else + recordVideo = WebCamRecordActive + + + recordChat = $root.find('#include-chat').is(':checked') + + + # if the video window isn't open, but a video option was selected... + if recordVideo != NoVideoRecordActive && !VideoStore.videoShared + logger.debug("prevent video from opening", VideoStore) + context.JK.prodBubble($root.find('.control'), 'video-window-not-open', {}, {positions:['bottom']}) + return + logger.debug("@inputType, @udiotye", recordChat, recordVideo) + window.opener.RecordingActions.startRecording(recordVideo, recordChat) onNoteShowHide: () -> + + $root = $(this.getDOMNode()) + audioVideoValue = $root.find('input[name="recording-input-type"]').val() + console.log("audio video value", audioVideoValue) this.setState(showNote: !this.state.showNote) getInitialState: () -> - {isRecording: window.ParentIsRecording, showNote: true, recordedOnce: false} + {isRecording: window.ParentIsRecording, showNote: true, recordedOnce: false, chatMixer: MixerStore.mixers?.chatMixer} render: () -> @@ -53,16 +95,33 @@ if accessOpener
` - recordingJSX = `
-
- - -
+ + chatHelp = `[?]` + + recordingJSX = + `
+
+

Recording Type

+
+ + +
+
+
+ + +
+
+
+ +
-
- - -
+ +
+
` @@ -108,6 +167,20 @@ if accessOpener windowUnloaded: () -> window.opener.RecordingActions.recordingControlsClosed() + onChatHelp: (e) -> + e.preventDefault() + + context.JK.prodBubble($(e.target), 'vid-record-chat-input', {}, {positions:['left']}) + trackInputType: (e) -> + $checkedType = $(e.target); + @inputType = $checkedType.val() + logger.debug("updated @inputType",e.target, @inputType) + + trackAudioType: (e) -> + $checkedType = $(e.target); + @audioType = $checkedType.val() + logger.debug("updated @audioType", @inputType) + componentDidMount: () -> $(window).unload(@windowUnloaded) @@ -116,6 +189,21 @@ if accessOpener $recordingType = $root.find('input[type="radio"]') context.JK.checkbox($recordingType) + @inputType = 'audio-only' + @audioType = 'audio-only' + + $root.find('input[name="recording-input-type"]').on('ifChanged', @trackInputType) + $root.find('input[name="recording-input-chat-option"]').on('ifChanged', @trackAudioType) + + $recordingRegion = $root.find('#recording-selection') + #console.log("$recordingou", $recordingRegion) + #context.JK.dropdown($recordingRegion) + + $includeChat = $root.find('#include-chat') + context.JK.checkbox($includeChat) + + + @resizeWindow() # this is necessary due to whatever the client's rendering behavior is. @@ -124,6 +212,9 @@ if accessOpener componentDidUpdate: () -> @resizeWindow() + $root = jQuery(this.getDOMNode()) + $includeChat = $root.find('#include-chat') + resizeWindow: () => $container = $('#minimal-container') width = $container.width() diff --git a/web/app/assets/javascripts/react-components/SessionScreen.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionScreen.js.jsx.coffee index 46288400d..c3e0e0381 100644 --- a/web/app/assets/javascripts/react-components/SessionScreen.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/SessionScreen.js.jsx.coffee @@ -68,6 +68,7 @@ SessionActions = @SessionActions beforeDisconnect: () -> @logger.debug("session beforeDisconnect") + return { freezeInteraction: true }; onAllowLeaveSession: () -> @allowLeave = true diff --git a/web/app/assets/javascripts/react-components/stores/RecordingStore.js.jsx.coffee b/web/app/assets/javascripts/react-components/stores/RecordingStore.js.jsx.coffee index f68e4d091..684013ec9 100644 --- a/web/app/assets/javascripts/react-components/stores/RecordingStore.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/stores/RecordingStore.js.jsx.coffee @@ -23,8 +23,15 @@ logger = context.JK.logger @recordingModel = recordingModel this.trigger({isRecording: @recordingModel.isRecording()}) - onStartRecording: () -> - @recordingModel.startRecording() + onStartRecording: (recordVideo, recordChat) -> + + frameRate = context.jamClient.GetCurrentVideoFrameRate() || 30; + + NoVideoRecordActive = 0 + WebCamRecordActive = 1 + ScreenRecordActive = 2 + logger.debug("onStartRecording: recordVideo: #{recordVideo}, recordChat: #{recordChat} frameRate: #{frameRate}") + @recordingModel.startRecording(recordVideo, recordChat, frameRate) onStopRecording: () -> @recordingModel.stopRecording() @@ -67,7 +74,7 @@ logger = context.JK.logger popupRecordingControls: () -> logger.debug("poupRecordingControls") - @recordingWindow = window.open("/popups/recording-controls", 'Recording', 'scrollbars=yes,toolbar=no,status=no,height=315,width=350') + @recordingWindow = window.open("/popups/recording-controls", 'Recording', 'scrollbars=yes,toolbar=no,status=no,height=315,width=340') @recordingWindow.ParentRecordingStore = context.RecordingStore @recordingWindow.ParentIsRecording = @recordingModel.isRecording() diff --git a/web/app/assets/javascripts/recordingModel.js b/web/app/assets/javascripts/recordingModel.js index aff392fba..b74fb24d9 100644 --- a/web/app/assets/javascripts/recordingModel.js +++ b/web/app/assets/javascripts/recordingModel.js @@ -79,7 +79,7 @@ return context.JK.dkeys(groupedTracks); } - function startRecording() { + function startRecording(recordVideo, recordChat, recordFramerate) { $self.triggerHandler('startingRecording', {}); @@ -88,14 +88,15 @@ context.RecordingActions.startingRecording({isRecording: false}) - currentRecording = rest.startRecording({"music_session_id": sessionId}) + // 0 indicates the NoVideoRecordActive mode; so anything but that means video got recorded + currentRecording = rest.startRecording({"music_session_id": sessionId, record_video: recordVideo != 0}) .done(function(recording) { currentRecordingId = recording.id; currentOrLastRecordingId = recording.id; // ask the backend to start the session. var groupedTracks = groupTracksToClient(recording); - jamClient.StartRecording(recording["id"], groupedTracks, 0, false, 0); + jamClient.StartRecording(recording["id"], groupedTracks, recordVideo, recordChat, recordFramerate); }) .fail(function(jqXHR) { var details = { clientId: app.clientId, reason: 'rest', detail: arguments, isRecording: false } diff --git a/web/app/assets/stylesheets/dialogs/recordingFinishedDialog.css.scss b/web/app/assets/stylesheets/dialogs/recordingFinishedDialog.css.scss index 22c66dd7b..4a41f3d9f 100644 --- a/web/app/assets/stylesheets/dialogs/recordingFinishedDialog.css.scss +++ b/web/app/assets/stylesheets/dialogs/recordingFinishedDialog.css.scss @@ -53,5 +53,9 @@ .signed_in_to_google { color: yellow; } + + .save-video { + margin-top:10px; + } } diff --git a/web/app/assets/stylesheets/minimal/minimal.css.scss b/web/app/assets/stylesheets/minimal/minimal.css.scss index 5ac30bc35..0e3be9153 100644 --- a/web/app/assets/stylesheets/minimal/minimal.css.scss +++ b/web/app/assets/stylesheets/minimal/minimal.css.scss @@ -7,6 +7,8 @@ *= require client/ftue *= require client/help *= require icheck/minimal/minimal +*= require easydropdown +*= require easydropdown_jk *= require_directory . *= require client/metronomePlaybackModeSelect *= require_directory ../client/react-components diff --git a/web/app/assets/stylesheets/minimal/recording_controls.css.scss b/web/app/assets/stylesheets/minimal/recording_controls.css.scss index b2817b8ee..1cbfaec28 100644 --- a/web/app/assets/stylesheets/minimal/recording_controls.css.scss +++ b/web/app/assets/stylesheets/minimal/recording_controls.css.scss @@ -10,12 +10,12 @@ body.recording-start-stop { } .recording-start-stop { - padding-left:44px; + padding-left:30px; } .control-holder { width:100%; - margin: 1em 0; + margin: 10px 0 20px; } .helper { @@ -24,7 +24,20 @@ body.recording-start-stop { vertical-align: middle; } + .audio-settings { + margin-top:40px; + + label { + display:inline; + margin-left:6px; + } + + .icheckbox_minimal { + vertical-align:middle; + } + } .control { + margin-left:20px; width:231px; height:34px; @include border_box_sizing; @@ -38,7 +51,10 @@ body.recording-start-stop { color:#ccc; } - + .chat-help { + text-decoration:none; + outline:0; + } .control img { vertical-align:middle; margin-right:5px; @@ -59,12 +75,16 @@ body.recording-start-stop { .field { height:18px; - &:nth-child(1) { + &:nth-of-type(1) { } - &:nth-child(2) { + &:nth-of-type(2) { margin-top:9px; } + &:nth-of-type(3) { + margin-top: 10px; + padding-left: 22px; + } } .note-show-hide { @@ -77,12 +97,22 @@ body.recording-start-stop { } .important-note { - margin-top:30px; + margin-top:15px; line-height:150%; font-size:12px; width:260px; } + h3 { + font-size:14px; + font-weight:bold; + margin-bottom:6px; + } + + .video-settings { + margin-bottom:20px; + } + a.note-show-hide { margin-top:5px; text-decoration:underline; diff --git a/web/app/controllers/api_recordings_controller.rb b/web/app/controllers/api_recordings_controller.rb index 0767d9053..77358b13c 100644 --- a/web/app/controllers/api_recordings_controller.rb +++ b/web/app/controllers/api_recordings_controller.rb @@ -83,7 +83,7 @@ class ApiRecordingsController < ApiController raise JamPermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR unless music_session.users.exists?(current_user) - @recording = Recording.start(music_session, current_user) + @recording = Recording.start(music_session, current_user, record_video: params[:record_video]) if @recording.errors.any? response.status = :unprocessable_entity diff --git a/web/app/views/api_recordings/show.rabl b/web/app/views/api_recordings/show.rabl index d59732cb3..285d7bbb6 100644 --- a/web/app/views/api_recordings/show.rabl +++ b/web/app/views/api_recordings/show.rabl @@ -1,6 +1,6 @@ object @recording -attributes :id, :band, :created_at, :duration, :comment_count, :like_count, :play_count, :when_will_be_discarded?, :jam_track_id, :jam_track_initiator_id, :music_session_id, :music_session +attributes :id, :band, :created_at, :duration, :comment_count, :like_count, :play_count, :when_will_be_discarded?, :jam_track_id, :jam_track_initiator_id, :music_session_id, :music_session, :video node :fan_access do |recording| recording.non_active_music_session.fan_access diff --git a/web/app/views/clients/_help.html.slim b/web/app/views/clients/_help.html.slim index 6823d0b45..c271db7b3 100644 --- a/web/app/views/clients/_help.html.slim +++ b/web/app/views/clients/_help.html.slim @@ -352,4 +352,11 @@ script type="text/template" id="template-help-ftue-video-disable" script type="text/template" id="template-help-no-change-while-loading" span Certain actions are disabled while a track is being loaded. +script type="text/template" id="template-help-video-window-not-open" + .video-window-not-open + p You've selected to record video, but the video window is not open. + p Click the VIDEO button in the main window and try again. +script type="text/template" id="template-help-vid-record-chat-input" + .vid-record-chat-input + p Any chat inputs in the session will also be included in the video if checked. \ No newline at end of file diff --git a/web/app/views/clients/index.html.erb b/web/app/views/clients/index.html.erb index 950622bda..4eb4f3797 100644 --- a/web/app/views/clients/index.html.erb +++ b/web/app/views/clients/index.html.erb @@ -366,7 +366,7 @@ } JK.app = JK.JamKazam(); - var jamServer = new JK.JamServer(JK.app, function(event_type) {JK.app.activeElementEvent(event_type)}); + var jamServer = new JK.JamServer(JK.app, function(event_type) {return JK.app.activeElementEvent(event_type)}); jamServer.initialize(); var clientInit = new JK.ClientInit(); diff --git a/web/app/views/dialogs/_recordingFinishedDialog.html.haml b/web/app/views/dialogs/_recordingFinishedDialog.html.haml index bbbdfa701..4447f4d8f 100644 --- a/web/app/views/dialogs/_recordingFinishedDialog.html.haml +++ b/web/app/views/dialogs/_recordingFinishedDialog.html.haml @@ -25,12 +25,12 @@ %br/ %textarea#claim-recording-description.w100{:name => "description"} -if (Rails.application.config.video_available=="full") || (current_user && current_user.admin) - .field.left{:purpose => "save_video"} + .save-video.field.left{:purpose => "save_video"} %input{:name => "save_video", :type => "checkbox"}/ %label{:for => "save_video"} Save Video to Computer - .field.left{:purpose => "upload_to_youtube"} + .hidden.field.left{:purpose => "upload_to_youtube"} %span - %input{:name => "upload_to_youtube", :type => "checkbox"}/ + %input{:name => "upload_to_youtube", :type => "checkbox", :checked => "checked"}/ %label{:for => "upload_to_youtube"} Upload Video to YouTube %span = render(:partial => "shared/google_login") From 4290d449ed1f6fdc36fbd321b925c27be15c4178 Mon Sep 17 00:00:00 2001 From: Seth Call Date: Wed, 30 Sep 2015 12:26:24 -0500 Subject: [PATCH 25/25] * default to full avail --- web/config/application.rb | 2 +- web/config/environments/development.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/web/config/application.rb b/web/config/application.rb index febfd4114..5b156a278 100644 --- a/web/config/application.rb +++ b/web/config/application.rb @@ -350,7 +350,7 @@ if defined?(Bundler) config.recurly_tax_estimate_jam_track_plan = 'jamtrack-acdc-backinblack' config.minimal_curtain = false - config.video_available = "none" + config.video_available = "full" config.alerts_api_enabled = true config.gear_check_ignore_high_latency = false diff --git a/web/config/environments/development.rb b/web/config/environments/development.rb index d2143a987..ad49ae6b7 100644 --- a/web/config/environments/development.rb +++ b/web/config/environments/development.rb @@ -92,7 +92,7 @@ SampleApp::Application.configure do config.jam_tracks_available = true config.purchases_enabled = true config.minimal_curtain = true - config.video_available= ENV['VIDEO_AVAILABILITY'] || "none" + config.video_available= ENV['VIDEO_AVAILABILITY'] || "full" config.email_generic_from = 'nobody-dev@jamkazam.com' config.email_alerts_alias = ENV['ALERT_EMAIL'] || 'alerts-dev@jamkazam.com' config.guard_against_fraud = true