diff --git a/admin/app/admin/jam_tracks.rb b/admin/app/admin/jam_tracks.rb index 8e62bbc85..6cfb6c1a3 100644 --- a/admin/app/admin/jam_tracks.rb +++ b/admin/app/admin/jam_tracks.rb @@ -26,6 +26,7 @@ ActiveAdmin.register JamRuby::JamTrack, :as => 'JamTracks' do column :id column :name column :description + column :version column :initial_play_silence column :time_signature column :status diff --git a/admin/app/views/admin/jam_tracks/_form.html.slim b/admin/app/views/admin/jam_tracks/_form.html.slim index 16e53837d..9ec25ffcf 100644 --- a/admin/app/views/admin/jam_tracks/_form.html.slim +++ b/admin/app/views/admin/jam_tracks/_form.html.slim @@ -7,7 +7,9 @@ | JamTrack should only be made available (to end users) if all its sub-component are in place: = f.input :available, as: :boolean = f.input :description, :input_html => { :rows=>5, :maxlength=>1000 } - = f.input :initial_play_silence, :label => 'Initial Play Silence (seconds)' + = f.input :plan_code, :label=>'Recurly Plan Code', :required=>true, :hint => 'Must match plan code in Recurly' + = f.input :version, :label => 'Version', :hint => 'Increment this value whenever you invalidate (update) the definition of this JamTrack' + //= f.input :initial_play_silence, :label => 'Initial Play Silence (seconds)' = f.input :time_signature, collection: JamRuby::JamTrack::TIME_SIGNATURES, include_blank: false = f.input :status, collection: JamRuby::JamTrack::STATUS, include_blank: false = f.input :recording_type, collection: JamRuby::JamTrack::RECORDING_TYPE, include_blank: false @@ -24,8 +26,9 @@ = f.input :reproduction_royalty_amount, :required=>true, :input_html=>{type:'numeric'} = f.input :licensor_royalty_amount, :required=>true, :input_html=>{type:'numeric'} = f.input :pro_royalty_amount, :required=>true, :input_html=>{type:'numeric'} - = f.input :plan_code, :label=>'Recurly Plan Code', :required=>true = f.input :url, :as => :file, :label => 'Audio File' + = f.input :jmep_text, :as => :text, :label => "JMEP Text", :input_html => {:rows => 5 } + = f.input :jmep_json, :as => :text, :label => "JMEP Json", :input_html => {:rows => 5, :readonly=>true }, :hint => 'readonly' = f.semantic_fields_for :jam_track_tracks do |track| = render 'jam_track_track_fields', f: track diff --git a/admin/config/application.rb b/admin/config/application.rb index 684f30d7e..102a03176 100644 --- a/admin/config/application.rb +++ b/admin/config/application.rb @@ -147,5 +147,8 @@ module JamAdmin config.influxdb_hosts = ["localhost"] config.influxdb_port = 8086 config.influxdb_ignored_environments = ENV["INFLUXDB_ENABLED"] == '1' ? ['test', 'cucumber'] : ['test', 'cucumber', 'development'] + + config.jamtracks_dir = ENV['JAMTRACKS_DIR'] || File.expand_path(File.join(File.dirname(__FILE__), "..", "..", "..", "jamtracks")) + config.jmep_dir = ENV['JMEP_DIR'] || File.expand_path(File.join(File.dirname(__FILE__), "..", "..", "..", "jmep")) end end diff --git a/admin/config/initializers/jam_tracks.rb b/admin/config/initializers/jam_tracks.rb new file mode 100644 index 000000000..c88fc6a2c --- /dev/null +++ b/admin/config/initializers/jam_tracks.rb @@ -0,0 +1,25 @@ +class JamRuby::JamTrack + + # add a custom validation + + before_save :jmep_json_generate + validate :jmep_text_validate + + def jmep_text_validate + begin + JmepManager.execute(self.jmep_text) + rescue ArgumentError => err + errors.add(:jmep_text, err.to_s) + end + end + + def jmep_json_generate + begin + self[:jmep_json] = JmepManager.execute(self.jmep_text) + rescue ArgumentError => err + #errors.add(:jmep_text, err.to_s) + end + end + + +end diff --git a/build b/build index 6d408c180..db8ab33a4 100755 --- a/build +++ b/build @@ -66,7 +66,7 @@ DEB_SERVER=http://localhost:9010/apt-`uname -p` GEM_SERVER=http://localhost:9000/gems # if still going, then push all debs up - if [[ "$GIT_BRANCH" == *develop* || "$GIT_BRANCH" == *master* || "$GIT_BRANCH" == *release* ]]; then + if [[ "$GIT_BRANCH" == *develop* || "$GIT_BRANCH" == *master* || "$GIT_BRANCH" == *release* || "$GIT_BRANCH" == *feature* || "$GIT_BRANCH" == *hotfix* ]]; then echo "" echo "PUSHING DB ARTIFACTS" diff --git a/db/manifest b/db/manifest index cf4efdfb2..f5ffa9cc3 100755 --- a/db/manifest +++ b/db/manifest @@ -255,3 +255,6 @@ user_syncs_include_backing_tracks.sql remove_bpm_from_jamtracks.sql add_jam_track_bitrates.sql widen_user_authorization_token.sql +jam_track_version.sql +recorded_jam_track_tracks.sql +jam_track_jmep_data.sql \ No newline at end of file diff --git a/db/up/jam_track_jmep_data.sql b/db/up/jam_track_jmep_data.sql new file mode 100644 index 000000000..46c6e67b5 --- /dev/null +++ b/db/up/jam_track_jmep_data.sql @@ -0,0 +1,2 @@ +ALTER TABLE jam_tracks ADD COLUMN jmep_text VARCHAR; +ALTER TABLE jam_tracks ADD COLUMN jmep_json JSON; \ No newline at end of file diff --git a/db/up/jam_track_version.sql b/db/up/jam_track_version.sql new file mode 100644 index 000000000..7c1efff86 --- /dev/null +++ b/db/up/jam_track_version.sql @@ -0,0 +1 @@ +ALTER TABLE jam_tracks ADD COLUMN version VARCHAR NOT NULL DEFAULT 0; diff --git a/db/up/recorded_jam_track_tracks.sql b/db/up/recorded_jam_track_tracks.sql index 246b76425..08a485643 100644 --- a/db/up/recorded_jam_track_tracks.sql +++ b/db/up/recorded_jam_track_tracks.sql @@ -1,15 +1,15 @@ -ALTER TABLE recordings ADD COLUMN jam_track_id VARCHAR(64) REFERENCES jam_tracks(id); +ALTER TABLE recordings ADD COLUMN jam_track_id BIGINT REFERENCES jam_tracks(id); ALTER TABLE recordings ADD COLUMN jam_track_initiator_id VARCHAR(64) REFERENCES users(id); CREATE TABLE recorded_jam_track_tracks ( id BIGINT PRIMARY KEY, - user_id VARCHAR(64) REFERENCES users(id) ON DELETE CASCADE, - jam_track_track_id VARCHAR(64), - recording_id VARCHAR(64) NOT NULL, + user_id VARCHAR(64) NOT NULL REFERENCES users(id) ON DELETE CASCADE, + jam_track_track_id VARCHAR(64) REFERENCES jam_track_tracks(id) NOT NULL, + recording_id VARCHAR(64) REFERENCES recordings(id) NOT NULL, discard BOOLEAN, timeline JSON, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); -ALTER TABLE recorded_jam_tracks ALTER COLUMN id SET DEFAULT nextval('tracks_next_tracker_seq'); \ No newline at end of file +ALTER TABLE recorded_jam_track_tracks ALTER COLUMN id SET DEFAULT nextval('tracks_next_tracker_seq'); diff --git a/ruby/lib/jam_ruby.rb b/ruby/lib/jam_ruby.rb index e147eadc6..67f81933b 100755 --- a/ruby/lib/jam_ruby.rb +++ b/ruby/lib/jam_ruby.rb @@ -139,6 +139,7 @@ require "jam_ruby/models/recorded_backing_track_observer" require "jam_ruby/models/recorded_track" require "jam_ruby/models/recorded_track_observer" require "jam_ruby/models/recorded_video" +require "jam_ruby/models/recorded_jam_track_track" require "jam_ruby/models/quick_mix" require "jam_ruby/models/quick_mix_observer" require "jam_ruby/models/share_token" @@ -204,6 +205,7 @@ require "jam_ruby/models/user_sync" require "jam_ruby/models/video_source" require "jam_ruby/models/text_message" require "jam_ruby/jam_tracks_manager" +require "jam_ruby/jmep_manager" include Jampb diff --git a/ruby/lib/jam_ruby/jam_tracks_manager.rb b/ruby/lib/jam_ruby/jam_tracks_manager.rb index 0cccbbeb1..fc5dd5df3 100644 --- a/ruby/lib/jam_ruby/jam_tracks_manager.rb +++ b/ruby/lib/jam_ruby/jam_tracks_manager.rb @@ -20,7 +20,6 @@ module JamRuby def save_jam_track_right_jkz(jam_track_right, bitrate=48) jam_track = jam_track_right.jam_track - #py_root = File.expand_path(File.join(File.dirname(__FILE__), "..", "..", "..", "..", "jamtracks")) py_root = APP_CONFIG.jamtracks_dir Dir.mktmpdir do |tmp_dir| jam_file_opts="" @@ -42,10 +41,11 @@ module JamRuby title=jam_track.name output_jkz=File.join(tmp_dir, "#{title.parameterize}.jkz") py_file = File.join(py_root, "jkcreate.py") + version = jam_track.version @@log.info "Executing python source in #{py_file}, outputting to #{tmp_dir} (#{output_jkz})" # From http://stackoverflow.com/questions/690151/getting-output-of-system-calls-in-ruby/5970819#5970819: - cli = "python #{py_file} -D -k #{sku} -p #{tmp_dir}/pkey.pem -s #{tmp_dir}/skey.pem #{jam_file_opts} -o #{output_jkz} -t '#{title}'" + cli = "python #{py_file} -D -k #{sku} -p #{tmp_dir}/pkey.pem -s #{tmp_dir}/skey.pem #{jam_file_opts} -o #{output_jkz} -t '#{title}' -V '#{version}'" Open3.popen3(cli) do |stdin, stdout, stderr, wait_thr| pid = wait_thr.pid exit_status = wait_thr.value diff --git a/ruby/lib/jam_ruby/jmep_manager.rb b/ruby/lib/jam_ruby/jmep_manager.rb new file mode 100644 index 000000000..a4d0215bc --- /dev/null +++ b/ruby/lib/jam_ruby/jmep_manager.rb @@ -0,0 +1,55 @@ +require 'json' +require 'tempfile' +require 'open3' +require 'fileutils' +require 'open-uri' + +module JamRuby + + # Interact with external python tools to create jmep json + class JmepManager + + @@log = Logging.logger[JmepManager] + + class << self + + def execute(jmep_text) + + json = nil + + if jmep_text.blank? + return nil + end + + py_root = APP_CONFIG.jmep_dir + Dir.mktmpdir do |tmp_dir| + + output_json = File.join(tmp_dir, "jmep.json") + input_text = File.join(tmp_dir, "jmep.txt") + + # put JMEP text into input file + File.open(input_text, 'w') { |file| file.write(jmep_text) } + + py_file = File.join(py_root, "jmepgen.py") + @@log.info "Executing python source in #{py_file}, outputting to #{output_json})" + + # From http://stackoverflow.com/questions/690151/getting-output-of-system-calls-in-ruby/5970819#5970819: + cli = "python #{py_file} -i '#{input_text}' -o '#{output_json}'" + 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) + + raise ArgumentError, "#{out} #{err}" if exit_status != 0 + + json = File.read(output_json) + end + end + + json + end + + end + end +end diff --git a/ruby/lib/jam_ruby/models/jam_track.rb b/ruby/lib/jam_ruby/models/jam_track.rb index 557316541..f6513ac66 100644 --- a/ruby/lib/jam_ruby/models/jam_track.rb +++ b/ruby/lib/jam_ruby/models/jam_track.rb @@ -16,7 +16,7 @@ module JamRuby :original_artist, :songwriter, :publisher, :licensor, :licensor_id, :pro, :genre, :genre_id, :sales_region, :price, :reproduction_royalty, :public_performance_royalty, :reproduction_royalty_amount, :licensor_royalty_amount, :pro_royalty_amount, :plan_code, :initial_play_silence, :jam_track_tracks_attributes, - :jam_track_tap_ins_attributes, :available, as: :admin + :jam_track_tap_ins_attributes, :available, :version, :jmep_json, :jmep_text, as: :admin validates :name, presence: true, uniqueness: true, length: {maximum: 200} validates :description, length: {maximum: 1000} @@ -30,6 +30,7 @@ module JamRuby validates :sales_region, inclusion: {in: [nil] + SALES_REGION} validates_format_of :price, with: /^\d+\.*\d{0,2}$/ validates :initial_play_silence, numericality: true, :allow_nil => true + validates :version, presence: true validates :reproduction_royalty, inclusion: {in: [nil, true, false]} validates :public_performance_royalty, inclusion: {in: [nil, true, false]} @@ -50,6 +51,8 @@ module JamRuby has_many :playing_sessions, :class_name => "JamRuby::ActiveMusicSession" + has_many :recordings, :class_name => "JamRuby::Recording" + accepts_nested_attributes_for :jam_track_tracks, allow_destroy: true accepts_nested_attributes_for :jam_track_tap_ins, allow_destroy: true @@ -156,6 +159,7 @@ module JamRuby def sanitize_active_admin self.genre_id = nil if self.genre_id == '' self.licensor_id = nil if self.licensor_id == '' + self.jmep_json = nil if self.jmep_json = '' 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 1ae19ae9e..f7f1f7ddf 100644 --- a/ruby/lib/jam_ruby/models/jam_track_right.rb +++ b/ruby/lib/jam_ruby/models/jam_track_right.rb @@ -143,8 +143,6 @@ module JamRuby state = nil if signed state = 'SIGNED' - elsif error_count > 0 - state = 'ERROR' elsif signing_started_at if Time.now - signing_started_at > APP_CONFIG.signing_job_run_max_time state = 'SIGNING_TIMEOUT' @@ -157,6 +155,8 @@ module JamRuby else state = 'QUEUED' end + elsif error_count > 0 + state = 'ERROR' else state = 'QUIET' # needs to be poked to go build end diff --git a/ruby/lib/jam_ruby/models/jam_track_track.rb b/ruby/lib/jam_ruby/models/jam_track_track.rb index 65dc1816c..c608cb004 100644 --- a/ruby/lib/jam_ruby/models/jam_track_track.rb +++ b/ruby/lib/jam_ruby/models/jam_track_track.rb @@ -23,6 +23,8 @@ module JamRuby belongs_to :instrument, class_name: "JamRuby::Instrument" belongs_to :jam_track, class_name: "JamRuby::JamTrack" + has_many :recorded_jam_track_tracks, :class_name => "JamRuby::RecordedJamTrackTrack", :foreign_key => :jam_track_track_id, :dependent => :destroy + # create storage directory that will house this jam_track, as well as def store_dir "#{jam_track.store_dir}/tracks" diff --git a/ruby/lib/jam_ruby/models/mix.rb b/ruby/lib/jam_ruby/models/mix.rb index 83557f4f7..f441a441f 100644 --- a/ruby/lib/jam_ruby/models/mix.rb +++ b/ruby/lib/jam_ruby/models/mix.rb @@ -141,12 +141,35 @@ module JamRuby recording.recorded_tracks.each do |recorded_track| manifest["files"] << { "filename" => recorded_track.sign_url(one_day), "codec" => "vorbis", "offset" => 0 } - mix_params << { "level" => 100, "balance" => 0 } + mix_params << { "level" => 1.0, "balance" => 0 } end recording.recorded_backing_tracks.each do |recorded_backing_track| manifest["files"] << { "filename" => recorded_backing_track.sign_url(one_day), "codec" => "vorbis", "offset" => 0 } - mix_params << { "level" => 100, "balance" => 0 } + mix_params << { "level" => 1.0, "balance" => 0 } + end + + recording.recorded_jam_track_tracks.each do |recorded_jam_track_track| + manifest["files"] << { "filename" => recorded_jam_track_track.jam_track_track.sign_url(one_day), "codec" => "vorbis", "offset" => 0 } + # let's look for level info from the client + level = 1.0 # default value - means no effect + if recorded_jam_track_track.timeline + + timeline_data = JSON.parse(recorded_jam_track_track.timeline) + + # always take the 1st entry for now + first = timeline_data[0] + + if first["mute"] + # mute equates to no noise + level = 0.0 + else + # otherwise grab the left channel... + level = first["vol_l"] + end + end + + mix_params << { "level" => level, "balance" => 0 } end manifest["timeline"] << { "timestamp" => 0, "mix" => mix_params } diff --git a/ruby/lib/jam_ruby/models/recorded_jam_track_track.rb b/ruby/lib/jam_ruby/models/recorded_jam_track_track.rb index 19907eb1c..f77615beb 100644 --- a/ruby/lib/jam_ruby/models/recorded_jam_track_track.rb +++ b/ruby/lib/jam_ruby/models/recorded_jam_track_track.rb @@ -10,11 +10,11 @@ module JamRuby validates :jam_track_track, presence:true def self.create_from_jam_track_track(jam_track_track, recording) - recorded_backing_track = self.new - recorded_backing_track.recording = recording - recorded_backing_track.jam_track_track = jam_track_track - recorded_backing_track.save - recorded_backing_track + recorded_jam_track_track = self.new + recorded_jam_track_track.recording = recording + recorded_jam_track_track.jam_track_track = jam_track_track + recorded_jam_track_track.save + recorded_jam_track_track end end diff --git a/ruby/lib/jam_ruby/models/recording.rb b/ruby/lib/jam_ruby/models/recording.rb index 69d690962..c4537c94d 100644 --- a/ruby/lib/jam_ruby/models/recording.rb +++ b/ruby/lib/jam_ruby/models/recording.rb @@ -12,6 +12,7 @@ module JamRuby has_many :recorded_tracks, :class_name => "JamRuby::RecordedTrack", :foreign_key => :recording_id, :dependent => :destroy has_many :recorded_videos, :class_name => "JamRuby::RecordedVideo", :foreign_key => :recording_id, :dependent => :destroy has_many :recorded_backing_tracks, :class_name => "JamRuby::RecordedBackingTrack", :foreign_key => :recording_id, :dependent => :destroy + has_many :recorded_jam_track_tracks, :class_name => "JamRuby::RecordedJamTrackTrack", :foreign_key => :recording_id, :dependent => :destroy has_many :comments, :class_name => "JamRuby::RecordingComment", :foreign_key => "recording_id", :dependent => :destroy has_many :likes, :class_name => "JamRuby::RecordingLiker", :foreign_key => "recording_id", :dependent => :destroy has_many :plays, :class_name => "JamRuby::PlayablePlay", :as => :playable, :dependent => :destroy @@ -20,6 +21,8 @@ module JamRuby belongs_to :owner, :class_name => "JamRuby::User", :inverse_of => :owned_recordings, :foreign_key => 'owner_id' belongs_to :band, :class_name => "JamRuby::Band", :inverse_of => :recordings belongs_to :music_session, :class_name => "JamRuby::ActiveMusicSession", :inverse_of => :recordings, foreign_key: :music_session_id + belongs_to :jam_track, :class_name => "JamRuby::JamTrack", :inverse_of => :recordings, :foreign_key => 'jam_track_id' + belongs_to :jam_track_initiator, :class_name => "JamRuby::User", :inverse_of => :initiated_jam_track_recordings, :foreign_key => 'jam_track_initiator_id' accepts_nested_attributes_for :recorded_tracks, :mixes, :claimed_recordings, allow_destroy: true @@ -223,6 +226,14 @@ module JamRuby recording.recorded_backing_tracks << RecordedBackingTrack.create_from_backing_track(backing_track, recording) end end + + if music_session.jam_track + music_session.jam_track.jam_track_tracks.each do |jam_track_track| + recording.recorded_jam_track_tracks << RecordedJamTrackTrack.create_from_jam_track_track(jam_track_track, recording) + end + recording.jam_track = music_session.jam_track + recording.jam_track_initiator = music_session.jam_track_initiator + end end end @@ -556,7 +567,7 @@ module JamRuby :recording_id => recorded_item.recording_id, :client_track_id => recorded_item.client_track_id, :next => recorded_item.id - }) + }) else end @@ -678,6 +689,24 @@ module JamRuby self.save(:validate => false) end + def add_timeline(timeline) + tracks = timeline["tracks"] + + raise JamArgumentError, "tracks must be specified" unless tracks + + jam_tracks = tracks.select {|track| track["type"] == "jam_track"} + jam_tracks.each do |client_jam_track| + recorded_jam_track_track = RecordedJamTrackTrack.find_by_jam_track_track_id(client_jam_track["id"]) + if recorded_jam_track_track + recorded_jam_track_track.timeline = client_jam_track["timeline"].to_json + recorded_jam_track_track.save! + else + @@log.error("unable to find JamTrackTrack with id #{recorded_jam_track_track.id}") + end + end + end + + private def self.validate_user_is_band_member(user, band) unless band.users.exists? user diff --git a/ruby/lib/jam_ruby/models/user.rb b/ruby/lib/jam_ruby/models/user.rb index 3bb4c270a..a469ab69a 100644 --- a/ruby/lib/jam_ruby/models/user.rb +++ b/ruby/lib/jam_ruby/models/user.rb @@ -124,6 +124,10 @@ module JamRuby has_many :recorded_videos, :foreign_key => "user_id", :class_name => "JamRuby::RecordedVideo", :inverse_of => :user has_many :recorded_backing_tracks, :foreign_key => "user_id", :class_name => "JamRuby::RecordedBackingTrack", :inverse_of => :user has_many :quick_mixes, :foreign_key => "user_id", :class_name => "JamRuby::QuickMix", :inverse_of => :user + has_many :recorded_jam_track_tracks, :foreign_key => "user_id", :class_name => "JamRuby::RecordedJamTrackTrack", :inverse_of => :user + + # jam track recordings started + has_many :initiated_jam_track_recordings, :foreign_key => 'jam_track_initiator_id', :class_name => "JamRuby::Recording", :inverse_of => :jam_track_initiator # invited users has_many :invited_users, :foreign_key => "sender_id", :class_name => "JamRuby::InvitedUser" diff --git a/ruby/lib/jam_ruby/recurly_client.rb b/ruby/lib/jam_ruby/recurly_client.rb index eb70824c9..3bf68e991 100644 --- a/ruby/lib/jam_ruby/recurly_client.rb +++ b/ruby/lib/jam_ruby/recurly_client.rb @@ -111,6 +111,7 @@ module JamRuby end def place_order(current_user, jam_track) + jam_track_right = nil account = get_account(current_user) if (account.present?) begin @@ -136,7 +137,7 @@ module JamRuby else raise RecurlyClientError, "Could not find account to place order." end - account + jam_track_right end def find_or_create_account(current_user, billing_info=nil) diff --git a/ruby/spec/factories.rb b/ruby/spec/factories.rb index 8c940bd0b..07008dda7 100644 --- a/ruby/spec/factories.rb +++ b/ruby/spec/factories.rb @@ -268,7 +268,6 @@ FactoryGirl.define do association :recording, factory: :recording end - factory :recorded_video, :class => JamRuby::RecordedVideo do sequence(:client_video_source_id) { |n| "client_video_source_id-#{n}"} fully_uploaded true @@ -277,6 +276,12 @@ FactoryGirl.define do association :recording, factory: :recording end + factory :recorded_jam_track_track, :class => JamRuby::RecordedJamTrackTrack do + association :user, factory: :user + association :recording, factory: :recording + association :jam_track_track, factory: :jam_track_track + end + factory :instrument, :class => JamRuby::Instrument do description { |n| "Instrument #{n}" } end diff --git a/ruby/spec/jam_ruby/models/recording_spec.rb b/ruby/spec/jam_ruby/models/recording_spec.rb index d2fd7c3b0..3d8515f29 100644 --- a/ruby/spec/jam_ruby/models/recording_spec.rb +++ b/ruby/spec/jam_ruby/models/recording_spec.rb @@ -1073,6 +1073,33 @@ describe Recording do RecordedVideo.find_by_id(video.id).should_not be_nil end end + + describe "add_timeline" do + + let!(:recorded_jam_track_track) {FactoryGirl.create(:recorded_jam_track_track)} + let(:recording) {recorded_jam_track_track.recording} + let(:timeline_data) {{"sample" => "data"}} + let(:good_timeline) { { + "tracks" => [ + { + "id" => recorded_jam_track_track.jam_track_track.id, + "timeline" => timeline_data, + "type" => "jam_track" + } + ] + } + } + + it "applies timeline data correctly" do + recording.add_timeline good_timeline + recorded_jam_track_track.reload + JSON.parse(recorded_jam_track_track.timeline).should eq(timeline_data) + end + + it "fails if no tracks data" do + expect { recording.add_timeline({}) }.to raise_error(JamRuby::JamArgumentError) + end + end end diff --git a/web/app/assets/javascripts/JamServer.js b/web/app/assets/javascripts/JamServer.js index 73997c0a7..61b889eb2 100644 --- a/web/app/assets/javascripts/JamServer.js +++ b/web/app/assets/javascripts/JamServer.js @@ -529,10 +529,12 @@ if(server.connecting) { logger.error("server.connect should never be called if we are already connecting. cancelling.") + // XXX should return connectDeferred, but needs to be tested/vetted return; } if(server.connected) { logger.error("server.connect should never be called if we are already connected. cancelling.") + // XXX should return connectDeferred, but needs to be tested/vetted return; } @@ -678,7 +680,12 @@ logger.info("server.send(" + jsMessage + ")"); } if (server !== undefined && server.socket !== undefined && server.socket.send !== undefined) { - server.socket.send(jsMessage); + try { + server.socket.send(jsMessage); + } + catch(err) { + logger.warn("error when sending on websocket: " + err) + } } else { logger.warn("Dropped message because server connection is closed."); } diff --git a/web/app/assets/javascripts/dialog/openJamTrackDialog.js b/web/app/assets/javascripts/dialog/openJamTrackDialog.js index 21432a68e..349742ad7 100644 --- a/web/app/assets/javascripts/dialog/openJamTrackDialog.js +++ b/web/app/assets/javascripts/dialog/openJamTrackDialog.js @@ -24,6 +24,7 @@ } function beforeShow() { + $dialog.data('result', null) emptyList(); resetPagination(); showing = true; @@ -77,18 +78,8 @@ // tell the server we are about to start a recording rest.openJamTrack({id: context.JK.CurrentSessionModel.id(), jam_track_id: jamTrack.id}) .done(function(response) { - context.jamClient.JamTrackStopPlay(); - var result = context.jamClient.JamTrackPlay(jamTrack.id); - - logger.debug("JamTrackPlay response: %o", result); - - if(result) { - app.layout.closeDialog('open-jam-track-dialog'); - } - else { - logger.error("unable to open jam track") - } - + $dialog.data('result', {success:true, jamTrack: jamTrack}) + app.layout.closeDialog('open-jam-track-dialog'); }) .fail(function(jqXHR) { app.notifyServerError(jqXHR, "Unable to Open JamTrack For Playback"); diff --git a/web/app/assets/javascripts/download_jamtrack.js.coffee b/web/app/assets/javascripts/download_jamtrack.js.coffee index 75e684bc6..6e09d8bc5 100644 --- a/web/app/assets/javascripts/download_jamtrack.js.coffee +++ b/web/app/assets/javascripts/download_jamtrack.js.coffee @@ -2,10 +2,471 @@ $ = jQuery context = window context.JK ||= {}; -context.JK.DownloadJamTrack = class SyncViewer - constructor: (@app) -> +# This is the sequence of how this widget works: +# checkState() is the heart of the state machine; it is called to get things going, and is called whenevr a state ends +# checkState() checks first against what the client thinks about the state of the JamTrack; +# if it on the disk then the state machine may enter one of: +# * synchronized +# * keying +# +# if it's still on the server, then the state machine may be: +# * packaging +# * downloading +# +# errored state can be entered from @jamTrack.jam_track_right_id +# +# other state; you augment the error to the user by suppling @errorMessage before transitioning +# +# no-client is the way the widget behaves when you are in a normal browser (i.e., nothing happens other than tell the user to use the client) +# +# Discussion of the different states: +# There are different states that a JamTrack can be in. +# The final success state is that the JamTrack is on disk and loadable. (show synchronized state) +# But there are others until you get there: +# The JamTrack does not exist on the server, so we will create it (packaging state) +# The JamTrack exists on the server, but not on disk, so we will download it (downloading state) +# The JamTrack is on the disk, but does not yet have keys, so we will fetch them (keying) + +context.JK.DownloadJamTracks = {} +context.JK.DownloadJamTrack = class DownloadJamTrack + constructor: (@app, jamTrack, size = 'large') -> @EVENTS = context.JK.EVENTS @rest = context.JK.Rest() + @logger = context.JK.logger + @jamTrack = jamTrack + @size = size + @attemptedEnqueue = false + @errorReason = null + @errorMessage = null + @transitionTimer = null + @downloadTimer = null + @trackDetail = null + @stateHolder = null + @active = false + @startTime = null + @attempts = 0 + @tracked = false + @ajaxEnqueueAborted = false + @ajaxGetJamTrackRightAborted = false + throw "no JamTrack specified" unless @jamTrack? + throw "invalid size" if @size != 'large' && @size != 'small' + throw "no JamTrack version" unless @jamTrack.version? + + @path = [] + @states = { + no_client: { name: 'no-client', show: @showNoClient, leaf: true }, + synchronized: { name: 'synchronized', show: @showSynchronized, leaf: true}, + packaging: { name: 'packaging', show: @showPackaging }, + downloading: { name: 'downloading', show: @showDownloading }, + keying: { name: 'keying', show: @showKeying, max_time: 10000 }, + initial: { name: 'initial', show: @showInitial }, + quiet: { name: 'quiet', show: @showQuiet }, + errored: { name: 'errored', show: @showError, leaf: true} + } + + context.JK.DownloadJamTracks[@jamTrack.id] = this + downloadJamTrackTemplate = $('#template-download-jamtrack') + throw "no download jamtrack template" if not downloadJamTrackTemplate.exists() + + @root = $(downloadJamTrackTemplate.html()) + @stateHolder = @root.find('.state') + @root.on('remove', this.destroy) # automatically destroy self when removed from DOM + + # populate in template and visual transition functions + for state, data of @states + data.template = $("#template-download-jamtrack-state-#{data.name}") + + # start off in quiet state, but don't do it through transition system. The transition system expects a change, not initial state + @state = @states.quiet + + this.showState() + + + # after you've created the DownloadJamTrack widget, call synchronize which will begin ensuring that the jamtrack + # is downloaded and ready to open init: () => - @root = $($('#template-download-jamtrack').html()) + @active = true + @root.addClass('active') + this.reset() + + # check if we are in a browser or client + if !gon.isNativeClient + this.transition(@states.no_client) + else + this.transition(@states.initial) + + # when done with the widget, call destroy; this ensures it's not still active, and tracks final metrics + destroy: () => + $(this).off() + @active = false + @root.removeClass('active') + this.trackProgress() + # since we are not in a leave node, we need to report a state since this is effectively our end state + this.reset() + + reset: () => + @path = [] + @attempts = 0 + @tracked = false + @startTime = new Date() + # reset attemptedEnqueue to false, to allow one attempt to enqueue + @attemptedEnqueue = false + this.clearDownloadTimer() + this.clearTransitionTimer() + this.abortEnqueue() + this.abortGetJamTrackRight() + for state, data of @states + if data.timer? + clearInterval(data.timer) + data.timer = null + + abortEnqueue: () => + if @ajaxEnqueueAborted + @logger.debug("DownloadJamTrack: aborting ajax enqueue") + # we need to clear out @ajaxEnqueue *before* calling abort(), because the .fail callback fires inline + ajax = @ajaxEnqueueAborted + @ajaxEnqueueAborted = true + ajax.abort() + + abortGetJamTrackRight: () => + if @ajaxGetJamTrackRightAborted + @logger.debug("DownloadJamTrack: aborting ajax GetJamTrackRight") + # we need to clear out @ajaxEnqueue *before* calling abort(), because the .fail callback fires inline + ajax = @ajaxGetJamTrackRightAborted + @ajaxGetJamTrackRightAborted = true + ajax.abort() + + showState: () => + @state.stateStartTime = new Date(); + @stateHolder.children().remove() + @stateHolder.append(context._.template(@state.template.html(), @jamTrack, { variable: 'data' })) + @stateHolder.find('.' + @size).removeClass('hidden') + @state.show() + + # report a stat now that we've reached the end of this widget's journey + trackProgress: () => + + # do not double-report + if @tracked + return + + if @path.length == 0 + return + + unless @state.leaf + # we've been asked to report at a non-leaf node, meaning the user must have cancelled + @path.push('user-cancelled') + + flattened_path = @path.join('-') + + data = { + value: 1, + path: flattened_path, + duration: (new Date().getTime() - @startTime.getTime()) / 1000, + attempts: @attempts, + user_id: context.JK.currentUserId, + user_name: context.JK.currentUserName} + if @state == @states.errored + data.result = 'error' + data.detail = @errorReason + else + data.result = 'success' + + context.stats.write('web.jamtrack.downloader', data) + @tracked = true + + showPackaging: () => + @logger.debug("showing #{@state.name}") + this.expectTransition() + + 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, this.makeDownloadSuccessCallback(), this.makeDownloadFailureCallback()) + + showKeying: () => + @logger.debug("showing #{@state.name}") + context.jamClient.JamTrackKeysRequest() + this.waitForState() + + showQuiet: () => + @logger.debug("showing #{@state.name}") + + showInitial: () => + @logger.debug("showing #{@state.name}") + @attempts = @attempts + 1 + this.expectTransition() + context.JK.SubscriptionUtils.subscribe('jam_track_right', @jamTrack.jam_track_right_id).on(context.JK.EVENTS.SUBSCRIBE_NOTIFICATION, this.onJamTrackRightEvent) + this.checkState() + + showError: () => + @logger.debug("showing #{@state.name}") + context.JK.SubscriptionUtils.unsubscribe('jam_track_right', @jamTrack.jam_track_right_id) + + if @size == 'large' + @stateHolder.find('.msg').text(@errorMessage) + @stateHolder.find('.retry-button').click(this.retry) + else + @stateHolder.find('.msg').text(@jamTrack.name + ' (error)') + @stateHolder.find('.errormsg').text(@errorMessage) + @stateHolder.find('.retry-button').on('click', this.retry) + + retryMsg = '' + if @attempts > 1 + retryMsg = 'Continue retrying or contact support@jamkazam.com' + + @stateHolder.find('.retry').text(retryMsg) + + showSynchronized: () => + @logger.debug("showing #{@state.name}") + context.JK.SubscriptionUtils.unsubscribe('jam_track_right', @jamTrack.jam_track_right_id) + + showNoClient: () => + @logger.debug("showing #{@state.name}") + + downloadCheck: () => + @logger.debug "downloadCheck" + + retry: () => + @path = [] + @path.push('retry') + # just switch to the initial state again, causing the loop to start again + this.transition(@states.initial) + return false + + clearStateTimer: () => + if @state.timer? + clearInterval(@state.timer) + @state.timer = null + + stateIntervalCheck: () => + this.checkState() + + # if the timer is null now, then it must have been whacked due to a state change + # if not, then let's see if we have timed out + if @state.timer? + if (new Date()).getTime() - @state.stateStartTime.getTime() > @state.max_time + @logger.debug("The current step (#{@state.name}) took too long") + + if @state == @states.keying + # specific message + this.transitionError("#{@state.name}-timeout", "It took too long for the JamTrack to be keyed.") + else + # generic message + this.transitionError("#{@state.name}-timeout", "The current step (#{@state.name}) took too long") + + + # sets an interval timer for every second, waiting for the status to change + waitForState: () => + unless @active + @logger.error("DownloadJamTrack: ignoring waitForState because we are not active") + + @state.timer = setInterval(this.stateIntervalCheck, 1000) + + + # unused atm; the backend is good about always signalling. we still should though + expectDownload: () => + unless @active + @logger.error("DownloadJamTrack: ignoring expectDownload because we are not active") + + # every 10 seconds, wake up and check the server and see if we missed a state transition + this.clearDownloadTimer() + @downloadTimer = setTimeout(this.downloadCheck, 10000) + + clearDownloadTimer: () => + if @downloadTimer? + clearTimeout(@downloadTimer) + @downloadTimer = null + + transitionError: (reasonCode, errorMessage) => + @errorReason = reasonCode + @errorMessage = errorMessage + this.transition(@states.errored) + + transitionCheck: () => + this.checkState() + + # this should be called every time something changes statefully, to restart a 12 second timer to hit the server for update. + # if everything is moving snappily, we won't have to query the server much, because we are also getting subscription events + # about any changes to the status of the jam track. But, we could miss a message or there could be a path in the server where + # we don't get an event, so that's why, after 12 seconds, we'll still go to the server and check. + # exception: this should not be runngi + expectTransition: () => + unless @active + @logger.error("DownloadJamTrack: ignoring expectTransition because we are not active") + + # every 12 seconds, wake up and check the server and see if we missed a state transition + this.clearTransitionTimer() + @transitionTimer = setTimeout(this.transitionCheck, 12000) + + clearTransitionTimer: () => + if @transitionTimer? + clearTimeout(@transitionTimer) + @transitionTimer = null + + transition: (newState) => + unless @active + @logger.error("DownloadJamTrack: ignoring state change because we are not active") + return + + if newState == @state + @logger.debug("DownloadJamTrack: ignoring state change #{@state.name} #{newState}") + return + + if @state? + @logger.debug("DownloadJamTrack: state change: #{@state.name} => #{newState.name}") + # make sure there is no timer running on the old state + this.clearTransitionTimer() + this.clearStateTimer() + this.abortEnqueue() + @logger.debug("aborting getJamTrack right on state change") + this.abortGetJamTrackRight() + else + @logger.debug("DownloadJamTrack: initial state: #{newState.name}") + + @state = newState + + # track which states were taken + @path.push(@state.name) + + if @state.leaf + this.trackProgress() + + this.showState() + + $(this).triggerHandler(@EVENTS.JAMTRACK_DOWNLOADER_STATE_CHANGED, {state: @state}) + + checkState: () => + # check for the success state against the local state of the client... if it's playable, then we should be OK + @trackDetail = context.jamClient.JamTrackGetTrackDetail (@jamTrack.id) + + @logger.debug("DownloadJamTrack: JamTrackGetTrackDetail.key_state: " + @trackDetail.key_state) + + # first check if the version is not the same; if so, invalidate. + + if @trackDetail.version? + if @jamTrack.version != @trackDetail.version + @logger.info("DownloadJamTrack: JamTrack on disk is different version (stored: #{@trackDetail.version}, server: #{@jamTrack.version}. Invalidating") + context.jamClient.InvalidateJamTrack(@jamTrack.id) + @trackDetail = context.jamClient.JamTrackGetTrackDetail (@jamTrack.id) + + if @trackDetail.version? + @logger.error("after invalidating package, the version is still wrong!") + throw "after invalidating package, the version is still wrong!" + + switch @trackDetail.key_state + when 'pending' + this.transition(@states.keying) + when 'not authorized' + # TODO: if not authorized, do we need to re-initiate a keying attempt? + this.transition(@states.keying) + when 'ready' + this.transition(@states.synchronized) + when 'unknown' + @ajaxGetJamTrackRightAborted = false + @rest.getJamTrackRight({id: @jamTrack.id}) + .done(this.processJamTrackRight) + .fail(this.processJamTrackRightFail) + + + processSigningState: (signingState) => + @logger.debug("DownloadJamTrack: processSigningState: " + signingState) + + switch signingState + when 'QUIET' + if @attemptedEnqueue + # this means we've already tried to poke the server. something is wrong + this.transitionError("enqueue-timeout", "The server has not begun building your JamTrack.") + else + this.expectTransition() + + this.attemptToEnqueue() + when 'QUEUED' + # when it's queued, there is nothing to do except wait. + this.transition(@states.packaging) + when 'QUEUED_TIMEOUT' + this.transitionError("queued-timeout", "The server took too long to begin processing your JamTrack.") + when 'SIGNING' + this.transition(@states.packaging) + when 'SIGNING_TIMEOUT' + this.transitionError("signing-timeout", "The server took too long to create your JamTrack.") + when 'SIGNED' + this.transition(@states.downloading) + when 'ERROR' + if @attemptedEnqueue + # this means we've already tried to poke the server. something is wrong + this.transitionError("package-error", "The server failed to create your package.") + else + this.expectTransition() + + this.attemptToEnqueue() + else + @logger.error("unknown state: " + signingState) + this.transitionError("unknown-state-#{signingState}", "The server sent an unknown state message: " + signingState) + + attemptToEnqueue: () => + @attemptedEnqueue = true + @ajaxEnqueueAborted = false + @rest.enqueueJamTrack({id: @jamTrack.id}) + .done(this.processEnqueueJamTrack) + .fail(this.processEnqueueJamTrackFail) + + + processJamTrackRight: (myJamTrack) => + unless @ajaxGetJamTrackRightAborted + this.processSigningState(myJamTrack.signing_state) + else + @logger.debug("DownloadJamTrack: ignoring processJamTrackRight response") + + processJamTrackRightFail: () => + unless @ajaxGetJamTrackRightAborted? + this.transitionError("status-check-error", "Unable to check with the server on the status of your JamTrack.") + else + @logger.debug("DownloadJamTrack: ignoring processJamTrackRightFail response") + + processEnqueueJamTrack: (enqueueResponse) => + unless @ajaxEnqueueAborted + this.expectTransition() # the act of enqueuing should send down events to the client. we wait... + else + @logger.debug("DownloadJamTrack: ignoring processEnqueueJamTrack response") + + processEnqueueJamTrackFail: () => + unless @ajaxEnqueueAborted + this.transitionError("enqueue-error", "Unable to ask the server to build your JamTrack.") + else + @logger.debug("DownloadJamTrack: ignoring processEnqueueJamTrackFail response") + + onJamTrackRightEvent: (e, data) => + @logger.debug("DownloadJamTrack: subscription notification received: type:" + data.type) + this.expectTransition() + this.processSigningState(data.body.signing_state) + + downloadProgressCallback: (bytesReceived, bytesTotal, downloadSpeedMegSec, timeRemaining) => + bytesReceived = Number(bytesReceived) + bytesTotal = Number(bytesTotal) + # bytesTotal from Qt is not trust worthy; trust server's answer instead + #progressWidth = ((bytesReceived / updateSize) * 100).toString() + "%"; + # $('#progress-bar').width(progressWidth) + + downloadSuccessCallback: (updateLocation) => + # is the package loadable yet? + @logger.debug("DownloadJamTrack: download complete - on to keying") + this.transition(@states.keying) + + downloadFailureCallback: (errorMsg) => + + this.transitionError("download-error", errorMsg) + + # makes a function name for the backend + makeDownloadProgressCallback: () => + "JK.DownloadJamTracks['#{@jamTrack.id}'].downloadProgressCallback" + + # makes a function name for the backend + makeDownloadSuccessCallback: () => + "JK.DownloadJamTracks['#{@jamTrack.id}'].downloadSuccessCallback" + + # makes a function name for the backend + makeDownloadFailureCallback: () => + "JK.DownloadJamTracks['#{@jamTrack.id}'].downloadFailureCallback" + diff --git a/web/app/assets/javascripts/faderHelpers.js b/web/app/assets/javascripts/faderHelpers.js index b4cb3823d..ba66a44ca 100644 --- a/web/app/assets/javascripts/faderHelpers.js +++ b/web/app/assets/javascripts/faderHelpers.js @@ -20,7 +20,8 @@ e.stopPropagation(); var $fader = $(this); - + var sessionModel = window.JK.CurrentSessionModel || null; + var mediaControlsDisabled = $fader.data('media-controls-disabled'); if(mediaControlsDisabled) { var mediaTrackOpener = $fader.data('media-track-opener'); @@ -28,11 +29,16 @@ return false; } + if(sessionModel && sessionModel.areControlsLockedForJamTrackRecording() && $control.closest('.session-track').data('track_data').type == 'jam_track') { + window.JK.prodBubble($fader, 'jamtrack-controls-disabled', {}, {positions:['top'], offsetParent: $fader.closest('.screen')}) + return false; + } + if($fader.data('showHelpAboutMediaMixers')) { - if(window.JK.CurrentSessionModel) { - if(!window.JK.CurrentSessionModel.hasShownAudioMediaMixerHelp()) { + if(sessionModel) { + if(!sessionModel.hasShownAudioMediaMixerHelp()) { window.JK.prodBubble($fader, 'volume-media-mixers', {}, {positions:['top'], offsetParent: $fader.closest('.screen')}) - window.JK.CurrentSessionModel.markShownAudioMediaMixerHelp() + sessionModel.markShownAudioMediaMixerHelp() } } } @@ -138,10 +144,16 @@ var mediaControlsDisabled = $draggingFaderHandle.data('media-controls-disabled'); var mediaTrackOpener = $draggingFaderHandle.data('media-track-opener'); - + var sessionModel = window.JK.CurrentSessionModel || null; + if(mediaControlsDisabled) { return false; } + + if(sessionModel && sessionModel.areControlsLockedForJamTrackRecording() && $draggingFaderHandle.closest('.session-track').data('track_data').type == 'jam_track') { + return false; + } + return true; } diff --git a/web/app/assets/javascripts/fakeJamClient.js b/web/app/assets/javascripts/fakeJamClient.js index 4db550fcb..7a15ffbb1 100644 --- a/web/app/assets/javascripts/fakeJamClient.js +++ b/web/app/assets/javascripts/fakeJamClient.js @@ -707,6 +707,16 @@ function TrackSetInstrument(track, instrumentId) {} + function JamTrackStopPlay() {} + function JamTrackPlay(){return true; } + function JamTrackIsPlayable() { + return true; + } + function JamTrackGetTrackDetail() { + return {key_state: 'unknown'} + } + function JamTrackKeysRequest() {} + function JamTrackDownload() {} // Method which sets volume function UpdateMixer(mixerId) {} @@ -1062,6 +1072,13 @@ this.TrackGetChatUsesMusic = TrackGetChatUsesMusic; this.TrackSetChatUsesMusic = TrackSetChatUsesMusic; + this.JamTrackStopPlay = JamTrackStopPlay; + this.JamTrackPlay = JamTrackPlay; + this.JamTrackIsPlayable = JamTrackIsPlayable; + this.JamTrackGetTrackDetail = JamTrackGetTrackDetail; + this.JamTrackKeysRequest = JamTrackKeysRequest; + this.JamTrackDownload = JamTrackDownload; + // Scoring Knobs this.GetScoreWorkTimingInterval = GetScoreWorkTimingInterval; this.SetScoreWorkTimingInterval = SetScoreWorkTimingInterval; diff --git a/web/app/assets/javascripts/globals.js b/web/app/assets/javascripts/globals.js index 584eba17b..01c640719 100644 --- a/web/app/assets/javascripts/globals.js +++ b/web/app/assets/javascripts/globals.js @@ -45,7 +45,8 @@ SUBSCRIBE_NOTIFICATION: 'subscribe_notification', CONNECTION_UP: 'connection_up', CONNECTION_DOWN: 'connection_down', - SCREEN_CHANGED: 'screen_changed' + SCREEN_CHANGED: 'screen_changed', + JAMTRACK_DOWNLOADER_STATE_CHANGED: 'jamtrack_downloader_state' }; context.JK.ALERT_NAMES = { diff --git a/web/app/assets/javascripts/jam_rest.js b/web/app/assets/javascripts/jam_rest.js index f0500ae65..dca216937 100644 --- a/web/app/assets/javascripts/jam_rest.js +++ b/web/app/assets/javascripts/jam_rest.js @@ -1434,6 +1434,28 @@ }); } + function getJamTrackRight(options) { + var jamTrackId = options['id']; + + return $.ajax({ + type: "GET", + url: '/api/jamtracks/rights/' + jamTrackId + '?' + $.param(options), + dataType: "json", + contentType: 'application/json' + }) + } + + function enqueueJamTrack(options) { + var jamTrackId = options['id']; + + return $.ajax({ + type: "POST", + url: '/api/jamtracks/enqueue?' + jamTrackId + '?' + $.param(options), + dataType: "json", + contentType: 'applications/json' + }); + } + function getPurchasedJamTracks(options) { return $.ajax({ type: "GET", @@ -1563,13 +1585,23 @@ }); } - function validateUrlSite(url, sitetype) { + function validateUrlSite(url, sitetype) { + return $.ajax({ + type: "GET", + url: '/api/data_validation?sitetype='+sitetype+'&data=' + encodeURIComponent(url), + contentType: 'application/json' + }); + } + + function addRecordingTimeline(recordingId, data) { return $.ajax({ - type: "GET", - url: '/api/data_validation?sitetype='+sitetype+'&data=' + encodeURIComponent(url), - contentType: 'application/json' + type: "POST", + url: '/api/recordings/' + recordingId + '/timeline', + dataType: "json", + contentType: 'application/json', + data: JSON.stringify(data), }); - } + } function initialize() { return self; @@ -1694,6 +1726,8 @@ this.updateAudioLatency = updateAudioLatency; this.getJamtracks = getJamtracks; this.getPurchasedJamTracks = getPurchasedJamTracks; + this.getJamTrackRight = getJamTrackRight; + this.enqueueJamTrack = enqueueJamTrack; this.getBackingTracks = getBackingTracks; this.addJamtrackToShoppingCart = addJamtrackToShoppingCart; this.getShoppingCarts = getShoppingCarts; @@ -1710,6 +1744,7 @@ this.createSourceChange = createSourceChange; this.validateUrlSite = validateUrlSite; this.markRecordedBackingTrackSilent = markRecordedBackingTrackSilent; + this.addRecordingTimeline = addRecordingTimeline; return this; }; diff --git a/web/app/assets/javascripts/order.js b/web/app/assets/javascripts/order.js index fb741ad31..c5eb34e65 100644 --- a/web/app/assets/javascripts/order.js +++ b/web/app/assets/javascripts/order.js @@ -4,9 +4,12 @@ context.JK = context.JK || {}; context.JK.OrderScreen = function(app) { + var EVENTS = context.JK.EVENTS; var logger = context.JK.logger; var $screen = null; + var $templateOrderContent = null; + var $templatePurchasedJamTrack = null; var $navigation = null; var $billingInfo = null; var $shippingInfo = null; @@ -16,15 +19,23 @@ var $paymentInfoPanel = null; var $orderPanel = null; var $thanksPanel = null; + var $jamTrackInBrowser = null; + var $purchasedJamTrack = null; + var $purchasedJamTrackHeader = null; + var $purchasedJamTracks = null; var $orderContent = null; var userDetail = null; var step = null; var billing_info = null; var shipping_info = null; var shipping_as_billing = null; + var downloadJamTracks = []; + var purchasedJamTracks = null; + var purchasedJamTrackIterator = 0; function beforeShow() { - beforeShowPaymentInfo(); + beforeShowPaymentInfo(); + resetJamTrackDownloadInfo(); } function beforeShowPaymentInfo() { @@ -33,6 +44,12 @@ renderAccountInfo(); } + function resetJamTrackDownloadInfo() { + $purchasedJamTrack.addClass('hidden'); + $purchasedJamTracks.children().remove() + $jamTrackInBrowser.hide('hidden'); + } + function renderAccountInfo() { rest.getUserDetail() .done(populateAccountInfo) @@ -81,6 +98,21 @@ } function afterShow(data) { + // XXX : style-test code + // moveToThanks({jam_tracks: [{id: 14, jam_track_right_id: 11, name: 'Back in Black'}, {id: 15, jam_track_right_id: 11, name: 'In Bloom'}, {id: 16, jam_track_right_id: 11, name: 'Love Bird Supreme'}]}); + } + + function beforeHide() { + if(downloadJamTracks) { + context._.each(downloadJamTracks, function(downloadJamTrack) { + downloadJamTrack.destroy(); + downloadJamTrack.root.remove(); + }) + + downloadJamTracks = []; + } + purchasedJamTracks = null; + purchasedJamTrackIterator = 0; } function next(e) { @@ -412,7 +444,7 @@ data.shipping_as_billing = shipping_as_billing var orderContentHtml = $( context._.template( - $('#template-order-content').html(), + $templateOrderContent.html(), data, {variable: 'data'} ) @@ -430,13 +462,86 @@ beforeShowOrder(); } - function moveToThanks() { + function moveToThanks(purchaseResponse) { $("#order_error").addClass("hidden") $paymentInfoPanel.addClass("hidden") $orderPanel.addClass("hidden") $thanksPanel.removeClass("hidden") rest.clearShoppingCart() beforeShowOrder() + handleJamTracksPurchased(purchaseResponse.jam_tracks) + } + + function handleJamTracksPurchased(jamTracks) { + // were any JamTracks purchased? + var jamTracksPurchased = jamTracks && jamTracks.length > 0; + if(jamTracksPurchased) { + if(gon.isNativeClient) { + startDownloadJamTracks(jamTracks) + } + else { + $jamTrackInBrowser.removeClass('hidden'); + } + } + } + function startDownloadJamTracks(jamTracks) { + // there can be multiple purchased JamTracks, so we cycle through them + + purchasedJamTracks = jamTracks; + + // populate list of jamtracks purchased, that we will iterate through graphically + context._.each(jamTracks, function(jamTrack) { + var downloadJamTrack = new context.JK.DownloadJamTrack(app, jamTrack, 'small'); + var $purchasedJamTrack = $(context._.template( + $templatePurchasedJamTrack.html(), + jamTrack, + {variable: 'data'} + )); + + $purchasedJamTracks.append($purchasedJamTrack) + + // show it on the page + $purchasedJamTrack.append(downloadJamTrack.root) + + downloadJamTracks.push(downloadJamTrack) + }) + + iteratePurchasedJamTracks(); + } + + function iteratePurchasedJamTracks() { + if(purchasedJamTrackIterator < purchasedJamTracks.length ) { + var downloadJamTrack = downloadJamTracks[purchasedJamTrackIterator++]; + + // make sure the 'purchasing JamTrack' section can be seen + $purchasedJamTrack.removeClass('hidden'); + + // the widget indicates when it gets to any transition; we can hide it once it reaches completion + $(downloadJamTrack).on(EVENTS.JAMTRACK_DOWNLOADER_STATE_CHANGED, function(e, data) { + + if(data.state == downloadJamTrack.states.synchronized) { + logger.debug("jamtrack " + downloadJamTrack.jamTrack.name + " synchronized;") + //downloadJamTrack.root.remove(); + downloadJamTrack.destroy(); + + // go to the next JamTrack + iteratePurchasedJamTracks() + } + }) + + logger.debug("jamtrack " + downloadJamTrack.jamTrack.name + " downloader initializing") + + // kick off the download JamTrack process + downloadJamTrack.init() + + // XXX style-test code + // downloadJamTrack.transitionError("package-error", "The server failed to create your package.") + + } + else { + logger.debug("done iterating over purchased JamTracks") + $purchasedJamTrackHeader.text('All purchased JamTracks have been downloaded successfully! You can now play them in a session.') + } } function moveToPaymentInfo(e) { @@ -506,21 +611,28 @@ function initialize() { var screenBindings = { 'beforeShow': beforeShow, - 'afterShow': afterShow + 'afterShow': afterShow, + 'beforeHide' : beforeHide }; app.bindScreen('order', screenBindings); - $screen = $("#orderScreen"); - $paymentInfoPanel = $screen.find(".checkout-payment-info"); - $orderPanel = $screen.find(".order-panel"); - $thanksPanel = $screen.find(".thanks-panel"); - $navigation = $screen.find(".checkout-navigation-bar"); - $billingInfo = $paymentInfoPanel.find(".billing-address"); - $shippingInfo = $paymentInfoPanel.find(".shipping-address"); - $paymentMethod = $paymentInfoPanel.find(".payment-method"); - $shippingAddress = $paymentInfoPanel.find(".shipping-address-detail"); - $shippingAsBilling = $paymentInfoPanel.find("#shipping-as-billing"); - $orderContent = $orderPanel.find(".order-content"); + $screen = $("#orderScreen"); + $templateOrderContent = $("#template-order-content"); + $templatePurchasedJamTrack = $('#template-purchased-jam-track'); + $paymentInfoPanel = $screen.find(".checkout-payment-info"); + $orderPanel = $screen.find(".order-panel"); + $thanksPanel = $screen.find(".thanks-panel"); + $jamTrackInBrowser = $screen.find(".thanks-detail.jam-tracks-in-browser"); + $purchasedJamTrack = $thanksPanel.find(".thanks-detail.purchased-jam-track"); + $purchasedJamTrackHeader = $purchasedJamTrack.find(".purchased-jam-track-header"); + $purchasedJamTracks = $purchasedJamTrack.find(".purchased-list") + $navigation = $screen.find(".checkout-navigation-bar"); + $billingInfo = $paymentInfoPanel.find(".billing-address"); + $shippingInfo = $paymentInfoPanel.find(".shipping-address"); + $paymentMethod = $paymentInfoPanel.find(".payment-method"); + $shippingAddress = $paymentInfoPanel.find(".shipping-address-detail"); + $shippingAsBilling = $paymentInfoPanel.find("#shipping-as-billing"); + $orderContent = $orderPanel.find(".order-content"); if($screen.length == 0) throw "$screen must be specified"; if($navigation.length == 0) throw "$navigation must be specified"; diff --git a/web/app/assets/javascripts/playbackControls.js b/web/app/assets/javascripts/playbackControls.js index e05c185ae..3930635e6 100644 --- a/web/app/assets/javascripts/playbackControls.js +++ b/web/app/assets/javascripts/playbackControls.js @@ -47,6 +47,7 @@ var canUpdateBackend = false; var playbackMode = PlaybackMode.EveryWhere; var monitorPlaybackTimeout = null; + var jamTrackMode = false; // if true, we use different APIs to determine playback info function startPlay() { updateIsPlaying(true); @@ -67,7 +68,7 @@ playbackPositionMs = parseInt((offsetLeft / sliderBarWidth) * playbackDurationMs); updateCurrentTimeText(playbackPositionMs); if(canUpdateBackend) { - $self.triggerHandler('change-position', {positionMs: playbackPositionMs}); + $self.triggerHandler('change-position', {positionMs: playbackPositionMs, jamTrackMode: jamTrackMode}); canUpdateBackend = false; } } @@ -100,13 +101,25 @@ } $playButton.on('click', function(e) { - startPlay(); + var sessionModel = context.JK.CurrentSessionModel || null; + if(sessionModel && sessionModel.areControlsLockedForJamTrackRecording() && $parentElement.closest('.session-track').data('track_data').type == 'jam_track') { + context.JK.prodBubble($fader, 'jamtrack-controls-disabled', {}, {positions:['top'], offsetParent: $playButton}) return false; + } + + startPlay(); + return false; }); $pauseButton.on('click', function(e) { - stopPlay(); + var sessionModel = context.JK.CurrentSessionModel || null; + if(sessionModel && sessionModel.areControlsLockedForJamTrackRecording() && $parentElement.closest('.session-track').data('track_data').type == 'jam_track') { + context.JK.prodBubble($pauseButton, 'jamtrack-controls-disabled', {}, {positions:['top'], offsetParent: $pauseButton}) return false; + } + + stopPlay(); + return false; }); $sliderBar.on('click', function(e) { @@ -144,16 +157,31 @@ }); function monitorRecordingPlayback() { + if(jamTrackMode) { + var positionMs = context.jamClient.SessionCurrrentJamTrackPlayPosMs(); + var duration = context.jamClient.SessionGetJamTracksPlayDurationMs(); + var durationMs = duration.media_len; + var start = duration.start; // needed to understand start offset, and prevent slider from moving in tapins + //console.log("JamTrack start: " + start) + } + else { + var positionMs = context.jamClient.SessionCurrrentPlayPosMs(); + var durationMs = context.jamClient.SessionGetTracksPlayDurationMs(); + } + var isPlaying = context.jamClient.isSessionTrackPlaying(); - var positionMs = context.jamClient.SessionCurrrentPlayPosMs(); - var durationMs = context.jamClient.SessionGetTracksPlayDurationMs(); + + if(positionMs < 0) { + // bug in backend? + positionMs = 0; + } update(positionMs, durationMs, isPlaying); monitorPlaybackTimeout = setTimeout(monitorRecordingPlayback, 500); } - function update(currentTimeMs, durationTimeMs, isPlaying) { + function update(currentTimeMs, durationTimeMs, isPlaying, offsetStart) { if(dragging) { return; @@ -175,6 +203,10 @@ } } + + if(currentTimeMs < offsetStart) { + currentTimeMs = 0; // this is to squelch movement during tap-in period + } updateDurationTime(durationTimeMs); updateCurrentTime(currentTimeMs); updateIsPlaying(isPlaying); @@ -247,7 +279,10 @@ } } - function startMonitor() { + function startMonitor(_jamTrackMode) { + + jamTrackMode = !!_jamTrackMode; + monitorRecordingPlayback(); } diff --git a/web/app/assets/javascripts/recordingModel.js b/web/app/assets/javascripts/recordingModel.js index 078a41c4d..a0e275f48 100644 --- a/web/app/assets/javascripts/recordingModel.js +++ b/web/app/assets/javascripts/recordingModel.js @@ -132,7 +132,6 @@ $self.triggerHandler('stoppedRecording', {'recordingId': recording.id, 'reason' : 'rest', 'details' : arguments}); } }); - }); return true; } diff --git a/web/app/assets/javascripts/session.js b/web/app/assets/javascripts/session.js index b47946641..114c12c8a 100644 --- a/web/app/assets/javascripts/session.js +++ b/web/app/assets/javascripts/session.js @@ -82,6 +82,7 @@ var startingRecording = false; // double-click guard var claimedRecording = null; var backing_track_path = null; + var jamTrack = null; var playbackControls = null; var promptLeave = false; var rateSessionDialog = null; @@ -93,6 +94,10 @@ var $screen = null; var $mixModeDropdown = null; var $templateMixerModeChange = null; + var $otherAudioContainer = null; + var $myTracksContainer = null; + var $liveTracksContainer = null; + var downloadJamTrack = null; var $closePlaybackRecording = null; var $openBackingTrack = null; var mediaTrackGroups = [ChannelGroupIds.MediaTrackGroup, ChannelGroupIds.JamTrackGroup, ChannelGroupIds.MetronomeGroup]; @@ -106,7 +111,7 @@ window.location = '/client#/home'; } promptLeave = true; - $('#session-mytracks-container').empty(); + $myTracksContainer.empty(); displayDoneRecording(); // assumption is that you can't join a recording session, so this should be safe var shareDialog = new JK.ShareDialog(context.JK.app, sessionId, "session"); @@ -247,6 +252,7 @@ $(sessionModel.recordingModel) .on('startingRecording', function(e, data) { displayStartingRecording(); + lockControlsforJamTrackRecording(); }) .on('startedRecording', function(e, data) { if(data.reason) { @@ -292,12 +298,30 @@ { displayStartedRecording(); displayWhoCreated(data.clientId); + lockControlsforJamTrackRecording(); } }) .on('stoppingRecording', function(e, data) { displayStoppingRecording(data); + unlockControlsforJamTrackRecording(); }) .on('stoppedRecording', function(e, data) { + + unlockControlsforJamTrackRecording(); + if(sessionModel.selfOpenedJamTracks()) { + + var timeline = context.jamClient.GetJamTrackTimeline(); + + rest.addRecordingTimeline(data.recordingId, timeline) + .fail(function(){ + app.notify( + { title: "Unable to Add JamTrack Volume Data", + text: "The volume of the JamTrack will not be correct in the recorded mix." }, + null, + true); + }) + } + if(data.reason) { logger.warn("Recording Discarded: ", data); var reason = data.reason; @@ -461,6 +485,14 @@ playbackControls.stopMonitor(); } backing_track_path = currentSession == null ? null : currentSession.backing_track_path; + + if(jamTrack == null && (currentSession && currentSession.jam_track != null)) { + playbackControls.startMonitor(true); + } + else if(jamTrack && (currentSession == null || currentSession.jam_track == null)) { + playbackControls.stopMonitor(); + } + jamTrack = currentSession == null ? null : currentSession.jam_track; } function sessionChanged() { @@ -518,6 +550,14 @@ } } + function resetOtherAudioContent() { + if ($('.session-recordings .track').length === 0 && $('.session-recordings .download-jamtrack').length === 0) { + $('.session-recordings .when-empty').show(); + $('.session-recording-name-wrapper').hide(); + $('.session-recordings .recording-controls').hide(); + $('.session-recordings .session-recording-name').text('(No audio loaded)') + } + } function didSelfOpenMedia() { var localMediaMixers = _mixersForGroupIds([ChannelGroupIds.MediaTrackGroup, ChannelGroupIds.JamTrackGroup, ChannelGroupIds.MetronomeGroup], MIX_MODES.MASTER); @@ -530,7 +570,7 @@ } function renderSession() { - $('#session-mytracks-container').empty(); + $myTracksContainer.empty(); $('.session-track').remove(); // Remove previous tracks var $voiceChat = $('#voice-chat'); $voiceChat.hide(); @@ -545,7 +585,9 @@ if ($('.session-livetracks .track').length === 0) { $('.session-livetracks .when-empty').show(); } - + resetOtherAudioContent(); + + /** if ($('.session-recordings .track').length === 0) { $('.session-recordings .when-empty').show(); $('.session-recording-name-wrapper').hide(); @@ -558,6 +600,7 @@ $('.session-recordings .recording-controls').show(); checkShowCloseControl(); } + */ // Handle long labels: $(".track-label").dotdotdot() @@ -1047,6 +1090,7 @@ // Default trackData to participant + no Mixer state. var trackData = { + type: 'backing_track', trackId: backingTrack.id, clientId: backingTrack.client_id, name: 'Backing', @@ -1101,19 +1145,14 @@ // find the track or tracks that correspond to the mixer var correspondingTracks = [] $.each(jamTracks, function(i, jamTrack) { - if(mixer.id.indexOf("L") == 0) { - if(mixer.id.substring(1) == jamTrack.id) { + if(mixer.id == jamTrack.id) { correspondingTracks.push(jamTrack); } - else { - // this should not be possible - alert("Invalid state: the backing track had neither persisted_track_id or persisted_client_id"); - } - } }); 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", @@ -1145,6 +1184,7 @@ // Default trackData to participant + no Mixer state. var trackData = { + type: 'jam_track', trackId: oneOfTheTracks.id, clientId: oneOfTheTracks.client_id, name: name, @@ -1254,6 +1294,7 @@ // Default trackData to participant + no Mixer state. var trackData = { + type: 'metronome', trackId: "MS" + oneOfTheTracks.id, clientId: oneOfTheTracks.client_id, name: "Metronome", @@ -1361,6 +1402,7 @@ // Default trackData to participant + no Mixer state. var trackData = { + type: 'recorded_track', trackId: oneOfTheTracks.id, clientId: oneOfTheTracks.client_id, name: name, @@ -1771,15 +1813,14 @@ function _addTrack(allowDelete, trackData, mixer, oppositeMixer) { - var parentSelector = '#session-mytracks-container'; - var $destination = $(parentSelector); + var $destination = $myTracksContainer; if (trackData.clientId !== app.clientId) { - parentSelector = '#session-livetracks-container'; - $destination = $(parentSelector); + $destination = $liveTracksContainer $('.session-livetracks .when-empty').hide(); } var template = $('#template-session-track').html(); var newTrack = $(context.JK.fillTemplate(template, trackData)); + newTrack.data('track_data', trackData) var audioOverlay = $('.disabled-track-overlay', newTrack); var $trackIconMute = newTrack.find('.track-icon-mute') $trackIconMute.muteSelector().on(EVENTS.MUTE_SELECTED, trackMuteSelected) @@ -1789,7 +1830,7 @@ $destination.append(newTrack); // Render VU meters and gain fader - var trackSelector = parentSelector + ' .session-track[track-id="' + trackData.trackId + '"]'; + var trackSelector = $destination.selector + ' .session-track[track-id="' + trackData.trackId + '"]'; var gainPercent = trackData.gainPercent || 0; connectTrackToMixer(trackSelector, trackData, trackData.mixerId, gainPercent, trackData.group_id); @@ -1805,21 +1846,31 @@ tracks[trackData.trackId] = new context.JK.SessionTrack(trackData.clientId); } - + // something is being shown now in the other audio area + function otherAudioFilled() { + $('.session-recordings .when-empty').hide(); + $('.session-recording-name-wrapper').show(); + } function _addRecordingTrack(trackData, mixer, oppositeMixer) { + + otherAudioFilled(); + + $('.session-recordings .recording-controls').show(); + var parentSelector = '#session-recordedtracks-container'; var $destination = $(parentSelector); - + var template = $('#template-session-track').html(); var newTrack = $(context.JK.fillTemplate(template, trackData)); - $destination.append(newTrack); + newTrack.data('track_data', trackData); + $otherAudioContainer.append(newTrack); if(trackData.preMasteredClass) { context.JK.helpBubble($('.track-instrument', newTrack), 'pre-processed-track', {}, {offsetParent: newTrack.closest('.content-body')}); } // Render VU meters and gain fader - var trackSelector = parentSelector + ' .session-track[track-id="' + trackData.trackId + '"]'; + var trackSelector = $otherAudioContainer.selector + ' .session-track[track-id="' + trackData.trackId + '"]'; var gainPercent = trackData.gainPercent || 0; var $track = connectTrackToMixer(trackSelector, trackData, trackData.mixerId, gainPercent, null); var $trackIconMute = $track.find('.track-icon-mute') @@ -2072,6 +2123,11 @@ return false; } + if(sessionModel.areControlsLockedForJamTrackRecording() && $control.closest('.session-track').data('track_data').type == 'jam_track') { + context.JK.prodBubble($control, 'jamtrack-controls-disabled', {}, {positions:['bottom'], offsetParent: $control.closest('.screen')}) + return false; + } + if($control.data('showHelpAboutMediaMixers')) { if(!sessionModel.hasShownAudioMediaMixerHelp()) { context.JK.prodBubble($control, 'volume-media-mixers', {}, {positions:['bottom'], offsetParent: $control.closest('.screen')}) @@ -2079,6 +2135,8 @@ } } + + $.each(mixerIds, function(i,v) { var mixerId = v; // behavior: if this is the user's track in personal mode, then we mute the track globally @@ -2316,6 +2374,14 @@ $('#recording-status').text("Make a Recording"); } + function lockControlsforJamTrackRecording() { + sessionModel.lockControlsforJamTrackRecording(); + } + + function unlockControlsforJamTrackRecording() { + sessionModel.unlockControlsforJamTrackRecording(); + } + function displayWhoCreated(clientId) { if(app.clientId != clientId) { // don't show to creator sessionModel.findUserBy({clientId: clientId}) @@ -2382,7 +2448,69 @@ return false; } - app.layout.showDialog('open-jam-track-dialog'); + app.layout.showDialog('open-jam-track-dialog').one(EVENTS.DIALOG_CLOSED, function(e, data) { + + // once the dialog is closed, see if the user has a jamtrack selected + if(!data.canceled && data.result.jamTrack) { + + var jamTrack = data.result.jamTrack; + + // hide 'other audio' placeholder + otherAudioFilled(); + + if(downloadJamTrack) { + // if there was one showing before somehow, destroy it. + logger.warn("destroying existing JamTrack") + downloadJamTrack.root.remove(); + downloadJamTrack.destroy(); + downloadJamTrack = null + } + + downloadJamTrack = new context.JK.DownloadJamTrack(app, jamTrack, 'large'); + + // the widget indicates when it gets to any transition; we can hide it once it reaches completion + $(downloadJamTrack).on(EVENTS.JAMTRACK_DOWNLOADER_STATE_CHANGED, function(e, data) { + + if(data.state == downloadJamTrack.states.synchronized) { + logger.debug("jamtrack synchronized; hide widget and show tracks") + downloadJamTrack.root.remove(); + downloadJamTrack.destroy(); + downloadJamTrack = null; + + // XXX: test with this removed; it should be unnecessary + context.jamClient.JamTrackStopPlay(); + + if(jamTrack.jmep) + { + logger.debug("setting jmep data") + context.jamClient.JamTrackLoadJmep(jamTrack.id, jamTrack.jmep) + } + else { + logger.debug("no jmep data for jamtrack") + } + + // JamTrackPlay means 'load' + var result = context.jamClient.JamTrackPlay(jamTrack.id); + + if(!result) { + app.notify( + { title: "JamTrack Can Not Open", + text: "Unable to open your JamTrack. Please contact support@jamkazam.com" + }, null, true); + } + } + }) + + // show it on the page + $otherAudioContainer.append(downloadJamTrack.root) + + // kick off the download JamTrack process + downloadJamTrack.init() + } + else { + logger.debug("OpenJamTrack dialog closed with no selection; ignoring", data) + } + }) return false; } @@ -2497,7 +2625,7 @@ if(sessionModel.recordedTracks()) { closeRecording(); } - else if(sessionModel.jamTracks()) { + else if(sessionModel.jamTracks() || downloadJamTrack) { closeJamTrack(); } else if(sessionModel.backingTrack() && sessionModel.backingTrack().path) { @@ -2533,6 +2661,20 @@ } function closeJamTrack() { + + logger.debug("closing recording"); + + if(downloadJamTrack) { + logger.debug("closing DownloadJamTrack widget") + downloadJamTrack.root.remove(); + downloadJamTrack.destroy(); + downloadJamTrack = null; + + // this is necessary because a syncing widget means no jamtracks are loaded; + // so removing the widget will not cause a backend media change event (and so renderSession will not be called, ultimately) + resetOtherAudioContent(); + } + rest.closeJamTrack({id: sessionModel.id()}) .done(function() { sessionModel.refreshCurrentSession(true); @@ -2567,6 +2709,8 @@ } function closeRecording() { + logger.debug("closing recording"); + rest.stopPlayClaimedRecording({id: sessionModel.id(), claimed_recording_id: sessionModel.getCurrentSession().claimed_recording.id}) .done(function(response) { //sessionModel.refreshCurrentSession(true); @@ -2598,10 +2742,28 @@ function onChangePlayPosition(e, data){ logger.debug("calling jamClient.SessionTrackSeekMs(" + data.positionMs + ")"); + + if(data.jamTrackMode) { + context.jamClient.SessionJamTrackSeekMs(data.positionMs); + } + else { context.jamClient.SessionTrackSeekMs(data.positionMs); + } } function startStopRecording() { + + // check first if a jamtrack is loaded, and playing; if so, tell user to stop the play + if(sessionModel.jamTracks() && context.jamClient.isSessionTrackPlaying()) { + app.notify( + { title: "Can't Recording a Play JamTrack", + text: "Stop the JamTrack before trying to recording." }, + null, + true); + + return; + } + if(sessionModel.recordingModel.isRecording()) { sessionModel.recordingModel.stopRecording(); } @@ -2720,6 +2882,9 @@ $screen = $('#session-screen'); $mixModeDropdown = $screen.find('select.monitor-mode') $templateMixerModeChange = $('#template-mixer-mode-change'); + $otherAudioContainer = $('#session-recordedtracks-container'); + $myTracksContainer = $('#session-mytracks-container') + $liveTracksContainer = $('#session-livetracks-container'); $closePlaybackRecording = $('#close-playback-recording') $openBackingTrack = $('#open-a-backingtrack'); events(); diff --git a/web/app/assets/javascripts/sessionModel.js b/web/app/assets/javascripts/sessionModel.js index 7487dec2d..f1d7988ec 100644 --- a/web/app/assets/javascripts/sessionModel.js +++ b/web/app/assets/javascripts/sessionModel.js @@ -37,6 +37,7 @@ var previousBackingTracks = []; var openBackingTrack = null; var shownAudioMediaMixerHelp = false; + var controlsLockedForJamTrackRecording = false; var mixerMode = MIX_MODES.PERSONAL; @@ -113,6 +114,11 @@ } } + // did I open up the current JamTrack? + function selfOpenedJamTracks() { + return currentSession && (currentSession.jam_track_initiator_id == context.JK.currentUserId) + } + function backingTrack() { if(currentSession) { // TODO: objectize this for VRFS-2665, VRFS-2666, VRFS-2667, VRFS-2668 @@ -152,6 +158,18 @@ return inSession; } + function lockControlsforJamTrackRecording() { + controlsLockedForJamTrackRecording = true; + } + + function unlockControlsforJamTrackRecording() { + controlsLockedForJamTrackRecording = false; + } + + function areControlsLockedForJamTrackRecording() { + return controlsLockedForJamTrackRecording; + } + function onMixerModeChanged(newMixerMode) { mixerMode = newMixerMode; @@ -352,6 +370,7 @@ previousBackingTracks = [] openBackingTrack = null shownAudioMediaMixerHelp = false + controlsLockedForJamTrackRecording = false; } // you should only update currentSession with this function @@ -830,6 +849,10 @@ this.isMasterMixMode = isMasterMixMode; this.isPersonalMixMode = isPersonalMixMode; this.getMixMode = getMixMode; + this.selfOpenedJamTracks = selfOpenedJamTracks; + this.areControlsLockedForJamTrackRecording = areControlsLockedForJamTrackRecording; + this.lockControlsforJamTrackRecording = lockControlsforJamTrackRecording; + this.unlockControlsforJamTrackRecording = unlockControlsforJamTrackRecording; // ALERT HANDLERS this.onBackendMixerChanged = onBackendMixerChanged; diff --git a/web/app/assets/javascripts/subscription_utils.js.coffee b/web/app/assets/javascripts/subscription_utils.js.coffee index afd7852ab..b9b3e7823 100644 --- a/web/app/assets/javascripts/subscription_utils.js.coffee +++ b/web/app/assets/javascripts/subscription_utils.js.coffee @@ -84,6 +84,7 @@ class SubscriptionUtils # call subscribe, and use the returned object to listen for events of name context.JK.EVENTS.SUBSCRIBE_NOTIFICATION subscribe: (type, id) => + id = id.toString() key = this.genKey(type, id) @logger.debug("subscribing for any notifications for #{key}") @@ -104,6 +105,7 @@ class SubscriptionUtils # TODO: this should not send a unsubscribe message to the server it's the last listener for the specific type/id combo unsubscribe: (type, id) => + id = id.toString() key = this.genKey(type, id) @logger.debug("unsubscribing for any notifications for #{key}") diff --git a/web/app/assets/javascripts/utils.js b/web/app/assets/javascripts/utils.js index 1b6c1a575..fef57ccf5 100644 --- a/web/app/assets/javascripts/utils.js +++ b/web/app/assets/javascripts/utils.js @@ -910,6 +910,10 @@ return null; } + context.JK.makeAbsolute = function(path) { + return window.location.protocol + '//' + window.location.host + path; + } + context.JK.popExternalLinks = function ($parent) { if(!$parent) $parent = $('body'); diff --git a/web/app/assets/stylesheets/client/checkout.css.scss b/web/app/assets/stylesheets/client/checkout.css.scss index 6f9bc6a40..efe918e22 100644 --- a/web/app/assets/stylesheets/client/checkout.css.scss +++ b/web/app/assets/stylesheets/client/checkout.css.scss @@ -168,6 +168,40 @@ .thanks-panel { padding: 30px; + + span.notice { + font-style:italic; + font-size:12px; + } + + br.purchase-downloads { + clear:both; + margin-bottom:20px; + } + + .thanks-detail.purchased-jam-track { + + margin-top:20px; + + .purchased-jam-track-header { + font-size: 15px; + margin-bottom:10px; + } + + ul.purchased-list { + float:left; + margin:20px 100px 0 20px; + + li { + margin:0; + } + } + + .download-jamtrack { + width:auto; + vertical-align: middle; // to make bullets mid-align when error shows + } + } } .order-panel { diff --git a/web/app/assets/stylesheets/client/client.css b/web/app/assets/stylesheets/client/client.css index 8403cf75a..9e1899ce3 100644 --- a/web/app/assets/stylesheets/client/client.css +++ b/web/app/assets/stylesheets/client/client.css @@ -66,4 +66,5 @@ *= require jquery.Jcrop *= require icheck/minimal/minimal *= require users/syncViewer + *= require ./downloadJamTrack */ \ No newline at end of file diff --git a/web/app/assets/stylesheets/client/downloadJamTrack.css.scss b/web/app/assets/stylesheets/client/downloadJamTrack.css.scss index 1cfe42004..27eb232a0 100644 --- a/web/app/assets/stylesheets/client/downloadJamTrack.css.scss +++ b/web/app/assets/stylesheets/client/downloadJamTrack.css.scss @@ -1,5 +1,77 @@ @import "client/common"; .download-jamtrack { + display:inline-block; + width:100%; + .retry-button { + margin-top:20px; + } + + .retry { + margin-top:10px; + } + + .msg { + white-space: normal; + } + + .spinner-large { + margin:20px auto 0; + text-align:center; + } + + .small { + .state { + text-align:left; + } + font-size:inherit; + .msg { + line-height: 32px; + height: 32px; + display: inline-block; + vertical-align: middle; + } + .spinner-small { + display:inline-block; + vertical-align:middle; + } + } + + .large { + .state { + text-align:center; + } + } + + &.active { + + .small { + margin-bottom:5px; + + .msg { + font-weight:bold; + color:white; + display:inline; + } + .errormsg { + display:block; + font-size:14px; + } + .retry { + display:block; + margin:3px 0 0 0; + font-size:14px; + } + .retry-button { + float:right; + margin:5px 0 5px 20px; + } + + .msg-holder { + display:block; + } + } + + } } \ No newline at end of file diff --git a/web/app/assets/stylesheets/client/session.css.scss b/web/app/assets/stylesheets/client/session.css.scss index fc6f8ad17..7f38661c6 100644 --- a/web/app/assets/stylesheets/client/session.css.scss +++ b/web/app/assets/stylesheets/client/session.css.scss @@ -148,6 +148,17 @@ } } + .session-recording-name { + width:60%; + overflow:hidden; + margin-top:9px; + margin-bottom:8px; + font-size:16px; + } + + .download-jamtrack { + margin-top:50px; + } } diff --git a/web/app/controllers/api_jam_tracks_controller.rb b/web/app/controllers/api_jam_tracks_controller.rb index 567d0b7ad..03a80a51a 100644 --- a/web/app/controllers/api_jam_tracks_controller.rb +++ b/web/app/controllers/api_jam_tracks_controller.rb @@ -58,14 +58,22 @@ class ApiJamTracksController < ApiController end def keys - jamtrack_ids = params[:jamtracks] - - unless jamtrack_ids.kind_of?(Array) - render :json => {message: 'jamtracks parameter must be an array'}, :status => 200 + jamtrack_holder = params[:jamtracks] + + unless jamtrack_holder.kind_of?(Hash) + render :json => {message: 'jamtracks parameter must be an hash'}, :status => 422 return end - @jam_tracks = JamTrackRight.list_keys(current_user, params[:jamtracks]) + jamtrack_ids = jamtrack_holder[:tracks] + + unless jamtrack_ids.kind_of?(Array) + render :json => {message: 'jamtracks:tracks parameter must be an array'}, :status => 422 + return + end + + @jam_tracks = JamTrackRight.list_keys(current_user, jamtrack_ids) + render "api_jam_tracks/list_keys", :layout => nil end diff --git a/web/app/controllers/api_recordings_controller.rb b/web/app/controllers/api_recordings_controller.rb index 97ac23a22..b6f246e72 100644 --- a/web/app/controllers/api_recordings_controller.rb +++ b/web/app/controllers/api_recordings_controller.rb @@ -1,7 +1,7 @@ class ApiRecordingsController < ApiController before_filter :api_signed_in_user, :except => [ :add_like ] - before_filter :lookup_recording, :only => [ :show, :stop, :claim, :discard, :keep, :delete_claim ] + before_filter :lookup_recording, :only => [ :show, :stop, :claim, :discard, :keep, :delete_claim, :add_timeline_data ] before_filter :lookup_recorded_track, :only => [ :download, :upload_next_part, :upload_sign, :upload_part_complete, :upload_complete ] before_filter :lookup_recorded_backing_track, :only => [ :backing_track_download, :backing_track_upload_next_part, :backing_track_upload_sign, :backing_track_upload_part_complete, :backing_track_upload_complete ] before_filter :lookup_recorded_video, :only => [ :video_upload_sign, :video_upload_start, :video_upload_complete ] @@ -377,6 +377,13 @@ class ApiRecordingsController < ApiController end end + # metadata + def add_timeline + @recording.add_timeline(params[:metadata]) + + render :json => {}, :status => 200 + end + private def lookup_recording diff --git a/web/app/controllers/api_recurly_controller.rb b/web/app/controllers/api_recurly_controller.rb index cd0aa4b55..28d16933b 100644 --- a/web/app/controllers/api_recurly_controller.rb +++ b/web/app/controllers/api_recurly_controller.rb @@ -57,20 +57,36 @@ class ApiRecurlyController < ApiController def place_order error=nil puts "PLACING ORDER #{params.inspect}" + response = {jam_tracks:[]} + + # 1st confirm that all specified JamTracks exist + jam_tracks = [] + params[:jam_tracks].each do |jam_track_id| jam_track = JamTrack.where("id=?", jam_track_id).first if jam_track - @client.place_order(current_user, jam_track) + jam_tracks << jam_track else error="JamTrack not found for '#{jam_track_id}'" break end end + # then buy each + unless error + jam_tracks.each do |jam_track| + jam_track_right = @client.place_order(current_user, jam_track) + # build up the response object with JamTracks that were purchased. + # if this gets more complicated, we should switch to RABL + response[:jam_tracks] << {name: jam_track.name, id: jam_track.id, jam_track_right_id: jam_track_right.id} + end + end + + if error render json: { errors: {message:error}}, :status => 404 else - render :json=>{}, :status=>200 + render :json=>response, :status=>200 end rescue RecurlyClientError => x render json: { message: x.inspect, errors: x.errors}, :status => 404 diff --git a/web/app/controllers/spikes_controller.rb b/web/app/controllers/spikes_controller.rb index 4bfc90e54..ab6da676d 100644 --- a/web/app/controllers/spikes_controller.rb +++ b/web/app/controllers/spikes_controller.rb @@ -33,11 +33,26 @@ class SpikesController < ApplicationController def subscription + #Notification.send_reload(MessageFactory::ALL_NATIVE_CLIENTS) + Notification.send_subscription_message('test', '1', '{"msg": "oh hai 1"}') Notification.send_subscription_message('test', '2', '{"msg": "oh hai 2"}') render text: 'oh hai' end + def download_jam_track + + jamTrack = JamTrack.find(params[:jam_track_id]) + jamTrackRight = jamTrack.right_for_user(current_user) + + gon.jamTrackId = jamTrack.id + gon.jamTrackRightId = jamTrackRight.id + gon.size = params[:size] ? params[:size] : 'large' + gon.switchState = params[:state] + + render :layout => 'web' + end + def site_validate render :layout => 'web' end @@ -45,5 +60,4 @@ class SpikesController < ApplicationController def recording_source render :layout => 'web' end - end diff --git a/web/app/views/api_claimed_recordings/show.rabl b/web/app/views/api_claimed_recordings/show.rabl index aa7de2609..91174f87b 100644 --- a/web/app/views/api_claimed_recordings/show.rabl +++ b/web/app/views/api_claimed_recordings/show.rabl @@ -20,8 +20,16 @@ node :mix do |claimed_recording| end child(:recording => :recording) { - attributes :id, :created_at, :duration, :comment_count, :like_count, :play_count - + attributes :id, :created_at, :duration, :comment_count, :like_count, :play_count, :jam_track_id, :jam_track_initiator_id + + child(:jam_track => :jam_track) { + attributes :id + + node :jmep do |jam_track| + jam_track.jmep_json ? JSON.parse(jam_track.jmep_json) : nil + end + } + child(:band => :band) { attributes :id, :name, :location, :photo_url } @@ -50,6 +58,15 @@ child(:recording => :recording) { end } + child(:recorded_jam_track_tracks => :recorded_jam_track_tracks) { + node do |recorded_jam_track_track| + { + id: recorded_jam_track_track.jam_track_track.id, + timeline: recorded_jam_track_track.timeline ? JSON.parse(recorded_jam_track_track.timeline) : [] + } + end + } + child(:comments => :comments) { attributes :comment, :created_at diff --git a/web/app/views/api_jam_tracks/list_keys.rabl b/web/app/views/api_jam_tracks/list_keys.rabl index 43a33b24e..f82aa5650 100644 --- a/web/app/views/api_jam_tracks/list_keys.rabl +++ b/web/app/views/api_jam_tracks/list_keys.rabl @@ -2,7 +2,7 @@ object @jam_tracks node do |jam_track| { - id: jam_track['id'], + id: jam_track['id'].to_s, private: jam_track['private_key'], error: jam_track['private_key'] ? nil : ( jam_track['jam_track_right_id'] ? 'no_key' : 'not_purchased' ) } diff --git a/web/app/views/api_jam_tracks/show.rabl b/web/app/views/api_jam_tracks/show.rabl index f9ea856db..cab264aa5 100644 --- a/web/app/views/api_jam_tracks/show.rabl +++ b/web/app/views/api_jam_tracks/show.rabl @@ -1,6 +1,6 @@ object @jam_track -attributes :id, :name, :description, :recording_type, :original_artist, :songwriter, :publisher, :sales_region, :price +attributes :id, :name, :description, :recording_type, :original_artist, :songwriter, :publisher, :sales_region, :price, :version node :genres do |item| [item.genre.description] # XXX: need to return single genre; not array 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 4e708a4ac..364b13031 100644 --- a/web/app/views/api_jam_tracks/show_for_client.rabl +++ b/web/app/views/api_jam_tracks/show_for_client.rabl @@ -1,6 +1,14 @@ object @jam_track -attributes :id, :name, :description, :initial_play_silence, :original_artist +attributes :id, :name, :description, :initial_play_silence, :original_artist, :version + +node :jmep do |jam_track| + jam_track.jmep_json ? JSON.parse(jam_track.jmep_json) : nil +end + +node :jam_track_right_id do |jam_track| + jam_track.right_for_user(current_user).id +end child(:jam_track_tracks => :tracks) { attributes :id, :part, :instrument diff --git a/web/app/views/api_music_sessions/show.rabl b/web/app/views/api_music_sessions/show.rabl index cd277c3fe..7c1b0c8de 100644 --- a/web/app/views/api_music_sessions/show.rabl +++ b/web/app/views/api_music_sessions/show.rabl @@ -13,7 +13,7 @@ if !current_user } else - attributes :id, :name, :description, :musician_access, :approval_required, :fan_access, :fan_chat, :band_id, :user_id, :claimed_recording_initiator_id, :track_changes_counter, :max_score, :backing_track_path, :metronome_active + attributes :id, :name, :description, :musician_access, :approval_required, :fan_access, :fan_chat, :band_id, :user_id, :claimed_recording_initiator_id, :track_changes_counter, :max_score, :backing_track_path, :metronome_active, :jam_track_initiator_id node :can_join do |session| session.can_join?(current_user, true) diff --git a/web/app/views/api_recordings/show.rabl b/web/app/views/api_recordings/show.rabl index f3c817b07..a717a6bfb 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? +attributes :id, :band, :created_at, :duration, :comment_count, :like_count, :play_count, :when_will_be_discarded?, :jam_track_id, :jam_track_initiator_id node :mix do |recording| if recording.mix @@ -12,6 +12,13 @@ node :mix do |recording| end end +child(:jam_track => :jam_track) { + attributes :id + + node :jmep do |jam_track| + jam_track.jmep_json ? JSON.parse(jam_track.jmep_json) : nil + end +} child(:band => :band) { attributes :id, :name, :location, :photo_url @@ -33,6 +40,15 @@ child(:recorded_backing_tracks => :recorded_backing_tracks) { end } +child(:recorded_jam_track_tracks => :recorded_jam_track_tracks) { + node do |recorded_jam_track_track| + { + id: recorded_jam_track_track.jam_track_track.id, + timeline: recorded_jam_track_track.timeline ? JSON.parse(recorded_jam_track_track.timeline) : [] + } + end +} + child(:comments => :comments) { attributes :comment, :created_at diff --git a/web/app/views/clients/_download_jamtrack_templates.html.slim b/web/app/views/clients/_download_jamtrack_templates.html.slim index bf5831cb4..fef7f2cd8 100644 --- a/web/app/views/clients/_download_jamtrack_templates.html.slim +++ b/web/app/views/clients/_download_jamtrack_templates.html.slim @@ -1,2 +1,86 @@ script type="text/template" id='template-download-jamtrack' .download-jamtrack + .state + +script type="text/template" id="template-download-jamtrack-state-no-client" + .state-no-client + .large.hidden + .msg + | To play your JamTrack, launch the JamKazam application and open the JamTrack while in a session. + .small.hidden + .msg + | {{data.name}} (launch client) + +script type="text/template" id="template-download-jamtrack-state-synchronized" + .state-synchronized + .large.hidden + .msg + | Your JamTrack is on your system and ready to play. + .small.hidden + .msg + | {{data.name}} (done) + +script type="text/template" id="template-download-jamtrack-state-packaging" + .state-packaging + .large.hidden + .msg + | Your JamTrack is currently being created on the JamKazam server. + .spinner-large + .small.hidden + .msg + | {{data.name}} (packaging) + .spinner-small + +script type="text/template" id="template-download-jamtrack-state-downloading" + .state-downloading + .large.hidden + .msg + | Your JamTrack is currently being downloaded. + .spinner-large + .small.hidden + .msg + | {{data.name}} (downloading) + .spinner-small + +script type="text/template" id="template-download-jamtrack-state-keying" + .state-keying + .large.hidden + .msg + | Your JamTrack is being authenticated. + .spinner-large + .small.hidden + .msg + | {{data.name}} (keying) + .spinner-small + +script type="text/template" id="template-download-jamtrack-state-initial" + .state-initial + .large.hidden + .msg + | Initializing JamTrack... + .spinner-large + .small.hidden + .msg + | {{data.name}} (initializing) + .spinner-small + +script type="text/template" id="template-download-jamtrack-state-quiet" + .state-quiet + .large.hidden + .msg + .small.hidden + .msg + | {{data.name}} (pending) + +script type="text/template" id="template-download-jamtrack-state-errored" + .state-errored + .large.hidden + .msg + .retry + a.button-orange.retry-button RETRY + .small.hidden + .msg-holder + .msg + a.button-orange.retry-button RETRY + .errormsg + .retry diff --git a/web/app/views/clients/_help.html.slim b/web/app/views/clients/_help.html.slim index 7fe11020b..60646b6ec 100644 --- a/web/app/views/clients/_help.html.slim +++ b/web/app/views/clients/_help.html.slim @@ -199,6 +199,9 @@ script type="text/template" id="template-help-media-controls-disabled" | Only the person who opened the recording can control the volume levels. | {% } %} +script type="text/template" id="template-help-jamtrack-controls-disabled" + | During a recording, volume and mute controls for JamTracks are disabled. So, get the session volume levels right before starting the recording. + script type="text/template" id="template-help-volume-media-mixers" | Audio files only expose both master and personal mix controls, so any change here will also affect everyone in the session. diff --git a/web/app/views/clients/_order.html.slim b/web/app/views/clients/_order.html.slim index 241af7217..ffab91d7a 100644 --- a/web/app/views/clients/_order.html.slim +++ b/web/app/views/clients/_order.html.slim @@ -178,8 +178,15 @@ div layout="screen" layout-id="order" id="orderScreen" class="screen secondary" br .thanks-detail We'll send you an email confirming your order shortly. br - .thanks-detail If you purchased any JamTracks, the next time you run the JamKazam application, your JamTracks will automatically be downloaded to the app, and you will receive a notification when the download is complete. - + .thanks-detail.jam-tracks-in-browser.hidden + | To play your purchased JamTrack, launch the JamKazam application and open the JamTrack while in a session. + .thanks-detail.purchased-jam-track.hidden + h2.purchased-jam-track-header Downloading Your Purchased JamTracks + span Each JamTrack will be downloaded sequentially. + br + span.notice Note that you do not have to wait for this to complete in order to use your JamTrack later. + br.clear + ul.purchased-list @@ -276,4 +283,7 @@ script type='text/template' id='template-order-content' span and ' a href="http://www.jamkazam.com/corp/returns" returns policy - span . \ No newline at end of file + span . + +script type='text/template' id='template-purchased-jam-track' + li data-jam-track-id="{{data.jam_track_id}}" \ No newline at end of file diff --git a/web/app/views/clients/_session.html.slim b/web/app/views/clients/_session.html.slim index 7a79b81fd..49a5b2cb0 100644 --- a/web/app/views/clients/_session.html.slim +++ b/web/app/views/clients/_session.html.slim @@ -71,7 +71,7 @@ | other audio .session-recording-name-wrapper .session-recording-name.left - | (No recording loaded) + | (No audio loaded) .session-add.right a#close-playback-recording[href="#"] = image_tag "content/icon_close.png", {:width => 18, :height => 20, :align => "texttop"} diff --git a/web/app/views/clients/index.html.erb b/web/app/views/clients/index.html.erb index 94af76504..7586d0e9b 100644 --- a/web/app/views/clients/index.html.erb +++ b/web/app/views/clients/index.html.erb @@ -64,6 +64,7 @@ <%= render "overlay_small" %> <%= render "listenBroadcast" %> <%= render "sync_viewer_templates" %> +<%= render "download_jamtrack_templates" %> <%= render "help" %> <%= render 'dialogs/dialogs' %>
diff --git a/web/app/views/dialogs/_openJamTrackDialog.html.slim b/web/app/views/dialogs/_openJamTrackDialog.html.slim index 18ca215a7..2e5ec5f7c 100644 --- a/web/app/views/dialogs/_openJamTrackDialog.html.slim +++ b/web/app/views/dialogs/_openJamTrackDialog.html.slim @@ -31,7 +31,7 @@ a href='/client#/jamtrack' rel="external" | Shop for JamTracks .right - a href="#" class="button-grey" layout-action="close" + a href="#" class="button-grey" layout-action="cancel" | CANCEL diff --git a/web/app/views/spikes/download_jam_track.html.slim b/web/app/views/spikes/download_jam_track.html.slim new file mode 100644 index 000000000..ade3744b5 --- /dev/null +++ b/web/app/views/spikes/download_jam_track.html.slim @@ -0,0 +1,44 @@ += javascript_include_tag "download_jamtrack" += render "clients/download_jamtrack_templates" += stylesheet_link_tag "client/downloadJamTrack" + +- provide(:title, 'Download Jam Track Widget') + +.content-wrapper + h2 Jam Track State Widget + + h3 Possible States + ul + li synchronized + li no_client + li packaging + li downloading + li keying + li initial + li errored + #widget + +javascript: + var initialized = false; + $(document).on('JAMKAZAM_READY', function(e, data) { + window.JK.JamServer.get$Server().on(window.JK.EVENTS.CONNECTION_UP, function() { + if(initialized) { + return; + } + initialized = true + + setTimeout(function() { + window.downloadJamTrack = new JK.DownloadJamTrack(data.app, {id: gon.jamTrackId, jam_track_right_id: gon.jamTrackRightId, name: 'Back in Black'}, gon.size) + downloadJamTrack.init() + $('#widget').append(window.downloadJamTrack.root) + + if (gon.switchState == 'errored') { + downloadJamTrack.transitionError("package-error", "The server failed to create your package.") + } + else if (gon.switchState) { + downloadJamTrack.transition(downloadJamTrack.states[gon.switchState]); + } + }, 1) + + }) + }) \ No newline at end of file diff --git a/web/config/application.rb b/web/config/application.rb index 687b93ff4..2ec535647 100644 --- a/web/config/application.rb +++ b/web/config/application.rb @@ -209,6 +209,8 @@ if defined?(Bundler) # Location of jamtracks python tool: config.jamtracks_dir = ENV['JAMTRACKS_DIR'] || File.expand_path(File.join(File.dirname(__FILE__), "..", "..", "..", "jamtracks")) + config.jmep_dir = ENV['JMEP_DIR'] || File.expand_path(File.join(File.dirname(__FILE__), "..", "..", "..", "jmep")) + # amount of time before we think packaging job is broken config.signing_job_run_max_time = 60 # 1 minute # amount of time before we think the queue is stuck @@ -310,6 +312,7 @@ if defined?(Bundler) config.influxdb_port = 8086 config.influxdb_ignored_environments = ENV["INFLUXDB_ENABLED"] == '1' ? ['test', 'cucumber'] : ['test', 'cucumber', 'development'] + config.allow_spikes = false config.show_jamblaster_notice = true config.show_jamblaster_kickstarter_link = true config.metronome_available = true diff --git a/web/config/routes.rb b/web/config/routes.rb index cfefcc260..00fd25f38 100644 --- a/web/config/routes.rb +++ b/web/config/routes.rb @@ -81,21 +81,20 @@ SampleApp::Application.routes.draw do match '/endorse/:id/:service', to: 'users#endorse', :as => 'endorse' - # temporarily allow for debugging--only allows admini n - match '/listen_in', to: 'spikes#listen_in' - # embed resque-web if this is development mode - if Rails.env == "development" + if Rails.env == "development" || Rails.application.config.allow_spikes require 'resque/server' require 'resque-retry' require 'resque-retry/server' mount Resque::Server.new, :at => "/resque" if Rails.env == "development" # route to spike controller (proof-of-concepts) + match '/listen_in', to: 'spikes#listen_in' match '/facebook_invite', to: 'spikes#facebook_invite' match '/launch_app', to: 'spikes#launch_app' match '/websocket', to: 'spikes#websocket' match '/test_subscription', to: 'spikes#subscription' + match '/widgets/download_jam_track', to: 'spikes#download_jam_track' match '/site_validate', to: 'spikes#site_validate' match '/recording_source', to: 'spikes#recording_source' @@ -210,7 +209,7 @@ SampleApp::Application.routes.draw do match '/jamtracks/downloads' => 'api_jam_tracks#downloads', :via => :get, :as => 'api_jam_tracks_downloads' match '/jamtracks/download/:id' => 'api_jam_tracks#download', :via => :get, :as => 'api_jam_tracks_download' match '/jamtracks/enqueue/:id' => 'api_jam_tracks#enqueue', :via => :post, :as => 'api_jam_tracks_enqueue' - match '/jamtracks/show/:id' => 'api_jam_tracks#show_jam_track_right', :via => :get, :as => 'api_jam_tracks_show_right' + 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' # Shopping carts @@ -452,6 +451,7 @@ SampleApp::Application.routes.draw do match '/recordings/:id/comments' => 'api_recordings#add_comment', :via => :post, :as => 'api_recordings_add_comment' match '/recordings/:id/likes' => 'api_recordings#add_like', :via => :post, :as => 'api_recordings_add_like' match '/recordings/:id/discard' => 'api_recordings#discard', :via => :post, :as => 'api_recordings_discard' + match '/recordings/:id/timeline' => 'api_recordings#add_timeline', :via => :post, :as => 'api_recordings_timeline' # Recordings - recorded_tracks match '/recordings/:id/tracks/:track_id' => 'api_recordings#show_recorded_track', :via => :get, :as => 'api_recordings_show_recorded_track' diff --git a/web/lib/tasks/sample_data.rake b/web/lib/tasks/sample_data.rake index 853194455..c19d46dea 100644 --- a/web/lib/tasks/sample_data.rake +++ b/web/lib/tasks/sample_data.rake @@ -1,6 +1,6 @@ require 'factory_girl' require 'open-uri' - +#require './spec/factories.rb' # useful when run on a server namespace :db do desc "Add a simple one track recording to the database" diff --git a/web/spec/controllers/api_jam_tracks_controller_spec.rb b/web/spec/controllers/api_jam_tracks_controller_spec.rb index 1ff6058df..5ec41f318 100644 --- a/web/spec/controllers/api_jam_tracks_controller_spec.rb +++ b/web/spec/controllers/api_jam_tracks_controller_spec.rb @@ -102,6 +102,7 @@ describe ApiJamTracksController do response.should be_success json = JSON.parse(response.body) json['jamtracks'].length.should eq(1) + json['jamtracks'][0]['jam_track_right_id'].should eq(right.id) json['next'].should be_nil end end @@ -196,18 +197,18 @@ describe ApiJamTracksController do describe "keys" do it "empty" do - get :keys, jamtracks: [] + get :keys, jamtracks: {tracks: []} response.status.should == 200 json = JSON.parse(response.body) json.length == 0 end it "track with no rights" do - get :keys, jamtracks: [@jam_track.id] + get :keys, jamtracks: { tracks: [@jam_track.id] } response.status.should == 200 json = JSON.parse(response.body) json.length.should == 1 - json[0]['id'].should == @jam_track.id + json[0]['id'].should == @jam_track.id.to_s json[0]['private'].should be_nil json[0]['error'].should == 'not_purchased' end @@ -216,32 +217,32 @@ describe ApiJamTracksController do right = FactoryGirl.create(:jam_track_right, user: @user, private_key: nil, jam_track: @jam_track) - get :keys, jamtracks: [@jam_track.id] + get :keys, jamtracks: { tracks: [@jam_track.id] } response.status.should == 200 json = JSON.parse(response.body) json.length.should == 1 - json[0]['id'].should == @jam_track.id + json[0]['id'].should == @jam_track.id.to_s json[0]['private'].should be_nil json[0]['error'].should == 'no_key' end it "track with key" do right = FactoryGirl.create(:jam_track_right, user: @user, private_key: 'abc', jam_track: @jam_track) - get :keys, jamtracks: [@jam_track.id] + get :keys, jamtracks: { tracks: [@jam_track.id] } response.status.should == 200 json = JSON.parse(response.body) json.length.should == 1 - json[0]['id'].should == @jam_track.id + json[0]['id'].should == @jam_track.id.to_s json[0]['private'].should eq('abc') json[0]['error'].should be_nil end it "non-owning user asking for a real track" do right = FactoryGirl.create(:jam_track_right, user: FactoryGirl.create(:user), private_key: 'abc', jam_track: @jam_track) - get :keys, jamtracks: [@jam_track.id] + get :keys, jamtracks: { tracks: [@jam_track.id] } response.status.should == 200 json = JSON.parse(response.body) - json[0]['id'].should == @jam_track.id + json[0]['id'].should == @jam_track.id.to_s json[0]['private'].should be_nil json[0]['error'].should == 'not_purchased' end diff --git a/web/spec/controllers/api_recordings_controller_spec.rb b/web/spec/controllers/api_recordings_controller_spec.rb index f09a6130d..ac54f6272 100644 --- a/web/spec/controllers/api_recordings_controller_spec.rb +++ b/web/spec/controllers/api_recordings_controller_spec.rb @@ -3,217 +3,251 @@ require 'spec_helper' describe ApiRecordingsController do render_views + describe "recording with backing track" do - before(:each) do - @user = FactoryGirl.create(:user) - @instrument = FactoryGirl.create(:instrument, :description => 'a great instrument') - @music_session = FactoryGirl.create(:active_music_session, :creator => @user, :musician_access => true) - @connection = FactoryGirl.create(:connection, :user => @user, :music_session => @music_session) - @track = FactoryGirl.create(:track, :connection => @connection, :instrument => @instrument) - @backing_track = FactoryGirl.create(:backing_track, :connection => @connection) - controller.current_user = @user - end - - describe "start" do - it "should work" do - post :start, { :format => 'json', :music_session_id => @music_session.id } - response.should be_success - response_body = JSON.parse(response.body) - response_body['id'].should_not be_nil - recording = Recording.find(response_body['id']) + before(:each) do + @user = FactoryGirl.create(:user) + @instrument = FactoryGirl.create(:instrument, :description => 'a great instrument') + @music_session = FactoryGirl.create(:active_music_session, :creator => @user, :musician_access => true) + @connection = FactoryGirl.create(:connection, :user => @user, :music_session => @music_session) + @track = FactoryGirl.create(:track, :connection => @connection, :instrument => @instrument) + @backing_track = FactoryGirl.create(:backing_track, :connection => @connection) + controller.current_user = @user end - it "should not allow multiple starts" do - post :start, { :format => 'json', :music_session_id => @music_session.id } - post :start, { :format => 'json', :music_session_id => @music_session.id } - response.status.should == 422 - response_body = JSON.parse(response.body) - response_body["errors"]["music_session"][0].should == ValidationMessages::ALREADY_BEING_RECORDED + describe "start" do + it "should work" do + post :start, {:format => 'json', :music_session_id => @music_session.id} + response.should be_success + response_body = JSON.parse(response.body) + response_body['id'].should_not be_nil + recording = Recording.find(response_body['id']) + end + + it "should not allow multiple starts" do + post :start, {:format => 'json', :music_session_id => @music_session.id} + post :start, {:format => 'json', :music_session_id => @music_session.id} + response.status.should == 422 + response_body = JSON.parse(response.body) + response_body["errors"]["music_session"][0].should == ValidationMessages::ALREADY_BEING_RECORDED + end + + it "should not allow start while playback ongoing" do + recording = Recording.start(@music_session, @user) + recording.stop + recording.reload + claimed_recording = recording.claim(@user, "name", "description", Genre.first, true) + @music_session.claimed_recording_start(@user, claimed_recording) + @music_session.errors.any?.should be_false + post :start, {:format => 'json', :music_session_id => @music_session.id} + response.status.should == 422 + response_body = JSON.parse(response.body) + response_body["errors"]["music_session"][0].should == ValidationMessages::ALREADY_PLAYBACK_RECORDING + end + + it "should not allow start by somebody not in the music session" do + user2 = FactoryGirl.create(:user) + controller.current_user = user2 + post :start, {:format => 'json', :music_session_id => @music_session.id} + response.status.should == 403 + end end - it "should not allow start while playback ongoing" do - recording = Recording.start(@music_session, @user) - recording.stop - recording.reload - claimed_recording = recording.claim(@user, "name", "description", Genre.first, true) - @music_session.claimed_recording_start(@user, claimed_recording) - @music_session.errors.any?.should be_false - post :start, { :format => 'json', :music_session_id => @music_session.id } - response.status.should == 422 - response_body = JSON.parse(response.body) - response_body["errors"]["music_session"][0].should == ValidationMessages::ALREADY_PLAYBACK_RECORDING + describe "get" do + it "should work" do + post :start, {:format => 'json', :music_session_id => @music_session.id} + response.should be_success + response_body = JSON.parse(response.body) + response_body['id'].should_not be_nil + recordingId = response_body['id'] + get :show, {:format => 'json', :id => recordingId} + response.should be_success + response_body = JSON.parse(response.body) + response_body['id'].should == recordingId + end + end - it "should not allow start by somebody not in the music session" do - user2 = FactoryGirl.create(:user) - controller.current_user = user2 - post :start, { :format => 'json', :music_session_id => @music_session.id } - response.status.should == 403 + describe "stop" do + it "should work" do + post :start, {:format => 'json', :music_session_id => @music_session.id} + response_body = JSON.parse(response.body) + recording = Recording.find(response_body['id']) + post :stop, {:format => 'json', :id => recording.id} + response.should be_success + response_body = JSON.parse(response.body) + response_body['id'].should_not be_nil + Recording.find(response_body['id']).id.should == recording.id + end + + it "should not allow stop on a session not being recorded" do + post :start, {:format => 'json', :music_session_id => @music_session.id} + response_body = JSON.parse(response.body) + recording = Recording.find(response_body['id']) + post :stop, {:format => 'json', :id => recording.id} + post :stop, {:format => 'json', :id => recording.id} + response.status.should == 422 + response_body = JSON.parse(response.body) + end + + it "should not allow stop on a session requested by a different member" do + + post :start, {:format => 'json', :music_session_id => @music_session.id} + response_body = JSON.parse(response.body) + recording = Recording.find(response_body['id']) + user2 = FactoryGirl.create(:user) + controller.current_user = user2 + post :stop, {:format => 'json', :id => recording.id} + response.status.should == 403 + end + end + + describe "download track" do + let(:mix) { FactoryGirl.create(:mix) } + + it "should only allow a user to download a track if they have claimed the recording" do + post :start, {:format => 'json', :music_session_id => @music_session.id} + response_body = JSON.parse(response.body) + recording = Recording.find(response_body['id']) + post :stop, {:format => 'json', :id => recording.id} + response.should be_success + end + + + it "is possible" do + mix.touch + recorded_track = mix.recording.recorded_tracks[0] + controller.current_user = mix.recording.owner + get :download, {id: recorded_track.recording.id, track_id: recorded_track.client_track_id} + response.status.should == 302 + + recorded_track.reload + recorded_track.download_count.should == 1 + + get :download, {id: recorded_track.recording.id, track_id: recorded_track.client_track_id} + response.status.should == 302 + + recorded_track.reload + recorded_track.download_count.should == 2 + end + + + it "prevents download after limit is reached" do + mix.touch + recorded_track = mix.recording.recorded_tracks[0] + recorded_track.download_count = APP_CONFIG.max_audio_downloads + recorded_track.save! + controller.current_user = recorded_track.user + get :download, {format: 'json', id: recorded_track.recording.id, track_id: recorded_track.client_track_id} + response.status.should == 404 + JSON.parse(response.body, symbolize_names: true)[:message].should == "download limit surpassed" + end + + + it "lets admins surpass limit" do + mix.touch + recorded_track = mix.recording.recorded_tracks[0] + recorded_track.download_count = APP_CONFIG.max_audio_downloads + recorded_track.save! + recorded_track.user.admin = true + recorded_track.user.save! + + controller.current_user = recorded_track.user + get :download, {format: 'json', id: recorded_track.recording.id, track_id: recorded_track.client_track_id} + response.status.should == 302 + recorded_track.reload + recorded_track.download_count.should == 101 + end + end + + describe "download backing track" do + let(:mix) { FactoryGirl.create(:mix) } + + it "should only allow a user to download a track if they have claimed the recording" do + post :start, {:format => 'json', :music_session_id => @music_session.id} + response_body = JSON.parse(response.body) + recording = Recording.find(response_body['id']) + post :stop, {:format => 'json', :id => recording.id} + response.should be_success + end + + + it "is possible" do + mix.touch + recorded_track = mix.recording.recorded_tracks[0] + controller.current_user = mix.recording.owner + get :download, {id: recorded_track.recording.id, track_id: recorded_track.client_track_id} + response.status.should == 302 + + recorded_track.reload + recorded_track.download_count.should == 1 + + get :download, {id: recorded_track.recording.id, track_id: recorded_track.client_track_id} + response.status.should == 302 + + recorded_track.reload + recorded_track.download_count.should == 2 + end + + + it "prevents download after limit is reached" do + mix.touch + recorded_track = mix.recording.recorded_tracks[0] + recorded_track.download_count = APP_CONFIG.max_audio_downloads + recorded_track.save! + controller.current_user = recorded_track.user + get :download, {format: 'json', id: recorded_track.recording.id, track_id: recorded_track.client_track_id} + response.status.should == 404 + JSON.parse(response.body, symbolize_names: true)[:message].should == "download limit surpassed" + end + + + it "lets admins surpass limit" do + mix.touch + recorded_track = mix.recording.recorded_tracks[0] + recorded_track.download_count = APP_CONFIG.max_audio_downloads + recorded_track.save! + recorded_track.user.admin = true + recorded_track.user.save! + + controller.current_user = recorded_track.user + get :download, {format: 'json', id: recorded_track.recording.id, track_id: recorded_track.client_track_id} + response.status.should == 302 + recorded_track.reload + recorded_track.download_count.should == 101 + end end end - describe "get" do - it "should work" do - post :start, { :format => 'json', :music_session_id => @music_session.id } - response.should be_success - response_body = JSON.parse(response.body) - response_body['id'].should_not be_nil - recordingId = response_body['id'] - get :show, {:format => 'json', :id => recordingId} - response.should be_success - response_body = JSON.parse(response.body) - response_body['id'].should == recordingId + describe "recording with jam track" do + + before(:each) do + @user = FactoryGirl.create(:user) + @instrument = FactoryGirl.create(:instrument, :description => 'a great instrument') + @music_session = FactoryGirl.create(:active_music_session, :creator => @user, :musician_access => true) + @connection = FactoryGirl.create(:connection, :user => @user, :music_session => @music_session) + @track = FactoryGirl.create(:track, :connection => @connection, :instrument => @instrument) + @jam_track = FactoryGirl.create(:jam_track) + + # make sure the jam track is opened + @music_session.jam_track = @jam_track + @music_session.jam_track_initiator = @user + @music_session.save! + controller.current_user = @user end - end - - describe "stop" do - it "should work" do - post :start, { :format => 'json', :music_session_id => @music_session.id } - response_body = JSON.parse(response.body) - recording = Recording.find(response_body['id']) - post :stop, { :format => 'json', :id => recording.id } - response.should be_success - response_body = JSON.parse(response.body) - response_body['id'].should_not be_nil - Recording.find(response_body['id']).id.should == recording.id - end - - it "should not allow stop on a session not being recorded" do - post :start, { :format => 'json', :music_session_id => @music_session.id } - response_body = JSON.parse(response.body) - recording = Recording.find(response_body['id']) - post :stop, { :format => 'json', :id => recording.id } - post :stop, { :format => 'json', :id => recording.id } - response.status.should == 422 - response_body = JSON.parse(response.body) - end - - it "should not allow stop on a session requested by a different member" do - - post :start, { :format => 'json', :music_session_id => @music_session.id } - response_body = JSON.parse(response.body) - recording = Recording.find(response_body['id']) - user2 = FactoryGirl.create(:user) - controller.current_user = user2 - post :stop, { :format => 'json', :id => recording.id } - response.status.should == 403 - end - end - - describe "download track" do - let(:mix) { FactoryGirl.create(:mix) } - - it "should only allow a user to download a track if they have claimed the recording" do - post :start, { :format => 'json', :music_session_id => @music_session.id } - response_body = JSON.parse(response.body) - recording = Recording.find(response_body['id']) - post :stop, { :format => 'json', :id => recording.id } - response.should be_success - end - - - it "is possible" do - mix.touch - recorded_track = mix.recording.recorded_tracks[0] - controller.current_user = mix.recording.owner - get :download, {id: recorded_track.recording.id, track_id: recorded_track.client_track_id} - response.status.should == 302 - - recorded_track.reload - recorded_track.download_count.should == 1 - - get :download, {id: recorded_track.recording.id, track_id: recorded_track.client_track_id} - response.status.should == 302 - - recorded_track.reload - recorded_track.download_count.should == 2 - end - - - it "prevents download after limit is reached" do - mix.touch - recorded_track = mix.recording.recorded_tracks[0] - recorded_track.download_count = APP_CONFIG.max_audio_downloads - recorded_track.save! - controller.current_user = recorded_track.user - get :download, {format:'json', id: recorded_track.recording.id, track_id: recorded_track.client_track_id} - response.status.should == 404 - JSON.parse(response.body, symbolize_names: true)[:message].should == "download limit surpassed" - end - - - it "lets admins surpass limit" do - mix.touch - recorded_track = mix.recording.recorded_tracks[0] - recorded_track.download_count = APP_CONFIG.max_audio_downloads - recorded_track.save! - recorded_track.user.admin = true - recorded_track.user.save! - - controller.current_user = recorded_track.user - get :download, {format:'json', id: recorded_track.recording.id, track_id: recorded_track.client_track_id} - response.status.should == 302 - recorded_track.reload - recorded_track.download_count.should == 101 - end - end - - describe "download backing track" do - let(:mix) { FactoryGirl.create(:mix) } - - it "should only allow a user to download a track if they have claimed the recording" do - post :start, { :format => 'json', :music_session_id => @music_session.id } - response_body = JSON.parse(response.body) - recording = Recording.find(response_body['id']) - post :stop, { :format => 'json', :id => recording.id } - response.should be_success - end - - - it "is possible" do - mix.touch - recorded_track = mix.recording.recorded_tracks[0] - controller.current_user = mix.recording.owner - get :download, {id: recorded_track.recording.id, track_id: recorded_track.client_track_id} - response.status.should == 302 - - recorded_track.reload - recorded_track.download_count.should == 1 - - get :download, {id: recorded_track.recording.id, track_id: recorded_track.client_track_id} - response.status.should == 302 - - recorded_track.reload - recorded_track.download_count.should == 2 - end - - - it "prevents download after limit is reached" do - mix.touch - recorded_track = mix.recording.recorded_tracks[0] - recorded_track.download_count = APP_CONFIG.max_audio_downloads - recorded_track.save! - controller.current_user = recorded_track.user - get :download, {format:'json', id: recorded_track.recording.id, track_id: recorded_track.client_track_id} - response.status.should == 404 - JSON.parse(response.body, symbolize_names: true)[:message].should == "download limit surpassed" - end - - - it "lets admins surpass limit" do - mix.touch - recorded_track = mix.recording.recorded_tracks[0] - recorded_track.download_count = APP_CONFIG.max_audio_downloads - recorded_track.save! - recorded_track.user.admin = true - recorded_track.user.save! - - controller.current_user = recorded_track.user - get :download, {format:'json', id: recorded_track.recording.id, track_id: recorded_track.client_track_id} - response.status.should == 302 - recorded_track.reload - recorded_track.download_count.should == 101 + describe "start" do + it "should work" do + post :start, {:format => 'json', :music_session_id => @music_session.id} + response.should be_success + response_body = JSON.parse(response.body) + response_body['id'].should_not be_nil + recording = Recording.find(response_body['id']) + recorded_jam_track_track = response_body["recorded_jam_track_tracks"][0] + recorded_jam_track_track["id"].should eq(@jam_track.jam_track_tracks[0].id) + recorded_jam_track_track["timeline"].should eq([]) + end end end end + diff --git a/web/spec/javascripts/download_jamtrack_spec.js.coffee b/web/spec/javascripts/download_jamtrack_spec.js.coffee index b98150477..4c358b324 100644 --- a/web/spec/javascripts/download_jamtrack_spec.js.coffee +++ b/web/spec/javascripts/download_jamtrack_spec.js.coffee @@ -1,16 +1,310 @@ describe "DownloadJamTrack", -> beforeEach -> - this.fixtures = fixture.load("downoadJamTrack.html", "user_sync_track1.json"); # append these fixtures which were already cached - this.server = sinon.fakeServer.create(); + this.fixtures = fixture.load("downloadJamTrack.html"); # append these fixtures which were already cached window.jamClient = sinon.stub() - this.downloadJamTrack = new JK.DownloadJamTrack() - this.downloadJamTrack.init() - $('body').append(this.downloadJamTrack.root) + this.app = sinon.stub() + this.jamTrackId = '1' + this.jamTrack = {id: this.jamTrackId, jam_track_right_id: '1', name: 'Back in Black', version:'1'} + window.gon = {} + window.JK.JamServer = {} + window.stats = {} + @statsSpy = window.stats.write = sinon.spy() + window.JK.JamServer.send = sinon.stub(); # attempts to subscribe to the socket will need this afterEach -> - this.server.restore(); + window.stats.write.reset() + + describe "normal browser", -> + + beforeEach -> + window.gon.isNativeClient = false + @showNoClientSpy = sinon.spy(JK.DownloadJamTrack.prototype, 'showNoClient') + @downloadJamTrack = new JK.DownloadJamTrack(@app, @jamTrack) + $('body').append(this.downloadJamTrack.root) + + afterEach -> + @showNoClientSpy.restore() + + it "switches to 'no client' correctly", -> + window.jamClient.JamTrackGetTrackDetail = sinon.stub() + window.jamClient.InvalidateJamTrack = sinon.stub() + #window.jamClient.JamTrackGetTrackDetail.returns({'key_state' : 'ready'}) + + @downloadJamTrack.init(); + + expect(window.jamClient.JamTrackGetTrackDetail.callCount).toBe(0) + expect(@showNoClientSpy.calledOnce).toBe(true) + expect(@downloadJamTrack.stateHolder.find('.state-no-client')).toHaveLength(1) + expect(@statsSpy.calledOnce).toBe(true) + + describe "client", -> + + beforeEach -> + window.gon.isNativeClient = true + + describe "already synchronized", -> + beforeEach -> + @showSynchronizedSpy = sinon.spy(JK.DownloadJamTrack.prototype, 'showSynchronized') + @downloadJamTrack = new JK.DownloadJamTrack(@app, @jamTrack) + $('body').append(@downloadJamTrack.root) + + afterEach -> + @showSynchronizedSpy.restore() + @downloadJamTrack.destroy() + + it "shows synchronized state", -> + window.jamClient.JamTrackGetTrackDetail = sinon.stub() + window.jamClient.JamTrackGetTrackDetail.returns({'key_state' : 'ready', 'version' : '1'}) + window.jamClient.InvalidateJamTrack = sinon.stub() + + @downloadJamTrack.init(); + + expect(window.jamClient.JamTrackGetTrackDetail.callCount).toBe(1) + expect(window.jamClient.InvalidateJamTrack.callCount).toBe(0) + expect(@showSynchronizedSpy.calledOnce).toBe(true) + expect(@downloadJamTrack.transitionTimer).toBe(null) + expect(@downloadJamTrack.downloadTimer).toBe(null) + expect(@statsSpy.calledOnce).toBe(true) + + expect(@downloadJamTrack.stateHolder.find('.state-synchronized')).toHaveLength(1) + + describe "pending", -> + beforeEach -> + window.jamClient.JamTrackKeysRequest = sinon.stub() + @showSynchronizedSpy = sinon.spy(JK.DownloadJamTrack.prototype, 'showSynchronized') + @showErrorSpy = sinon.spy(JK.DownloadJamTrack.prototype, 'showError') + @showKeyingSpy = sinon.spy(JK.DownloadJamTrack.prototype, 'showKeying') + @downloadJamTrack = new JK.DownloadJamTrack(@app, @jamTrack) + @downloadJamTrack.states.keying.max_time = -1 # hurry up the test, instead of waiting 10 seconds + $('body').append(@downloadJamTrack.root) + + afterEach -> + @showSynchronizedSpy.restore() + @showErrorSpy.restore() + @showKeyingSpy.restore() + @downloadJamTrack.destroy() + + it "shows errored state due to timeout", -> + window.jamClient.JamTrackGetTrackDetail = sinon.stub() + window.jamClient.JamTrackGetTrackDetail.returns({'key_state' : 'pending', 'version' : '1'}) + window.jamClient.InvalidateJamTrack = sinon.stub() + + @downloadJamTrack.init(); + + expect(window.jamClient.JamTrackGetTrackDetail.callCount).toBe(1) + expect(@showKeyingSpy.calledOnce).toBe(true) + expect(@downloadJamTrack.states.keying.timer).toNotBe(null) + + @downloadJamTrack.stateIntervalCheck() + + expect(window.jamClient.JamTrackGetTrackDetail.callCount).toBe(2) + + expect(@showErrorSpy.calledOnce).toBe(true) + expect(@downloadJamTrack.states.keying.timer).toBe(null) + expect(@downloadJamTrack.stateHolder.find('.state-errored')).toHaveLength(1) + expect(@downloadJamTrack.stateHolder.find('.state-errored .msg')).toContainText('It took too long for the JamTrack to be keyed.') + expect(@statsSpy.calledOnce).toBe(true) + expect(@downloadJamTrack.attempts).toBe(1) + + # now simulate a retry attempt + @downloadJamTrack.stateHolder.find('.retry-button').trigger('click') + + # and verify that we are beginning a re-attempt + expect(window.jamClient.JamTrackGetTrackDetail.callCount).toBe(3) + expect(@showKeyingSpy.calledTwice).toBe(true) + expect(@downloadJamTrack.states.keying.timer).toNotBe(null) + expect(@downloadJamTrack.attempts).toBe(2) + + + it "shows synchronized", -> + window.jamClient.JamTrackGetTrackDetail = sinon.stub() + window.jamClient.JamTrackGetTrackDetail.returns({'key_state' : 'pending', 'version' : '1'}) + window.jamClient.InvalidateJamTrack = sinon.stub() + + @downloadJamTrack.init() + + expect(window.jamClient.JamTrackGetTrackDetail.callCount).toBe(1) + expect(@showKeyingSpy.calledOnce).toBe(true) + + # keying timer should be firing + expect(@downloadJamTrack.states.keying.timer).toNotBe(null) + + # say the keys have been fetched + window.jamClient.JamTrackGetTrackDetail.returns({'key_state' : 'ready', 'version' : '1'}) + window.jamClient.InvalidateJamTrack = sinon.stub() + + # then do a check + @downloadJamTrack.stateIntervalCheck() + + expect(@showSynchronizedSpy.calledOnce).toBe(true) + expect(@downloadJamTrack.states.keying.timer).toBe(null) + expect(@downloadJamTrack.stateHolder.find('.state-synchronized')).toHaveLength(1) + expect(@statsSpy.calledOnce).toBe(true) + + + + describe "JamTrack needs downloading", -> + beforeEach -> + window.jamClient.JamTrackDownload = sinon.stub() + @showSynchronizedSpy = sinon.spy(JK.DownloadJamTrack.prototype, 'showSynchronized') + @showErrorSpy = sinon.spy(JK.DownloadJamTrack.prototype, 'showError') + @showKeyingSpy = sinon.spy(JK.DownloadJamTrack.prototype, 'showKeying') + @showDownloadingSpy = sinon.spy(JK.DownloadJamTrack.prototype, 'showDownloading') + @showPackagingSpy = sinon.spy(JK.DownloadJamTrack.prototype, 'showPackaging') + @downloadJamTrack = new JK.DownloadJamTrack(@app, @jamTrack) + @downloadJamTrack.states.keying.max_time = -1 # hurry up the test, instead of waiting 10 seconds + $('body').append(@downloadJamTrack.root) + + afterEach -> + @showSynchronizedSpy.restore() + @showErrorSpy.restore() + @showKeyingSpy.restore() + @showDownloadingSpy.restore() + @showPackagingSpy.restore() + @downloadJamTrack.destroy() + + it "shows downloading for signed package", -> + window.jamClient.JamTrackKeysRequest = sinon.stub() + window.jamClient.JamTrackGetTrackDetail = sinon.stub() + window.jamClient.JamTrackGetTrackDetail.returns({'key_state' : 'unknown', 'version' : '1'}) + window.jamClient.InvalidateJamTrack = sinon.stub() + + spyOn(@downloadJamTrack.rest, 'getJamTrackRight').andCallFake((data) => + d = $.Deferred(); + d.resolve({signing_state: 'SIGNED'}); + d.promise(); + ) + + window.jamClient.JamTrackDownload = sinon.stub() + + @downloadJamTrack.init() + + expect(window.jamClient.JamTrackGetTrackDetail.callCount).toBe(1) + expect(window.jamClient.JamTrackDownload.callCount).toBe(1) + expect(@showDownloadingSpy.calledOnce).toBe(true) + expect(@downloadJamTrack.stateHolder.find('.state-downloading')).toHaveLength(1) + + eval(@downloadJamTrack.makeDownloadSuccessCallback() + '()') + + expect(@showKeyingSpy.calledOnce).toBe(true) + expect(@downloadJamTrack.stateHolder.find('.state-keying')).toHaveLength(1) + + # keying timer should be firing + expect(@downloadJamTrack.states.keying.timer).toNotBe(null) + + # say the keys have been fetched + window.jamClient.JamTrackGetTrackDetail.returns({'key_state' : 'ready', 'version' : '1'}) + window.jamClient.InvalidateJamTrack = sinon.stub() + + # check state again + @downloadJamTrack.stateIntervalCheck() + + # we should now be synchronized + expect(@showSynchronizedSpy.calledOnce).toBe(true) + expect(@downloadJamTrack.states.keying.timer).toBe(null) + expect(@downloadJamTrack.stateHolder.find('.state-synchronized')).toHaveLength(1) + expect(@statsSpy.calledOnce).toBe(true) + + it "is not yet packaged", -> + window.jamClient.JamTrackGetTrackDetail = sinon.stub() + window.jamClient.JamTrackGetTrackDetail.returns({'key_state' : 'unknown', 'version' : '1'}) + window.jamClient.InvalidateJamTrack = sinon.stub() + + spyOn(@downloadJamTrack.rest, 'getJamTrackRight').andCallFake((data) => + d = $.Deferred(); + d.resolve({signing_state: 'QUIET'}); + d.promise(); + ) + + spyOn(@downloadJamTrack.rest, 'enqueueJamTrack').andCallFake((data) => + d = $.Deferred(); + d.resolve({}); + d.promise(); + ) + + window.jamClient.JamTrackDownload = sinon.stub() + + @downloadJamTrack.init() + + expect(@downloadJamTrack.attemptedEnqueue).toBe(true) + expect(@downloadJamTrack.transitionTimer?).toBe(true) + + # simulate poke from server saying the track has been queued + @downloadJamTrack.onJamTrackRightEvent(null, {body: {signing_state: 'QUEUED'}}) + + # the frontend should be saying that it's packaging now + expect(@downloadJamTrack.transitionTimer?).toBe(true) + expect(@showPackagingSpy.calledOnce).toBe(true) + expect(@downloadJamTrack.stateHolder.find('.state-packaging')).toHaveLength(1) + + # simulate poke from server saying the track is currently signing + @downloadJamTrack.onJamTrackRightEvent(null, {body: {signing_state: 'SIGNING'}}) + + # the frontend still be saying it's packaging + expect(@downloadJamTrack.transitionTimer?).toBe(true) + expect(@showPackagingSpy.calledOnce).toBe(true) + + # simulate poke from server saying the track is signed + @downloadJamTrack.onJamTrackRightEvent(null, {body: {signing_state: 'SIGNED'}}) + + expect(@downloadJamTrack.transitionTimer?).toBe(false) + + # downloading has started; other test covers this, so we stop testing + expect(@showDownloadingSpy.calledOnce).toBe(true) + expect(@downloadJamTrack.stateHolder.find('.state-downloading')).toHaveLength(1) + + # since we haven't yet made it to a leave node, make sure we haven't reported a stat + expect(@statsSpy.callCount).toBe(0) + + it "queue time out when packaging", -> + window.jamClient.JamTrackGetTrackDetail = sinon.stub() + window.jamClient.JamTrackGetTrackDetail.returns({'key_state' : 'unknown', 'version' : '1'}) + window.jamClient.InvalidateJamTrack = sinon.stub() + + getJamTrackRightSpy = spyOn(@downloadJamTrack.rest, 'getJamTrackRight') + getJamTrackRightSpy.andCallFake((data) => + d = $.Deferred(); + d.resolve({signing_state: 'QUIET'}); + d.promise(); + ) + + spyOn(@downloadJamTrack.rest, 'enqueueJamTrack').andCallFake((data) => + d = $.Deferred(); + d.resolve({}); + d.promise(); + ) + + window.jamClient.JamTrackDownload = sinon.stub() + + @downloadJamTrack.init() + + expect(@downloadJamTrack.attemptedEnqueue).toBe(true) + expect(@downloadJamTrack.transitionTimer?).toBe(true) + + getJamTrackRightSpy.reset() + + # simulate timer running out, and server check resulting in QUEUED_TIMEOUT + getJamTrackRightSpy.andCallFake((data) => + d = $.Deferred(); + d.resolve({signing_state: 'QUEUED_TIMEOUT'}); + d.promise(); + ) + + @downloadJamTrack.transitionCheck() + + # the frontend should be saying that it's packaging now + expect(@downloadJamTrack.transitionTimer?).toBe(false) + expect(@showErrorSpy.calledOnce).toBe(true) + expect(@downloadJamTrack.stateHolder.find('.state-errored')).toHaveLength(1) + expect(@downloadJamTrack.stateHolder.find('.state-errored .msg')).toContainText('The server took too long to begin processing your JamTrack.') + + expect(@statsSpy.calledOnce).toBe(true) + + + + + + + - it "display state correctly", -> - $track = this.syncViewer.createTrack(this.track1) - this.syncViewer.updateTrackState($track)