diff --git a/db/manifest b/db/manifest index 94d24481b..986692c75 100755 --- a/db/manifest +++ b/db/manifest @@ -76,3 +76,4 @@ user_progress_tracking.sql whats_next.sql add_user_bio.sql users_geocoding.sql +recordings_public_launch.sql diff --git a/db/up/recordings_public_launch.sql b/db/up/recordings_public_launch.sql new file mode 100644 index 000000000..be7323e75 --- /dev/null +++ b/db/up/recordings_public_launch.sql @@ -0,0 +1,27 @@ +-- so that rows can live on after session is over +ALTER TABLE recordings DROP CONSTRAINT "recordings_music_session_id_fkey"; +-- unambiguous declartion that the recording is over or not +ALTER TABLE recordings ADD COLUMN is_done BOOLEAN DEFAULT FALSE; + +-- add name and description on claimed_recordings, which is the user's individual view of a recording +ALTER TABLE claimed_recordings ADD COLUMN description VARCHAR(8000); +ALTER TABLE claimed_recordings ADD COLUMN description_tsv tsvector; +ALTER TABLE claimed_recordings ADD COLUMN name_tsv tsvector; + +CREATE TRIGGER tsvectorupdate_description BEFORE INSERT OR UPDATE +ON claimed_recordings FOR EACH ROW EXECUTE PROCEDURE +tsvector_update_trigger(description_tsv, 'public.jamenglish', description); + +CREATE TRIGGER tsvectorupdate_name BEFORE INSERT OR UPDATE +ON claimed_recordings FOR EACH ROW EXECUTE PROCEDURE +tsvector_update_trigger(name_tsv, 'public.jamenglish', name); + +CREATE INDEX claimed_recordings_description_tsv_index ON claimed_recordings USING gin(description_tsv); +CREATE INDEX claimed_recordings_name_tsv_index ON claimed_recordings USING gin(name_tsv); + +-- copies of connection.client_id and track.id +ALTER TABLE recorded_tracks ADD COLUMN client_id VARCHAR(64) NOT NULL; +ALTER TABLE recorded_tracks ADD COLUMN track_id VARCHAR(64) NOT NULL; + +-- so that server can correlate to client track +ALTER TABLE tracks ADD COLUMN client_track_id VARCHAR(64) NOT NULL; diff --git a/pb/src/client_container.proto b/pb/src/client_container.proto index 6b4f6054b..ebe125766 100644 --- a/pb/src/client_container.proto +++ b/pb/src/client_container.proto @@ -177,6 +177,7 @@ message MusicianSessionDepart { optional string user_id = 2; // this is the user_id and can be used for user unicast messages optional string username = 3; // meant to be a display name optional string photo_url = 4; + optional string recordingId = 5; // if specified, the recording was stopped automatically } // route_to: client: diff --git a/ruby/lib/jam_ruby/connection_manager.rb b/ruby/lib/jam_ruby/connection_manager.rb index e79ee8b29..869906da9 100644 --- a/ruby/lib/jam_ruby/connection_manager.rb +++ b/ruby/lib/jam_ruby/connection_manager.rb @@ -362,6 +362,7 @@ SQL t.instrument = instrument t.connection = connection t.sound = track["sound"] + t.client_track_id = track["client_track_id"] t.save connection.tracks << t end diff --git a/ruby/lib/jam_ruby/constants/validation_messages.rb b/ruby/lib/jam_ruby/constants/validation_messages.rb index 8f7e8b514..7322fd62a 100644 --- a/ruby/lib/jam_ruby/constants/validation_messages.rb +++ b/ruby/lib/jam_ruby/constants/validation_messages.rb @@ -34,4 +34,18 @@ module ValidationMessages EMAIL_ALREADY_TAKEN = "has already been taken" EMAIL_MATCHES_CURRENT = "is same as your current email" INVALID_FPFILE = "is not valid" + + #connection + + SELECT_AT_LEAST_ONE = "Please select at least one track" + FAN_CAN_NOT_JOIN_AS_MUSICIAN = "A fan can not join a music session as a musician" + MUSIC_SESSION_MUST_BE_SPECIFIED = "A music session must be specified" + INVITE_REQUIRED = "You must be invited to join this session" + FANS_CAN_NOT_JOIN = "Fans can not join this session" + CANT_JOIN_RECORDING_SESSION = "is currently recording" + + # recordings + ALREADY_BEING_RECORDED = "already being recorded" + NO_LONGER_RECORDING = "no longer recording" + NOT_IN_SESSION = "not in session" end diff --git a/ruby/lib/jam_ruby/lib/audiomixer.rb b/ruby/lib/jam_ruby/lib/audiomixer.rb new file mode 100644 index 000000000..2efc0c955 --- /dev/null +++ b/ruby/lib/jam_ruby/lib/audiomixer.rb @@ -0,0 +1,21 @@ +require 'json' +require 'resque' + +module JamRuby + + @queue = :audiomixer + + class AudioMixer + + def self.perform(manifest) + tmp = Dir::Tmpname.make_tmpname "/var/tmp/audiomixer/manifest-#{manifest['recordingId']}", nil + File.open(tmp,"w") do |f| + f.write(manifest.to_json) + end + + system("tar zxvf some_big_tarball.tar.gz")) + end + + end + +end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/message_factory.rb b/ruby/lib/jam_ruby/message_factory.rb index 8167b71de..c21df795e 100644 --- a/ruby/lib/jam_ruby/message_factory.rb +++ b/ruby/lib/jam_ruby/message_factory.rb @@ -107,8 +107,8 @@ end # create a musician left session message - def musician_session_depart(session_id, user_id, username, photo_url) - left = Jampb::MusicianSessionDepart.new(:session_id => session_id, :user_id => user_id, :username => username, :photo_url => photo_url) + def musician_session_depart(session_id, user_id, username, photo_url, recordingId = nil) + left = Jampb::MusicianSessionDepart.new(:session_id => session_id, :user_id => user_id, :username => username, :photo_url => photo_url, :recordingId => recordingId) return Jampb::ClientMessage.new(:type => ClientMessage::Type::MUSICIAN_SESSION_DEPART, :route_to => CLIENT_TARGET, :musician_session_depart => left) end diff --git a/ruby/lib/jam_ruby/models/claimed_recording.rb b/ruby/lib/jam_ruby/models/claimed_recording.rb index 5e8bb963d..aa8d0764d 100644 --- a/ruby/lib/jam_ruby/models/claimed_recording.rb +++ b/ruby/lib/jam_ruby/models/claimed_recording.rb @@ -2,6 +2,7 @@ module JamRuby class ClaimedRecording < ActiveRecord::Base validates :name, no_profanity: true + validates :description, no_profanity: true belongs_to :recording, :class_name => "JamRuby::Recording", :inverse_of => :claimed_recordings belongs_to :user, :class_name => "JamRuby::User", :inverse_of => :claimed_recordings @@ -16,6 +17,7 @@ module JamRuby end self.name = params[:name] unless params[:name].nil? + self.description = params[:description] unless params[:description].nil? self.genre = Genre.find(params[:genre]) unless params[:genre].nil? self.is_public = params[:is_public] unless params[:is_public].nil? self.is_downloadable = params[:is_downloadable] unless params[:is_downloadable].nil? diff --git a/ruby/lib/jam_ruby/models/connection.rb b/ruby/lib/jam_ruby/models/connection.rb index 62935f4e3..cbc4a9bc2 100644 --- a/ruby/lib/jam_ruby/models/connection.rb +++ b/ruby/lib/jam_ruby/models/connection.rb @@ -3,11 +3,6 @@ require 'aasm' module JamRuby class Connection < ActiveRecord::Base - SELECT_AT_LEAST_ONE = "Please select at least one track" - FAN_CAN_NOT_JOIN_AS_MUSICIAN = "A fan can not join a music session as a musician" - MUSIC_SESSION_MUST_BE_SPECIFIED = "A music session must be specified" - INVITE_REQUIRED = "You must be invited to join this session" - FANS_CAN_NOT_JOIN = "Fans can not join this session" attr_accessor :joining_session @@ -72,37 +67,40 @@ module JamRuby def can_join_music_session if music_session.nil? - errors.add(:music_session, MUSIC_SESSION_MUST_BE_SPECIFIED) + errors.add(:music_session, ValidationMessages::MUSIC_SESSION_MUST_BE_SPECIFIED) return false end if as_musician unless self.user.musician - errors.add(:as_musician, FAN_CAN_NOT_JOIN_AS_MUSICIAN) + errors.add(:as_musician, ValidationMessages::FAN_CAN_NOT_JOIN_AS_MUSICIAN) return false end if music_session.musician_access if music_session.approval_required unless music_session.creator == user || music_session.invited_musicians.exists?(user) - errors.add(:approval_required, INVITE_REQUIRED) + errors.add(:approval_required, ValidationMessages::INVITE_REQUIRED) return false end end else unless music_session.creator == user || music_session.invited_musicians.exists?(user) - errors.add(:musician_access, INVITE_REQUIRED) + errors.add(:musician_access, ValidationMessages::INVITE_REQUIRED) return false end end else unless self.music_session.fan_access # it's someone joining as a fan, and the only way a fan can join is if fan_access is true - errors.add(:fan_access, FANS_CAN_NOT_JOIN) + errors.add(:fan_access, ValidationMessages::FANS_CAN_NOT_JOIN) return false end end + if music_session.is_recording? + errors.add(:music_session, ValidationMessages::CANT_JOIN_RECORDING_SESSION) + end return true end @@ -120,7 +118,7 @@ module JamRuby private def require_at_least_one_track_when_in_session if tracks.count == 0 - errors.add(:genres, SELECT_AT_LEAST_ONE) + errors.add(:genres, ValidationMessages::SELECT_AT_LEAST_ONE) end end diff --git a/ruby/lib/jam_ruby/models/music_session.rb b/ruby/lib/jam_ruby/models/music_session.rb index c61246b53..ddb3be628 100644 --- a/ruby/lib/jam_ruby/models/music_session.rb +++ b/ruby/lib/jam_ruby/models/music_session.rb @@ -1,6 +1,5 @@ module JamRuby class MusicSession < ActiveRecord::Base - self.primary_key = 'id' attr_accessor :legal_terms, :skip_genre_validation @@ -17,8 +16,7 @@ module JamRuby has_many :fan_invitations, :foreign_key => "music_session_id", :inverse_of => :music_session, :class_name => "JamRuby::FanInvitation" has_many :invited_fans, :through => :fan_invitations, :class_name => "JamRuby::User", :foreign_key => "receiver_id", :source => :receiver - has_one :recording, :class_name => "JamRuby::Recording", :inverse_of => :music_session - + has_many :recordings, :class_name => "JamRuby::Recording", :inverse_of => :music_session belongs_to :band, :inverse_of => :music_sessions, :class_name => "JamRuby::Band", :foreign_key => "band_id" after_save :require_at_least_one_genre, :limit_max_genres @@ -38,7 +36,7 @@ module JamRuby def creator_is_musician unless creator.musician? - errors.add(:creator, "creator must be a musician") + errors.add(:creator, "must be a musician") end end @@ -168,7 +166,22 @@ module JamRuby def access? user return self.users.exists? user end - + + # is this music session currently recording? + def is_recording? + recordings.where(:duration => nil).count > 0 + end + + def recording + recordings.where(:duration => nil).first + end + + # stops any active recording + def stop_recording + current_recording = self.recording + current_recording.stop unless current_recording.nil? + end + def to_s return description end diff --git a/ruby/lib/jam_ruby/models/notification.rb b/ruby/lib/jam_ruby/models/notification.rb index 227fab529..4f7a8a284 100644 --- a/ruby/lib/jam_ruby/models/notification.rb +++ b/ruby/lib/jam_ruby/models/notification.rb @@ -241,10 +241,10 @@ module JamRuby @@mq_router.server_publish_to_session(music_session, msg, sender = {:client_id => connection.client_id}) end - def send_musician_session_depart(music_session, client_id, user) + def send_musician_session_depart(music_session, client_id, user, recordingId = nil) # (1) create notification - msg = @@message_factory.musician_session_depart(music_session.id, user.id, user.name, user.photo_url) + msg = @@message_factory.musician_session_depart(music_session.id, user.id, user.name, user.photo_url, recordingId) # (2) send notification @@mq_router.server_publish_to_session(music_session, msg, sender = {:client_id => client_id}) diff --git a/ruby/lib/jam_ruby/models/recorded_track.rb b/ruby/lib/jam_ruby/models/recorded_track.rb index c53d02f62..4a2b0813d 100644 --- a/ruby/lib/jam_ruby/models/recorded_track.rb +++ b/ruby/lib/jam_ruby/models/recorded_track.rb @@ -12,17 +12,19 @@ module JamRuby belongs_to :instrument, :class_name => "JamRuby::Instrument" validates :sound, :inclusion => {:in => SOUND} - + validates :client_id, :presence => true # not a connection relation on purpose + validates :track_id, :presence => true # not a track relation on purpose before_destroy :delete_s3_files # Copy an ephemeral track to create a saved one. Some fields are ok with defaults def self.create_from_track(track, recording) recorded_track = self.new recorded_track.recording = recording + recorded_track.client_id = track.connection.client_id + recorded_track.track_id = track.id recorded_track.user = track.connection.user recorded_track.instrument = track.instrument recorded_track.sound = track.sound - recorded_track.save recorded_track end diff --git a/ruby/lib/jam_ruby/models/recording.rb b/ruby/lib/jam_ruby/models/recording.rb index d9e2a90ed..87740e05d 100644 --- a/ruby/lib/jam_ruby/models/recording.rb +++ b/ruby/lib/jam_ruby/models/recording.rb @@ -3,51 +3,48 @@ module JamRuby self.primary_key = 'id' + attr_accessible :name, :description, :genre, :is_public, :is_downloadable + has_many :claimed_recordings, :class_name => "JamRuby::ClaimedRecording", :inverse_of => :recording - has_many :users, :through => :claimed_recordings, :class_name => "JamRuby::User" + has_many :users, :through => :recorded_tracks, :class_name => "JamRuby::User" belongs_to :owner, :class_name => "JamRuby::User", :inverse_of => :owned_recordings belongs_to :band, :class_name => "JamRuby::Band", :inverse_of => :recordings - belongs_to :music_session, :class_name => "JamRuby::MusicSession", :inverse_of => :recording + belongs_to :music_session, :class_name => "JamRuby::MusicSession", :inverse_of => :recordings has_many :mixes, :class_name => "JamRuby::Mix", :inverse_of => :recording has_many :recorded_tracks, :class_name => "JamRuby::RecordedTrack", :foreign_key => :recording_id - - + validates :music_session, :presence => true + validate :not_already_recording, :on => :create + validate :already_stopped_recording + + def not_already_recording + if music_session.is_recording? + errors.add(:music_session, ValidationMessages::ALREADY_BEING_RECORDED) + end + end + + def already_stopped_recording + if is_done && is_done_was + errors.add(:music_session, ValidationMessages::NO_LONGER_RECORDING) + end + end + # Start recording a session. - def self.start(music_session_id, owner) - + def self.start(music_session, owner) recording = nil - # Use a transaction and lock to avoid races. - ActiveRecord::Base.transaction do - music_session = MusicSession.find(music_session_id, :lock => true) - - if music_session.nil? - raise PermissionError, "the session has ended" - end - - unless music_session.recording.nil? - raise PermissionError, "the session is already being recorded" - end - + music_session.with_lock do recording = Recording.new recording.music_session = music_session recording.owner = owner - + recording.band = music_session.band + music_session.connections.each do |connection| - # Note that we do NOT connect the recording to any users at this point. - # That ONLY happens if a user clicks 'save' - # recording.users << connection.user connection.tracks.each do |track| - RecordedTrack.create_from_track(track, recording) + recording.recorded_tracks << RecordedTrack.create_from_track(track, recording) end end - # Note that I believe this can be nil. - recording.band = music_session.band recording.save - - music_session.recording = recording - music_session.save end @@ -55,10 +52,10 @@ module JamRuby # NEED TO SEND NOTIFICATION TO ALL USERS IN THE SESSION THAT RECORDING HAS STARTED HERE. # I'LL STUB IT A BIT. NOTE THAT I REDO THE FIND HERE BECAUSE I DON'T WANT TO SEND THESE # NOTIFICATIONS WHILE THE DB ROW IS LOCKED - music_session = MusicSession.find(music_session_id) - music_session.connections.each do |connection| - # connection.notify_recording_has_started - end + #music_session = MusicSession.find(music_session_id) + #music_session.connections.each do |connection| + # # connection.notify_recording_has_started + #end recording end @@ -66,34 +63,27 @@ module JamRuby # Stop recording a session def stop # Use a transaction and lock to avoid races. - ActiveRecord::Base.transaction do - music_session = MusicSession.find(self.music_session_id, :lock => true) - if music_session.nil? - raise PermissionError, "the session has ended" - end - unless music_session.recording - raise PermissionError, "the session is not currently being recorded" - end - music_session.recording = nil - music_session.save + music_session = MusicSession.find_by_id(music_session_id) + locker = music_session.nil? ? self : music_session + locker.with_lock do + self.duration = Time.now - created_at + self.is_done = true + self.save end - - self.duration = Time.now - created_at - save + self end - # Called when a user wants to "claim" a recording. To do this, the user must have been one of the tracks in the recording. - def claim(user, name, genre, is_public, is_downloadable) - if self.users.include?(user) - raise PermissionError, "user already claimed this recording" - end + def claim(user, name, description, genre, is_public, is_downloadable) + # if self.users.include?(user) + # raise PermissionError, "user already claimed this recording" + # end - unless self.recorded_tracks.find { |recorded_track| recorded_track.user == user } + unless self.users.exists?(user) raise PermissionError, "user was not in this session" end - unless self.music_session.nil? + if self.music_session.is_recording? raise PermissionError, "recording cannot be claimed while it is being recorded" end @@ -105,11 +95,11 @@ module JamRuby claimed_recording.user = user claimed_recording.recording = self claimed_recording.name = name + claimed_recording.description = description claimed_recording.genre = genre claimed_recording.is_public = is_public claimed_recording.is_downloadable = is_downloadable self.claimed_recordings << claimed_recording - save claimed_recording end diff --git a/ruby/lib/jam_ruby/models/track.rb b/ruby/lib/jam_ruby/models/track.rb index 7ae02b24c..76b428b09 100644 --- a/ruby/lib/jam_ruby/models/track.rb +++ b/ruby/lib/jam_ruby/models/track.rb @@ -38,7 +38,68 @@ module JamRuby return query end - def self.save(id, connection_id, instrument_id, sound) + + # this is a bit different from a normal track synchronization in that the client just sends up all tracks, + # ... some may already exist + def self.sync(clientId, tracks) + result = [] + + Track.transaction do + connection = Connection.find_by_client_id!(clientId) + + if tracks.length == 0 + connection.tracks.delete_all + else + connection_tracks = connection.tracks + + # we will prune from this as we find matching tracks + to_delete = Set.new(connection_tracks) + to_add = Array.new(tracks) + + connection_tracks.each do |connection_track| + tracks.each do |track| + if track[:id] == connection_track.id || track[:client_track_id] == connection_track.client_track_id; + to_delete.delete(connection_track) + to_add.delete(track) + # don't update connection_id or client_id; it's unknown what would happen if these changed mid-session + connection_track.instrument = Instrument.find(track[:instrument_id]) + connection_track.sound = track[:sound] + connection_track.client_track_id = track[:client_track_id] + if connection_track.save + result.push(connection_track) + next + else + result = connection_track + raise ActiveRecord::Rollback + end + end + end + end + + to_add.each do |track| + connection_track = Track.new + connection_track.connection = connection + connection_track.instrument = Instrument.find(track[:instrument_id]) + connection_track.sound = track[:sound] + connection_track.client_track_id = track[:client_track_id] + if connection_track.save + result.push(connection_track) + else + result = connection_track + raise ActiveRecord::Rollback + end + end + + to_delete.each do| delete_me | + delete_me.delete + end + end + end + + result + end + + def self.save(id, connection_id, instrument_id, sound, client_track_id) if id.nil? track = Track.new() track.connection_id = connection_id @@ -54,6 +115,10 @@ module JamRuby track.sound = sound end + unless client_track_id.nil? + track.client_track_id = client_track_id + end + track.updated_at = Time.now.getutc track.save return track diff --git a/ruby/lib/jam_ruby/models/user.rb b/ruby/lib/jam_ruby/models/user.rb index 5ba60dc30..d6364b741 100644 --- a/ruby/lib/jam_ruby/models/user.rb +++ b/ruby/lib/jam_ruby/models/user.rb @@ -118,7 +118,7 @@ module JamRuby validates :first_name, presence: true, length: {maximum: 50}, no_profanity: true validates :last_name, presence: true, length: {maximum: 50}, no_profanity: true VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i - validates :email, presence: true, format: {with: VALID_EMAIL_REGEX} + validates :email, presence: true, format: {with: VALID_EMAIL_REGEX} validates :update_email, presence: true, format: {with: VALID_EMAIL_REGEX}, :if => :updating_email validates_length_of :password, minimum: 6, maximum: 100, :if => :should_validate_password? diff --git a/ruby/spec/factories.rb b/ruby/spec/factories.rb index 5de772b2b..dbb845cc5 100644 --- a/ruby/spec/factories.rb +++ b/ruby/spec/factories.rb @@ -90,7 +90,7 @@ FactoryGirl.define do factory :track, :class => JamRuby::Track do sound "mono" - + sequence(:client_track_id) { |n| "client_track_id#{n}"} end factory :recorded_track, :class => JamRuby::RecordedTrack do diff --git a/ruby/spec/jam_ruby/connection_manager_spec.rb b/ruby/spec/jam_ruby/connection_manager_spec.rb index aacd9f6c9..392807675 100644 --- a/ruby/spec/jam_ruby/connection_manager_spec.rb +++ b/ruby/spec/jam_ruby/connection_manager_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' # these tests avoid the use of ActiveRecord and FactoryGirl to do blackbox, non test-instrumented tests describe ConnectionManager do - TRACKS = [{"instrument_id" => "electric guitar", "sound" => "mono"}] + TRACKS = [{"instrument_id" => "electric guitar", "sound" => "mono", "client_track_id" => "some_client_track_id"}] before do @conn = PG::Connection.new(:dbname => SpecDb::TEST_DB_NAME, :user => "postgres", :password => "postgres", :host => "localhost") @@ -287,7 +287,7 @@ describe ConnectionManager do connection = @connman.join_music_session(user, client_id2, music_session, true, TRACKS) connection.errors.size.should == 1 - connection.errors.get(:as_musician).should == [Connection::FAN_CAN_NOT_JOIN_AS_MUSICIAN] + connection.errors.get(:as_musician).should == [ValidationMessages::FAN_CAN_NOT_JOIN_AS_MUSICIAN] end it "as_musician is coerced to boolean" do @@ -352,7 +352,7 @@ describe ConnectionManager do @connman.create_connection(user_id, client_id, "1.1.1.1") connection = @connman.join_music_session(user, client_id, music_session, true, TRACKS) connection.errors.size.should == 1 - connection.errors.get(:music_session).should == [Connection::MUSIC_SESSION_MUST_BE_SPECIFIED] + connection.errors.get(:music_session).should == [ValidationMessages::MUSIC_SESSION_MUST_BE_SPECIFIED] end it "join_music_session fails if approval_required and no invitation, but generates join_request" do diff --git a/ruby/spec/jam_ruby/models/mix_spec.rb b/ruby/spec/jam_ruby/models/mix_spec.rb index 91f3d0463..3ded0054a 100755 --- a/ruby/spec/jam_ruby/models/mix_spec.rb +++ b/ruby/spec/jam_ruby/models/mix_spec.rb @@ -9,7 +9,7 @@ describe Mix do @music_session = FactoryGirl.create(:music_session, :creator => @user, :musician_access => true) @music_session.connections << @connection @music_session.save - @recording = Recording.start(@music_session.id, @user) + @recording = Recording.start(@music_session, @user) @recording.stop @mix = Mix.schedule(@recording, "{}") end diff --git a/ruby/spec/jam_ruby/models/music_session_spec.rb b/ruby/spec/jam_ruby/models/music_session_spec.rb index 358cdbb09..a8c671208 100644 --- a/ruby/spec/jam_ruby/models/music_session_spec.rb +++ b/ruby/spec/jam_ruby/models/music_session_spec.rb @@ -394,5 +394,44 @@ describe MusicSession do music_session.valid?.should be_false end + it "is_recording? returns false if not recording" do + user1 = FactoryGirl.create(:user) + music_session = FactoryGirl.build(:music_session, :creator => user1) + music_session.is_recording?.should be_false + end + + describe "recordings" do + + before(:each) do + @user1 = FactoryGirl.create(:user) + @connection = FactoryGirl.create(:connection, :user => @user1) + @instrument = FactoryGirl.create(:instrument, :description => 'a great instrument') + @track = FactoryGirl.create(:track, :connection => @connection, :instrument => @instrument) + @music_session = FactoryGirl.create(:music_session, :creator => @user1, :musician_access => true) + @music_session.connections << @connection + @music_session.save + end + + describe "not recording" do + it "stop_recording should return nil if not recording" do + @music_session.stop_recording.should be_nil + end + end + + describe "currently recording" do + before(:each) do + @recording = FactoryGirl.create(:recording, :music_session => @music_session, :owner => @user1) + end + + it "is_recording? returns true if recording" do + @music_session.is_recording?.should be_true + end + + it "stop_recording should return recording object if recording" do + @music_session.stop_recording.should == @recording + end + + end + end end diff --git a/ruby/spec/jam_ruby/models/musician_search_spec.rb b/ruby/spec/jam_ruby/models/musician_search_spec.rb index a93fb1ea8..5ce018da5 100644 --- a/ruby/spec/jam_ruby/models/musician_search_spec.rb +++ b/ruby/spec/jam_ruby/models/musician_search_spec.rb @@ -79,11 +79,11 @@ describe User do music_session = FactoryGirl.create(:music_session, :creator => uu, :musician_access => true) music_session.connections << connection music_session.save - recording = Recording.start(music_session.id, uu) + recording = Recording.start(music_session, uu) recording.stop recording.reload genre = FactoryGirl.create(:genre) - recording.claim(uu, "name", genre, true, true) + recording.claim(uu, "name", "description", genre, true, true) recording.reload recording end diff --git a/ruby/spec/jam_ruby/models/recorded_track_spec.rb b/ruby/spec/jam_ruby/models/recorded_track_spec.rb index dcd374aaf..7f32a519e 100644 --- a/ruby/spec/jam_ruby/models/recorded_track_spec.rb +++ b/ruby/spec/jam_ruby/models/recorded_track_spec.rb @@ -6,8 +6,9 @@ describe RecordedTrack do @user = FactoryGirl.create(:user) @connection = FactoryGirl.create(:connection, :user => @user) @instrument = FactoryGirl.create(:instrument, :description => 'a great instrument') + @music_session = FactoryGirl.create(:music_session, :creator => @user, :musician_access => true) @track = FactoryGirl.create(:track, :connection => @connection, :instrument => @instrument) - @recording = FactoryGirl.create(:recording, :owner => @user) + @recording = FactoryGirl.create(:recording, :music_session => @music_session, :owner => @user) end it "should copy from a regular track properly" do @@ -17,6 +18,8 @@ describe RecordedTrack do @recorded_track.instrument.id.should == @track.instrument.id @recorded_track.next_part_to_upload.should == 0 @recorded_track.fully_uploaded.should == false + @recorded_track.client_id = @connection.client_id + @recorded_track.track_id = @track.id end it "should update the next part to upload properly" do @@ -38,11 +41,13 @@ describe RecordedTrack do it "properly finds a recorded track given its upload filename" do @recorded_track = RecordedTrack.create_from_track(@track, @recording) + @recorded_track.save.should be_true RecordedTrack.find_by_upload_filename("recording_#{@recorded_track.id}").should == @recorded_track end it "gets a url for the track" do @recorded_track = RecordedTrack.create_from_track(@track, @recording) + @recorded_track.save.should be_true @recorded_track.url.should == S3Manager.url(S3Manager.hashed_filename("recorded_track", @recorded_track.id)) end diff --git a/ruby/spec/jam_ruby/models/recording_spec.rb b/ruby/spec/jam_ruby/models/recording_spec.rb index ed1ce9a4e..e3565f712 100644 --- a/ruby/spec/jam_ruby/models/recording_spec.rb +++ b/ruby/spec/jam_ruby/models/recording_spec.rb @@ -5,23 +5,17 @@ describe Recording do before do S3Manager.set_unit_test @user = FactoryGirl.create(:user) - @connection = FactoryGirl.create(:connection, :user => @user) @instrument = FactoryGirl.create(:instrument, :description => 'a great instrument') + @music_session = FactoryGirl.create(: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) - @music_session = FactoryGirl.create(:music_session, :creator => @user, :musician_access => true) - @music_session.connections << @connection - @music_session.save - end - - it "should not start a recording if the music session doesnt exist" do - expect { Recording.start("bad_music_session_id", @user) }.to raise_error - end + end it "should set up the recording properly when recording is started with 1 user in the session" do - @music_session.recording.should == nil - @recording = Recording.start(@music_session.id, @user) + @music_session.is_recording?.should be_false + @recording = Recording.start(@music_session, @user) @music_session.reload - @music_session.recording.should == @recording + @music_session.recordings[0].should == @recording @recording.owner_id.should == @user.id @recorded_tracks = RecordedTrack.where(:recording_id => @recording.id) @@ -31,31 +25,34 @@ describe Recording do end it "should not start a recording if the session is already being recorded" do - Recording.start(@music_session.id, @user) - expect { Recording.start(@music_session.id, @user) }.to raise_error + Recording.start(@music_session, @user).errors.any?.should be_false + recording = Recording.start(@music_session, @user) + + recording.valid?.should_not be_true + recording.errors[:music_session].should_not be_nil end it "should return the state to normal properly when you stop a recording" do - @recording = Recording.start(@music_session.id, @user) + @recording = Recording.start(@music_session, @user) @recording.stop @music_session.reload - @music_session.recording.should == nil - @recording.reload - @recording.music_session.should == nil + @music_session.is_recording?.should be_false end it "should error when you stop a recording twice" do - @recording = Recording.start(@music_session.id, @user) + @recording = Recording.start(@music_session, @user) @recording.stop - expect { @recording.stop }.to raise_error + @recording.errors.any?.should be_false + @recording.stop + @recording.errors.any?.should be_true end it "should be able to start, stop then start a recording again for the same music session" do - @recording = Recording.start(@music_session.id, @user) + @recording = Recording.start(@music_session, @user) @recording.stop - @recording2 = Recording.start(@music_session.id, @user) - @music_session.recording.should == @recording2 + @recording2 = Recording.start(@music_session, @user) + @music_session.recordings.exists?(@recording2).should be_true end it "should NOT attach the recording to all users in a the music session when recording started" do @@ -66,7 +63,7 @@ describe Recording do @music_session.connections << @connection2 - @recording = Recording.start(@music_session.id, @user) + @recording = Recording.start(@music_session, @user) @user.recordings.length.should == 0 #@user.recordings.first.should == @recording @@ -75,7 +72,7 @@ describe Recording do end it "should report correctly whether its tracks have been uploaded" do - @recording = Recording.start(@music_session.id, @user) + @recording = Recording.start(@music_session, @user) @recording.uploaded?.should == false @recording.stop @recording.reload @@ -85,7 +82,7 @@ describe Recording do end it "should destroy a recording and all its recorded tracks properly" do - @recording = Recording.start(@music_session.id, @user) + @recording = Recording.start(@music_session, @user) @recording.stop @recording.reload @recorded_track = @recording.recorded_tracks.first @@ -95,11 +92,11 @@ describe Recording do end it "should allow a user to claim a recording" do - @recording = Recording.start(@music_session.id, @user) + @recording = Recording.start(@music_session, @user) @recording.stop @recording.reload @genre = FactoryGirl.create(:genre) - @recording.claim(@user, "name", @genre, true, true) + @recording.claim(@user, "name", "description", @genre, true, true) @recording.reload @recording.users.length.should == 1 @recording.users.first.should == @user @@ -108,56 +105,58 @@ describe Recording do @recording.claimed_recordings.length.should == 1 @claimed_recording = @recording.claimed_recordings.first @claimed_recording.name.should == "name" + @claimed_recording.description.should == "description" @claimed_recording.genre.should == @genre @claimed_recording.is_public.should == true @claimed_recording.is_downloadable.should == true end it "should fail if a user who was not in the session claims a recording" do - @recording = Recording.start(@music_session.id, @user) + @recording = Recording.start(@music_session, @user) @recording.stop @recording.reload user2 = FactoryGirl.create(:user) - expect { @recording.claim(user2) }.to raise_error + expect { @recording.claim(user2, "name", "description", @genre, true, true) }.to raise_error end it "should fail if a user tries to claim a recording twice" do - @recording = Recording.start(@music_session.id, @user) + @recording = Recording.start(@music_session, @user) @recording.stop @recording.reload @genre = FactoryGirl.create(:genre) - @recording.claim(@user, "name", @genre, true, true) + @recording.claim(@user, "name", "description", @genre, true, true) @recording.reload - expect { @recording.claim(@user, "name", @genre, true, true) }.to raise_error + expect { @recording.claim(@user, "name", "description", @genre, true, true) }.to raise_error end it "should allow editing metadata for claimed recordings" do - @recording = Recording.start(@music_session.id, @user) + @recording = Recording.start(@music_session, @user) @recording.stop @recording.reload @genre = FactoryGirl.create(:genre) - @claimed_recording = @recording.claim(@user, "name", @genre, true, true) + @claimed_recording = @recording.claim(@user, "name", "description", @genre, true, true) @genre2 = FactoryGirl.create(:genre) - @claimed_recording.update_fields(@user, :name => "name2", :genre => @genre2.id, :is_public => false, :is_downloadable => false) + @claimed_recording.update_fields(@user, :name => "name2", :description => "description2", :genre => @genre2.id, :is_public => false, :is_downloadable => false) @claimed_recording.reload @claimed_recording.name.should == "name2" + @claimed_recording.description.should == "description2" @claimed_recording.genre.should == @genre2 @claimed_recording.is_public.should == false @claimed_recording.is_downloadable.should == false end it "should only allow the owner to edit a claimed recording" do - @recording = Recording.start(@music_session.id, @user) + @recording = Recording.start(@music_session, @user) @recording.stop @recording.reload @genre = FactoryGirl.create(:genre) - @claimed_recording = @recording.claim(@user, "name", @genre, true, true) + @claimed_recording = @recording.claim(@user, "name", "description", @genre, true, true) @user2 = FactoryGirl.create(:user) expect { @claimed_recording.update_fields(@user2, "name2") }.to raise_error end it "should record the duration of the recording properly" do - @recording = Recording.start(@music_session.id, @user) + @recording = Recording.start(@music_session, @user) @recording.duration.should be_nil @recording.stop @recording.reload @@ -173,35 +172,35 @@ describe Recording do @track = FactoryGirl.create(:track, :connection => @connection2, :instrument => @instrument) @music_session.connections << @connection2 @music_session.save - @recording = Recording.start(@music_session.id, @user) + @recording = Recording.start(@music_session, @user) @recording.stop @recording.reload @genre = FactoryGirl.create(:genre) - @claimed_recording = @recording.claim(@user, "name", @genre, true, true) + @claimed_recording = @recording.claim(@user, "name", "description", @genre, true, true) expect { @claimed_recordign.discard(@user2) }.to raise_error - @claimed_recording = @recording.claim(@user2, "name2", @genre, true, true) + @claimed_recording = @recording.claim(@user2, "name2", "description2", @genre, true, true) @claimed_recording.discard(@user2) @recording.reload @recording.claimed_recordings.length.should == 1 end it "should destroy the entire recording if there was only one claimed_recording which is discarded" do - @recording = Recording.start(@music_session.id, @user) + @recording = Recording.start(@music_session, @user) @recording.stop @recording.reload @genre = FactoryGirl.create(:genre) - @claimed_recording = @recording.claim(@user, "name", @genre, true, true) + @claimed_recording = @recording.claim(@user, "name", "description", @genre, true, true) @claimed_recording.discard(@user) expect { Recording.find(@recording.id) }.to raise_error expect { ClaimedRecording.find(@claimed_recording.id) }.to raise_error end it "should return a file list for a user properly" do - @recording = Recording.start(@music_session.id, @user) + @recording = Recording.start(@music_session, @user) @recording.stop @recording.reload @genre = FactoryGirl.create(:genre) - @recording.claim(@user, "Recording", @genre, true, true) + @recording.claim(@user, "Recording", "Recording Description", @genre, true, true) Recording.list(@user)["downloads"].length.should == 0 Recording.list(@user)["uploads"].length.should == 1 file = Recording.list(@user)["uploads"].first @@ -241,7 +240,7 @@ describe Recording do @track2 = FactoryGirl.create(:track, :connection => @connection2, :instrument => @instrument2) @music_session.connections << @connection2 @music_session.save - @recording = Recording.start(@music_session.id, @user) + @recording = Recording.start(@music_session, @user) #sleep 4 @recording.stop @recording.recorded_tracks.length.should == 2 diff --git a/ruby/spec/jam_ruby/models/track_spec.rb b/ruby/spec/jam_ruby/models/track_spec.rb new file mode 100644 index 000000000..55bf5fd0a --- /dev/null +++ b/ruby/spec/jam_ruby/models/track_spec.rb @@ -0,0 +1,94 @@ +require 'spec_helper' + +describe Track do + + let (:connection) { FactoryGirl.create(:connection) } + let (:track) { FactoryGirl.create(:track, :connection => connection)} + let (:track2) { FactoryGirl.create(:track, :connection => connection)} + + let (:track_hash) { {:client_track_id => 'client_guid', :sound => 'stereo', :instrument_id => 'drums'} } + + before(:each) do + + end + + describe "sync" do + it "create one track" do + tracks = Track.sync(connection.client_id, [track_hash]) + tracks.length.should == 1 + track = tracks[0] + track.client_track_id.should == track_hash[:client_track_id] + track.sound = track_hash[:sound] + track.instrument.should == Instrument.find('drums') + end + + it "create two tracks" do + tracks = Track.sync(connection.client_id, [track_hash, track_hash]) + tracks.length.should == 2 + track = tracks[0] + track.client_track_id.should == track_hash[:client_track_id] + track.sound = track_hash[:sound] + track.instrument.should == Instrument.find('drums') + track = tracks[1] + track.client_track_id.should == track_hash[:client_track_id] + track.sound = track_hash[:sound] + track.instrument.should == Instrument.find('drums') + end + + it "delete only track" do + track.id.should_not be_nil + connection.tracks.length.should == 1 + tracks = Track.sync(connection.client_id, []) + tracks.length.should == 0 + end + + it "delete one of two tracks using .id to correlate" do + + track.id.should_not be_nil + track2.id.should_not be_nil + connection.tracks.length.should == 2 + tracks = Track.sync(connection.client_id, [{:id => track.id, :client_track_id => 'client_guid_new', :sound => 'mono', :instrument_id => 'drums'}]) + tracks.length.should == 1 + found = tracks[0] + found.id.should == track.id + found.sound.should == 'mono' + found.client_track_id.should == 'client_guid_new' + end + + it "delete one of two tracks using .client_track_id to correlate" do + + track.id.should_not be_nil + track2.id.should_not be_nil + connection.tracks.length.should == 2 + tracks = Track.sync(connection.client_id, [{:client_track_id => track.client_track_id, :sound => 'mono', :instrument_id => 'drums'}]) + tracks.length.should == 1 + found = tracks[0] + found.id.should == track.id + found.sound.should == 'mono' + found.client_track_id.should == track.client_track_id + end + + + it "updates a single track using .id to correlate" do + track.id.should_not be_nil + connection.tracks.length.should == 1 + tracks = Track.sync(connection.client_id, [{:id => track.id, :client_track_id => 'client_guid_new', :sound => 'mono', :instrument_id => 'drums'}]) + tracks.length.should == 1 + found = tracks[0] + found.id.should == track.id + found.sound.should == 'mono' + found.client_track_id.should == 'client_guid_new' + end + + it "updates a single track using .client_track_id to correlate" do + track.id.should_not be_nil + connection.tracks.length.should == 1 + tracks = Track.sync(connection.client_id, [{:client_track_id => track.client_track_id, :sound => 'mono', :instrument_id => 'drums'}]) + tracks.length.should == 1 + found = tracks[0] + found.id.should == track.id + found.sound.should == 'mono' + found.client_track_id.should == track.client_track_id + end + end +end \ No newline at end of file diff --git a/web/app/assets/javascripts/JamServer.js b/web/app/assets/javascripts/JamServer.js index 651ad616d..56d992936 100644 --- a/web/app/assets/javascripts/JamServer.js +++ b/web/app/assets/javascripts/JamServer.js @@ -104,7 +104,9 @@ payload = message[messageType], callbacks = server.dispatchTable[message.type]; - logger.log("server.onMessage:" + messageType + " payload:" + JSON.stringify(payload)); + if(message.type != context.JK.MessageType.HEARTBEAT_ACK) { + logger.log("server.onMessage:" + messageType + " payload:" + JSON.stringify(payload)); + } if (callbacks !== undefined) { var len = callbacks.length; @@ -113,6 +115,7 @@ callbacks[i](message, payload); } catch (ex) { logger.warn('exception in callback for websocket message:' + ex); + throw ex; } } } @@ -131,7 +134,9 @@ var jsMessage = JSON.stringify(message); - logger.log("server.send(" + jsMessage + ")"); + if(message.type != context.JK.MessageType.HEARTBEAT) { + logger.log("server.send(" + jsMessage + ")"); + } if (server !== undefined && server.socket !== undefined && server.socket.send !== undefined) { server.socket.send(jsMessage); } else { @@ -152,6 +157,11 @@ server.send(loginMessage); }; + /** with the advent of the reliable UDP channel, this is no longer how messages are sent from client-to-clent + * however, the mechanism still exists and is useful in test contexts; and maybe in the future + * @param receiver_id client ID of message to send + * @param message the actual message + */ server.sendP2PMessage = function(receiver_id, message) { logger.log("P2P message from [" + server.clientID + "] to [" + receiver_id + "]: " + message); var outgoing_msg = msg_factory.client_p2p_message(server.clientID, receiver_id, message); @@ -192,4 +202,5 @@ } + })(window, jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/addTrack.js b/web/app/assets/javascripts/addTrack.js index fdd3cb56f..e40732a31 100644 --- a/web/app/assets/javascripts/addTrack.js +++ b/web/app/assets/javascripts/addTrack.js @@ -71,7 +71,6 @@ // set arrays inputUnassignedList = _loadList(ASSIGNMENT.UNASSIGNED, true, false); - console.log("inputUnassignedList: " + JSON.stringify(inputUnassignedList)); track2AudioInputChannels = _loadList(ASSIGNMENT.TRACK2, true, false); } @@ -125,18 +124,25 @@ } function saveSettings() { + if (!context.JK.verifyNotRecordingForTrackChange(app)) { + return; + } + if (!validateSettings()) { return; } saveTrack(); + app.layout.closeDialog('add-track'); } function saveTrack() { // TRACK 2 INPUTS + var trackId = null; $("#add-track2-input > option").each(function() { logger.debug("Saving track 2 input = " + this.value); + trackId = this.value; context.jamClient.TrackSetAssignment(this.value, true, ASSIGNMENT.TRACK2); }); @@ -150,12 +156,52 @@ // UPDATE SERVER logger.debug("Adding track with instrument " + instrumentText); var data = {}; - // use the first track's connection_id (not sure why we need this on the track data model) - logger.debug("myTracks[0].connection_id=" + myTracks[0].connection_id); - data.connection_id = myTracks[0].connection_id; - data.instrument_id = instrumentText; - data.sound = "stereo"; - sessionModel.addTrack(sessionId, data); + + context.jamClient.TrackSaveAssignments(); + + /** + setTimeout(function() { + var inputTracks = context.JK.TrackHelpers.getTracks(context.jamClient, 2); + + // this is some ugly logic coming up, here's why: + // we need the id (guid) that the backend generated for the new track we just added + // to get it, we need to make sure 2 tracks come back, and then grab the track that + // is not the one we just added. + if(inputTracks.length != 2) { + var msg = "because we just added a track, there should be 2 available, but we found: " + inputTracks.length; + logger.error(msg); + alert(msg); + throw new Error(msg); + } + + var client_track_id = null; + $.each(inputTracks, function(index, track) { + + + console.log("track: %o, myTrack: %o", track, myTracks[0]); + if(track.id != myTracks[0].id) { + client_track_id = track.id; + return false; + } + }); + + if(client_track_id == null) + { + var msg = "unable to find matching backend track for id: " + this.value; + logger.error(msg); + alert(msg); + throw new Error(msg); + } + + // use the first track's connection_id (not sure why we need this on the track data model) + data.connection_id = myTracks[0].connection_id; + data.instrument_id = instrumentText; + data.sound = "stereo"; + data.client_track_id = client_track_id; + sessionModel.addTrack(sessionId, data); + }, 1000); + + */ } function validateSettings() { diff --git a/web/app/assets/javascripts/application.js b/web/app/assets/javascripts/application.js index 91cc6c0f6..581b7306a 100644 --- a/web/app/assets/javascripts/application.js +++ b/web/app/assets/javascripts/application.js @@ -18,4 +18,5 @@ //= require jquery.Jcrop //= require jquery.naturalsize //= require jquery.queryparams +//= require globals //= require_directory . diff --git a/web/app/assets/javascripts/configureTrack.js b/web/app/assets/javascripts/configureTrack.js index c2d9b8fb0..94feb2fc8 100644 --- a/web/app/assets/javascripts/configureTrack.js +++ b/web/app/assets/javascripts/configureTrack.js @@ -213,7 +213,6 @@ // remove option 1 from voice chat type dropdown if no music (based on what's unused on the Music Audio tab) or chat inputs are available if ($('#audio-inputs-unused > option').size() === 0 && chatOtherUnassignedList.length === 0 && chatOtherAssignedList.length === 0) { - logger.debug("Removing Option 1 from Voice Chat dropdown."); $option1.remove(); } else { @@ -353,6 +352,7 @@ // load Audio Driver dropdown devices = context.jamClient.TrackGetDevices(); + logger.debug("Called TrackGetDevices with response " + JSON.stringify(devices)); var keys = Object.keys(devices); for (var i=0; i < keys.length; i++) { @@ -471,13 +471,13 @@ function _initMusicTabData() { inputUnassignedList = _loadList(ASSIGNMENT.UNASSIGNED, true, false); - logger.debug("inputUnassignedList=" + JSON.stringify(inputUnassignedList)); + //logger.debug("inputUnassignedList=" + JSON.stringify(inputUnassignedList)); track1AudioInputChannels = _loadList(ASSIGNMENT.TRACK1, true, false); - logger.debug("track1AudioInputChannels=" + JSON.stringify(track1AudioInputChannels)); + //logger.debug("track1AudioInputChannels=" + JSON.stringify(track1AudioInputChannels)); track2AudioInputChannels = _loadList(ASSIGNMENT.TRACK2, true, false); - logger.debug("track2AudioInputChannels=" + JSON.stringify(track2AudioInputChannels)); + //logger.debug("track2AudioInputChannels=" + JSON.stringify(track2AudioInputChannels)); outputUnassignedList = _loadList(ASSIGNMENT.UNASSIGNED, false, false); outputAssignedList = _loadList(ASSIGNMENT.OUTPUT, false, false); @@ -485,16 +485,16 @@ function _initVoiceChatTabData() { chatUnassignedList = _loadList(ASSIGNMENT.UNASSIGNED, true, false); - logger.debug("chatUnassignedList=" + JSON.stringify(chatUnassignedList)); + //logger.debug("chatUnassignedList=" + JSON.stringify(chatUnassignedList)); chatAssignedList = _loadList(ASSIGNMENT.CHAT, true, false); - logger.debug("chatAssignedList=" + JSON.stringify(chatAssignedList)); + //logger.debug("chatAssignedList=" + JSON.stringify(chatAssignedList)); chatOtherUnassignedList = _loadList(ASSIGNMENT.UNASSIGNED, true, true); - logger.debug("chatOtherUnassignedList=" + JSON.stringify(chatOtherUnassignedList)); + //logger.debug("chatOtherUnassignedList=" + JSON.stringify(chatOtherUnassignedList)); chatOtherAssignedList = _loadList(ASSIGNMENT.CHAT, true, true); - logger.debug("chatOtherAssignedList=" + JSON.stringify(chatOtherAssignedList)); + //logger.debug("chatOtherAssignedList=" + JSON.stringify(chatOtherAssignedList)); } // TODO: copied in addTrack.js - refactor to common place @@ -548,6 +548,10 @@ } function saveSettings() { + if (!context.JK.verifyNotRecordingForTrackChange(app)) { + return; + } + if (!validateAudioSettings(false)) { return; } @@ -563,9 +567,6 @@ originalDeviceId = $('#audio-drivers').val(); app.layout.closeDialog('configure-audio'); - - // refresh Session screen - sessionModel.refreshCurrentSession(); } function saveAudioSettings() { @@ -591,11 +592,6 @@ // logger.debug("Saving track 1 instrument = " + instrumentVal); context.jamClient.TrackSetInstrument(ASSIGNMENT.TRACK1, instrumentVal); - // UPDATE SERVER - logger.debug("Updating track " + myTracks[0].trackId + " with instrument " + instrumentText); - var data = {}; - data.instrument_id = instrumentText; - sessionModel.updateTrack(sessionId, myTracks[0].trackId, data); // TRACK 2 INPUTS var track2Selected = false; @@ -609,25 +605,6 @@ // TRACK 2 INSTRUMENT instrumentVal = $('#track2-instrument').val(); instrumentText = $('#track2-instrument > option:selected').text().toLowerCase(); - - // track 2 new - add - if (myTrackCount === 1) { - data = {}; - // use the first track's connection_id (not sure why we need this on the track data model) - logger.debug("myTracks[0].connection_id=" + myTracks[0].connection_id); - data.connection_id = myTracks[0].connection_id; - data.instrument_id = instrumentText; - data.sound = "stereo"; - sessionModel.addTrack(sessionId, data); - } - // track 2 exists - update - else if (myTrackCount === 2) { - // UPDATE SERVER - logger.debug("Updating track " + myTracks[1].trackId + " with instrument " + instrumentText); - data = {}; - data.instrument_id = instrumentText; - sessionModel.updateTrack(sessionId, myTracks[1].trackId, data); - } logger.debug("Saving track 2 instrument = " + instrumentVal); context.jamClient.TrackSetInstrument(ASSIGNMENT.TRACK2, instrumentVal); @@ -636,7 +613,8 @@ // track 2 was removed if (myTrackCount === 2) { logger.debug("Deleting track " + myTracks[1].trackId); - sessionModel.deleteTrack(sessionId, myTracks[1].trackId); + client.TrackSetCount(1); + //sessionModel.deleteTrack(sessionId, myTracks[1].trackId); } } @@ -651,6 +629,8 @@ logger.debug("Saving session audio output = " + this.value); context.jamClient.TrackSetAssignment(this.value, false, ASSIGNMENT.OUTPUT); }); + + context.jamClient.TrackSaveAssignments(); } function saveVoiceChatSettings() { @@ -799,6 +779,14 @@ } function _init() { + var dialogBindings = { + 'beforeShow' : function() { + return context.JK.verifyNotRecordingForTrackChange(app); + } + }; + app.bindDialog('configure-audio', dialogBindings); + + // load instrument array for populating listboxes, using client_id in instrument_map as ID context.JK.listInstruments(app, function(instruments) { $.each(instruments, function(index, val) { @@ -807,7 +795,6 @@ }); originalVoiceChat = context.jamClient.TrackGetChatEnable() ? VOICE_CHAT.CHAT : VOICE_CHAT.NO_CHAT; - logger.debug("originalVoiceChat=" + originalVoiceChat); $('#voice-chat-type').val(originalVoiceChat); @@ -820,7 +807,6 @@ // remove option 1 from voice chat if none are available and not already assigned if (inputUnassignedList.length === 0 && chatAssignedList.length === 0 && chatOtherAssignedList.length === 0 && chatOtherUnassignedList.length === 0) { - logger.debug("Removing Option 1 from Voice Chat dropdown."); $option1.remove(); } // add it if it doesn't exist @@ -836,7 +822,6 @@ events(); _init(); myTrackCount = myTracks.length; - logger.debug("initialize:myTrackCount=" + myTrackCount); }; this.showMusicAudioPanel = showMusicAudioPanel; diff --git a/web/app/assets/javascripts/fakeJamClient.js b/web/app/assets/javascripts/fakeJamClient.js index 7c73e4f72..ab06fdd5b 100644 --- a/web/app/assets/javascripts/fakeJamClient.js +++ b/web/app/assets/javascripts/fakeJamClient.js @@ -3,7 +3,7 @@ "use strict"; context.JK = context.JK || {}; - context.JK.FakeJamClient = function(app) { + context.JK.FakeJamClient = function(app, p2pMessageFactory) { var logger = context.JK.logger; logger.info("*** Fake JamClient instance initialized. ***"); @@ -18,6 +18,8 @@ var device_id = -1; var latencyCallback = null; var frameSize = 2.5; + var fakeJamClientRecordings = null; + var p2pCallbacks = null; function dbg(msg) { logger.debug('FakeJamClient: ' + msg); } @@ -142,7 +144,31 @@ function LatencyUpdated(map) { dbg('LatencyUpdated:' + JSON.stringify(map)); } function LeaveSession(map) { dbg('LeaveSession:' + JSON.stringify(map)); } - function P2PMessageReceived(s1,s2) { dbg('P2PMessageReceived:' + s1 + ',' + s2); } + + // this is not a real bridge method; purely used by the fake jam client + function RegisterP2PMessageCallbacks(callbacks) { + p2pCallbacks = callbacks; + } + + function P2PMessageReceived(from, payload) { + + dbg('P2PMessageReceived'); + // this function is different in that the payload is a JSON ready string; + // whereas a real p2p message is base64 encoded binary packaged data + + try { + payload = JSON.parse(payload); + } + catch(e) { + logger.warn("unable to parse payload as JSON from client %o, %o, %o", from, e, payload); + } + + var callback = p2pCallbacks[payload.type]; + if(callback) { + callback(from, payload); + } + } + function JoinSession(sessionId) {dbg('JoinSession:' + sessionId);} function ParticipantLeft(session, participant) { dbg('ParticipantLeft:' + JSON.stringify(session) + ',' + @@ -167,9 +193,21 @@ } function StartPlayTest(s) { dbg('StartPlayTest' + JSON.stringify(arguments)); } function StartRecordTest(s) { dbg('StartRecordTest' + JSON.stringify(arguments)); } - function StartRecording(map) { dbg('StartRecording' + JSON.stringify(arguments)); } + function StartRecording(recordingId, groupedClientTracks) { + dbg('StartRecording'); + fakeJamClientRecordings.StartRecording(recordingId, groupedClientTracks); + } function StopPlayTest() { dbg('StopPlayTest'); } - function StopRecording(map) { dbg('StopRecording' + JSON.stringify(arguments)); } + function StopRecording(recordingId, groupedTracks, errorReason, detail) { + dbg('StopRecording'); + fakeJamClientRecordings.StopRecording(recordingId, groupedTracks, errorReason, detail); + } + + function AbortRecording(recordingId, errorReason, errorDetail) { + dbg('AbortRecording'); + fakeJamClientRecordings.AbortRecording(recordingId, errorReason, errorDetail); + } + function TestASIOLatency(s) { dbg('TestASIOLatency' + JSON.stringify(arguments)); } function TestLatency(clientID, callbackFunctionName, timeoutCallbackName) { @@ -244,6 +282,11 @@ "User@208.191.152.98_*" ]; } + + function RegisterRecordingCallbacks(startRecordingCallbackName, stopRecordingCallbackName, startedRecordingCallbackName, stoppedRecordingCallbackName, abortedRecordingCallbackName) { + fakeJamClientRecordings.RegisterRecordingCallbacks(startRecordingCallbackName, stopRecordingCallbackName, startedRecordingCallbackName,stoppedRecordingCallbackName, abortedRecordingCallbackName); + } + function SessionRegisterCallback(callbackName) { eventCallbackName = callbackName; if (callbackTimer) { context.clearInterval(callbackTimer); } @@ -483,8 +526,18 @@ } function ClientUpdateStartUpdate(path, successCallback, failureCallback) {} + // ------------------------------- + // fake jam client methods + // not a part of the actual bridge + // ------------------------------- + function SetFakeRecordingImpl(fakeRecordingsImpl) { + fakeJamClientRecordings = fakeRecordingsImpl; + } + + // Javascript Bridge seems to camel-case // Set the instance functions: + this.AbortRecording = AbortRecording; this.GetASIODevices = GetASIODevices; this.GetOS = GetOS; this.GetOSAsString = GetOSAsString; @@ -548,6 +601,7 @@ this.SessionAddTrack = SessionAddTrack; this.SessionGetControlState = SessionGetControlState; this.SessionGetIDs = SessionGetIDs; + this.RegisterRecordingCallbacks = RegisterRecordingCallbacks; this.SessionRegisterCallback = SessionRegisterCallback; this.SessionSetAlertCallback = SessionSetAlertCallback; this.SessionSetControlState = SessionSetControlState; @@ -596,6 +650,10 @@ this.ClientUpdateStartUpdate = ClientUpdateStartUpdate; this.OpenSystemBrowser = OpenSystemBrowser; + + // fake calls; not a part of the actual jam client + this.RegisterP2PMessageCallbacks = RegisterP2PMessageCallbacks; + this.SetFakeRecordingImpl = SetFakeRecordingImpl; }; })(window,jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/fakeJamClientMessages.js b/web/app/assets/javascripts/fakeJamClientMessages.js new file mode 100644 index 000000000..90792bf09 --- /dev/null +++ b/web/app/assets/javascripts/fakeJamClientMessages.js @@ -0,0 +1,78 @@ +(function(context,$) { + + "use strict"; + + context.JK = context.JK || {}; + context.JK.FakeJamClientMessages = function() { + + var self = this; + + function startRecording(recordingId) { + var msg = {}; + msg.type = self.Types.START_RECORDING; + msg.msgId = context.JK.generateUUID(); + msg.recordingId = recordingId; + return msg; + } + + function startRecordingAck(recordingId, success, reason, detail) { + var msg = {}; + msg.type = self.Types.START_RECORDING_ACK; + msg.msgId = context.JK.generateUUID(); + msg.recordingId = recordingId; + msg.success = success; + msg.reason = reason; + msg.detail = detail; + return msg; + } + + function stopRecording(recordingId, success, reason, detail) { + var msg = {}; + msg.type = self.Types.STOP_RECORDING; + msg.msgId = context.JK.generateUUID(); + msg.recordingId = recordingId; + msg.success = success === undefined ? true : success; + msg.reason = reason; + msg.detail = detail; + return msg; + } + + function stopRecordingAck(recordingId, success, reason, detail) { + var msg = {}; + msg.type = self.Types.STOP_RECORDING_ACK; + msg.msgId = context.JK.generateUUID(); + msg.recordingId = recordingId; + msg.success = success; + msg.reason = reason; + msg.detail = detail; + return msg; + } + + function abortRecording(recordingId, reason, detail) { + var msg = {}; + msg.type = self.Types.ABORT_RECORDING; + msg.msgId = context.JK.generateUUID(); + msg.recordingId = recordingId; + msg.success = false; + msg.reason = reason; + msg.detail = detail; + return msg; + } + + this.Types = {}; + this.Types.START_RECORDING = 'start_recording'; + this.Types.START_RECORDING_ACK = 'start_recording_ack'; + this.Types.STOP_RECORDING = 'stop_recording;' + this.Types.STOP_RECORDING_ACK = 'stop_recording_ack'; + this.Types.ABORT_RECORDING = 'abort_recording'; + + this.startRecording = startRecording; + this.startRecordingAck = startRecordingAck; + this.stopRecording = stopRecording; + this.stopRecordingAck = stopRecordingAck; + this.abortRecording = abortRecording; + } + + + +})(window, jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/fakeJamClientRecordings.js b/web/app/assets/javascripts/fakeJamClientRecordings.js new file mode 100644 index 000000000..b48ad335b --- /dev/null +++ b/web/app/assets/javascripts/fakeJamClientRecordings.js @@ -0,0 +1,255 @@ +// this code simulates what the actual backend recording feature will do +(function(context, $) { + + + "use strict"; + + context.JK = context.JK || {}; + context.JK.FakeJamClientRecordings = function(app, fakeJamClient, p2pMessageFactory) { + + var logger = context.JK.logger; + + var startRecordingResultCallbackName = null; + var stopRecordingResultCallbackName = null; + var startedRecordingResultCallbackName = null; + var stoppedRecordingEventCallbackName = null; + var abortedRecordingEventCallbackName = null; + + var startingSessionState = null; + var stoppingSessionState = null; + + var currentRecordingId = null; + var currentRecordingCreatorClientId = null; + var currentRecordingClientIds = null; + + function timeoutStartRecordingTimer() { + eval(startRecordingResultCallbackName).call(this, startingSessionState.recordingId, {success:false, reason:'client-no-response', detail:startingSessionState.groupedClientTracks[0]}); + startingSessionState = null; + } + + function timeoutStopRecordingTimer() { + eval(stopRecordingResultCallbackName).call(this, stoppingSessionState.recordingId, {success:false, reason:'client-no-response', detail:stoppingSessionState.groupedClientTracks[0]}); + } + + function StartRecording(recordingId, clients) { + startingSessionState = {}; + + // we expect all clients to respond within 3 seconds to mimic the reliable UDP layer + startingSessionState.aggegratingStartResultsTimer = setTimeout(timeoutStartRecordingTimer, 3000); + startingSessionState.recordingId = recordingId; + startingSessionState.groupedClientTracks = copyClientIds(clients, app.clientId); // we will manipulate this new one + + // store the current recording's data + currentRecordingId = recordingId; + currentRecordingCreatorClientId = app.clientId; + currentRecordingClientIds = copyClientIds(clients, app.clientId); + + if(startingSessionState.groupedClientTracks.length == 0) { + // if there are no clients but 'self', then you can declare a successful recording immediately + finishSuccessfulStart(recordingId); + } + else { + // signal all other connected clients that the recording has started + for(var i = 0; i < startingSessionState.groupedClientTracks.length; i++) { + var clientId = startingSessionState.groupedClientTracks[i]; + context.JK.JamServer.sendP2PMessage(clientId, JSON.stringify(p2pMessageFactory.startRecording(recordingId))); + } + } + } + + function StopRecording(recordingId, clients, result) { + + if(startingSessionState) { + // we are currently starting a session. + // TODO + } + + if(!result) { + result = {success:true} + } + + stoppingSessionState = {}; + + // we expect all clients to respond within 3 seconds to mimic the reliable UDP layer + stoppingSessionState.aggegratingStopResultsTimer = setTimeout(timeoutStopRecordingTimer, 3000); + stoppingSessionState.recordingId = recordingId; + stoppingSessionState.groupedClientTracks = copyClientIds(clients, app.clientId); + + if(stoppingSessionState.groupedClientTracks.length == 0) { + finishSuccessfulStop(recordingId); + } + else { + // signal all other connected clients that the recording has stopped + for(var i = 0; i < stoppingSessionState.groupedClientTracks.length; i++) { + var clientId = stoppingSessionState.groupedClientTracks[i]; + context.JK.JamServer.sendP2PMessage(clientId, JSON.stringify(p2pMessageFactory.stopRecording(recordingId, result.success, result.reason, result.detail))); + } + } + } + + function AbortRecording(recordingId, errorReason, errorDetail) { + // todo check recordingId + context.JK.JamServer.sendP2PMessage(currentRecordingCreatorClientId, JSON.stringify(p2pMessageFactory.abortRecording(recordingId, errorReason, errorDetail))); + } + + function onStartRecording(from, payload) { + logger.debug("received start recording request from " + from); + if(context.JK.CurrentSessionModel.recordingModel.isRecording()) { + // reject the request to start the recording + context.JK.JamServer.sendP2PMessage(from, JSON.stringify(p2pMessageFactory.startRecordingAck(payload.recordingId, false, "already-recording", null))); + } + else { + // accept the request, and then tell the frontend we are now recording + // a better client implementation would verify that the tracks specified match that what we have configured currently + + // store the current recording's data + currentRecordingId = payload.recordingId; + currentRecordingCreatorClientId = from; + + context.JK.JamServer.sendP2PMessage(from, JSON.stringify(p2pMessageFactory.startRecordingAck(payload.recordingId, true, null, null))); + eval(startedRecordingResultCallbackName).call(this, payload.recordingId, {success:true}, from); + } + } + + function onStartRecordingAck(from, payload) { + logger.debug("received start recording ack from " + from); + + // we should check transactionId; this could be an ACK for a different recording + if(startingSessionState) { + + if(payload.success) { + var index = startingSessionState.groupedClientTracks.indexOf(from); + startingSessionState.groupedClientTracks.splice(index, 1); + + if(startingSessionState.groupedClientTracks.length == 0) { + finishSuccessfulStart(payload.recordingId); + } + } + else { + // TOOD: a client responded with error; we need to tell all other clients to abandon recording + logger.warn("received an unsuccessful start_record_ack from: " + from); + } + } + else { + logger.warn("received a start_record_ack when there is no recording starting from: " + from); + // TODO: this is an error case; we should signal back to the sender that we gave up + } + } + + function onStopRecording(from, payload) { + logger.debug("received stop recording request from " + from); + + // TODO check recordingId, and if currently recording + // we should return success if we are currently recording, or if we were already asked to stop for this recordingId + // this means we should keep a list of the last N recordings that we've seen, rather than just keeping the current + context.JK.JamServer.sendP2PMessage(from, JSON.stringify(p2pMessageFactory.stopRecordingAck(payload.recordingId, true))); + + eval(stopRecordingResultCallbackName).call(this, payload.recordingId, {success:payload.success, reason:payload.reason, detail:from}); + } + + function onStopRecordingAck(from, payload) { + logger.debug("received stop recording ack from " + from); + + // we should check transactionId; this could be an ACK for a different recording + if(stoppingSessionState) { + + if(payload.success) { + var index = stoppingSessionState.groupedClientTracks.indexOf(from); + stoppingSessionState.groupedClientTracks.splice(index, 1); + + if(stoppingSessionState.groupedClientTracks.length == 0) { + finishSuccessfulStop(payload.recordingId); + } + } + else { + // TOOD: a client responded with error; what now? + logger.error("client responded with error: ", payload); + } + } + else { + // TODO: this is an error case; we should tell the caller we have no recording at the moment + } + } + + function onAbortRecording(from, payload) { + logger.debug("received abort recording from " + from); + + // TODO check if currently recording and if matches payload.recordingId + + // if creator, tell everyone else to stop + if(app.clientId == currentRecordingCreatorClientId) { + // ask the front end to stop the recording because it has the full track listing + for(var i = 0; i < currentRecordingClientIds.length; i++) { + var clientId = currentRecordingClientIds[i]; + context.JK.JamServer.sendP2PMessage(clientId, JSON.stringify(p2pMessageFactory.abortRecording(currentRecordingId, payload.reason, from))); + } + + } + else { + logger.debug("only the creator currently deals with the abort request. abort request sent from:" + from + " with a reason of: " + payload.errorReason); + } + + eval(abortedRecordingEventCallbackName).call(this, payload.recordingId, {success:payload.success, reason:payload.reason, detail:from}); + } + + function RegisterRecordingCallbacks(startRecordingCallbackName, + stopRecordingCallbackName, + startedRecordingCallbackName, + stoppedRecordingCallbackName, + abortedRecordingCallbackName) { + startRecordingResultCallbackName = startRecordingCallbackName; + stopRecordingResultCallbackName = stopRecordingCallbackName; + startedRecordingResultCallbackName = startedRecordingCallbackName; + stoppedRecordingEventCallbackName = stoppedRecordingCallbackName; + abortedRecordingEventCallbackName = abortedRecordingCallbackName; + } + + // copies all clientIds, but removes current client ID because we don't want to message that user + function copyClientIds(clientIds, myClientId) { + var newClientIds = []; + for(var i = 0; i < clientIds.length; i++) { + var clientId = clientIds[i] + if(clientId != myClientId) { + newClientIds.push(clientId); + } + } + return newClientIds; + } + + function finishSuccessfulStart(recordingId) { + // all clients have responded. + clearTimeout(startingSessionState.aggegratingStartResultsTimer); + startingSessionState = null; + eval(startRecordingResultCallbackName).call(this, recordingId, {success:true}); + } + + function finishSuccessfulStop(recordingId, errorReason) { + // all clients have responded. + clearTimeout(stoppingSessionState.aggegratingStopResultsTimer); + stoppingSessionState = null; + var result = { success: true } + if(errorReason) + { + result.success = false; + result.reason = errorReason + result.detail = "" + } + eval(stopRecordingResultCallbackName).call(this, recordingId, result); + } + + + // register for p2p callbacks + var callbacks = {}; + callbacks[p2pMessageFactory.Types.START_RECORDING] = onStartRecording; + callbacks[p2pMessageFactory.Types.START_RECORDING_ACK] = onStartRecordingAck; + callbacks[p2pMessageFactory.Types.STOP_RECORDING] = onStopRecording; + callbacks[p2pMessageFactory.Types.STOP_RECORDING_ACK] = onStopRecordingAck; + callbacks[p2pMessageFactory.Types.ABORT_RECORDING] = onAbortRecording; + fakeJamClient.RegisterP2PMessageCallbacks(callbacks); + this.StartRecording = StartRecording; + this.StopRecording = StopRecording; + this.AbortRecording = AbortRecording; + this.RegisterRecordingCallbacks = RegisterRecordingCallbacks; + } + + })(window, jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/ftue.js b/web/app/assets/javascripts/ftue.js index 21414246b..0c6216695 100644 --- a/web/app/assets/javascripts/ftue.js +++ b/web/app/assets/javascripts/ftue.js @@ -253,6 +253,16 @@ jamClient.TrackSetChatEnable(false); } + var defaultInstrumentId; + if (context.JK.userMe.instruments && context.JK.userMe.instruments.length > 0) { + defaultInstrumentId = context.JK.instrument_id_to_instrument[context.JK.userMe.instruments[0].instrument_id].client_id; + } + else { + defaultInstrumentId = context.JK.server_to_client_instrument_map['Other'].client_id; + } + + jamClient.TrackSetInstrument(1, defaultInstrumentId); + logger.debug("Calling FTUESave(" + persist + ")"); var response = jamClient.FTUESave(persist); setLevels(0); @@ -260,7 +270,6 @@ logger.warn(response); // TODO - we may need to do something about errors on save. // per VRFS-368, I'm hiding the alert, and logging a warning. - // context.alert(response); } } else { logger.debug("Aborting FTUESave as we need input + output selected."); @@ -684,13 +693,10 @@ } function setAsioSettingsVisibility() { - logger.debug("jamClient.FTUEHasControlPanel()=" + jamClient.FTUEHasControlPanel()); if (jamClient.FTUEHasControlPanel()) { - logger.debug("Showing ASIO button"); $('#btn-asio-control-panel').show(); } else { - logger.debug("Hiding ASIO button"); $('#btn-asio-control-panel').hide(); } } diff --git a/web/app/assets/javascripts/globals.js b/web/app/assets/javascripts/globals.js index 117e99960..1c47e8927 100644 --- a/web/app/assets/javascripts/globals.js +++ b/web/app/assets/javascripts/globals.js @@ -72,4 +72,17 @@ 240: { "server_id": "mandolin" }, 250: { "server_id": "other" } }; + + context.JK.instrument_id_to_instrument = {}; + + (function() { + $.each(context.JK.server_to_client_instrument_map, function(key, value) { + context.JK.instrument_id_to_instrument[value.server_id] = { client_id: value.client_id, display: key } + }); + })(); + + + context.JK.entityToPrintable = { + music_session: "music session" + } })(window,jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/jam_rest.js b/web/app/assets/javascripts/jam_rest.js index 341e0d284..d0dafc98d 100644 --- a/web/app/assets/javascripts/jam_rest.js +++ b/web/app/assets/javascripts/jam_rest.js @@ -13,7 +13,6 @@ var logger = context.JK.logger; function createJoinRequest(joinRequest) { - logger.debug("joinRequest=" + JSON.stringify(joinRequest)); return $.ajax({ type: "POST", dataType: "json", @@ -316,6 +315,53 @@ }); } + function startRecording(options) { + return $.ajax({ + type: "POST", + dataType: "json", + contentType: 'application/json', + url: "/api/recordings/start", + data: JSON.stringify(options) + }) + } + + function stopRecording(options) { + var recordingId = options["id"] + + return $.ajax({ + type: "POST", + dataType: "json", + contentType: 'application/json', + url: "/api/recordings/" + recordingId + "/stop", + data: JSON.stringify(options) + }) + } + + function getRecording(options) { + var recordingId = options["id"]; + + return $.ajax({ + type: "GET", + dataType: "json", + contentType: 'application/json', + url: "/api/recordings/" + recordingId + }) + } + + function putTrackSyncChange(options) { + var musicSessionId = options["id"] + delete options["id"]; + + return $.ajax({ + type: "PUT", + dataType: "json", + url: '/api/sessions/' + musicSessionId + '/tracks', + contentType: 'application/json', + processData: false, + data: JSON.stringify(options) + }); + } + function initialize() { return self; } @@ -346,6 +392,10 @@ this.createJoinRequest = createJoinRequest; this.updateJoinRequest = updateJoinRequest; this.updateUser = updateUser; + this.startRecording = startRecording; + this.stopRecording = stopRecording; + this.getRecording = getRecording; + this.putTrackSyncChange = putTrackSyncChange; return this; }; diff --git a/web/app/assets/javascripts/jamkazam.js b/web/app/assets/javascripts/jamkazam.js index 61da06700..cc204795e 100644 --- a/web/app/assets/javascripts/jamkazam.js +++ b/web/app/assets/javascripts/jamkazam.js @@ -175,7 +175,38 @@ */ function ajaxError(jqXHR, textStatus, errorMessage) { logger.error("Unexpected ajax error: " + textStatus); - app.notify({title: textStatus, text: errorMessage, detail: jqXHR.responseText}); + + if(jqXHR.status == 404) { + app.notify({title: "Oops!", text: "What you were looking for is gone now."}); + } + else if(jqXHR.status = 422) { + // present a nicer message + try { + var text = "