diff --git a/db/manifest b/db/manifest index 4bc66447b..e778d96da 100755 --- a/db/manifest +++ b/db/manifest @@ -74,3 +74,4 @@ crash_dumps_idx.sql music_sessions_user_history_add_session_removed_at.sql user_progress_tracking.sql whats_next.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..fcab08d92 --- /dev/null +++ b/db/up/recordings_public_launch.sql @@ -0,0 +1,30 @@ +-- so that columns can live on +ALTER TABLE recordings DROP CONSTRAINT "recordings_music_session_id_fkey"; +ALTER TABLE recordings ADD COLUMN is_done BOOLEAN DEFAULT FALSE; + +--ALTER TABLE music_session ADD COLUMN is_recording BOOLEAN DEFAULT FALSE; + +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); + +--ALTER TABLE recordings ADD COLUMN is_kept BOOLEAN NOT NULL DEFAULT false; + +--ALTER TABLE recordings ADD COLUMN is_public BOOLEAN NOT NULL DEFAULT true; +--ALTER TABLE recordings ADD COLUMN is_downloadable BOOLEAN NOT NULL DEFAULT true; +--ALTER TABLE recordings ADD COLUMN genre_id VARCHAR(64) NOT NULL REFERENCES genres(id); + +-- 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; 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/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/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 d66cfbd5a..881cc6690 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 @@ -71,37 +66,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, ValidationMesages::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 @@ -115,7 +113,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/spec/jam_ruby/connection_manager_spec.rb b/ruby/spec/jam_ruby/connection_manager_spec.rb index 0056e9486..cc673f0d4 100644 --- a/ruby/spec/jam_ruby/connection_manager_spec.rb +++ b/ruby/spec/jam_ruby/connection_manager_spec.rb @@ -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/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/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/web/app/assets/javascripts/JamServer.js b/web/app/assets/javascripts/JamServer.js index 651ad616d..13f1329ef 100644 --- a/web/app/assets/javascripts/JamServer.js +++ b/web/app/assets/javascripts/JamServer.js @@ -113,6 +113,7 @@ callbacks[i](message, payload); } catch (ex) { logger.warn('exception in callback for websocket message:' + ex); + throw ex; } } } @@ -152,6 +153,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 +198,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..eae6c5f78 100644 --- a/web/app/assets/javascripts/addTrack.js +++ b/web/app/assets/javascripts/addTrack.js @@ -125,6 +125,10 @@ } function saveSettings() { + if (!context.JK.verifyNotRecordingForTrackChange(app)) { + return; + } + if (!validateSettings()) { return; } 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 1a2b4fd3f..783c162a0 100644 --- a/web/app/assets/javascripts/configureTrack.js +++ b/web/app/assets/javascripts/configureTrack.js @@ -571,6 +571,10 @@ } function saveSettings() { + if (!context.JK.verifyNotRecordingForTrackChange(app)) { + return; + } + if (!validateAudioSettings(false)) { return; } @@ -797,6 +801,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) { diff --git a/web/app/assets/javascripts/fakeJamClient.js b/web/app/assets/javascripts/fakeJamClient.js index 7c73e4f72..2a12e2362 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 RecordingRegisterCallbacks(startRecordingCallbackName, stopRecordingCallbackName, startedRecordingCallbackName, stoppedRecordingCallbackName, requestStopCallbackName) { + fakeJamClientRecordings.RecordingRegisterCallbacks(startRecordingCallbackName, stopRecordingCallbackName, startedRecordingCallbackName,stoppedRecordingCallbackName, requestStopCallbackName); + } + 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.RecordingRegisterCallbacks = RecordingRegisterCallbacks; 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..b64c474ed --- /dev/null +++ b/web/app/assets/javascripts/fakeJamClientMessages.js @@ -0,0 +1,76 @@ +(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, errorReason, errorDetail) { + var msg = {}; + msg.type = self.Types.STOP_RECORDING; + msg.msgId = context.JK.generateUUID(); + msg.recordingId = recordingId; + msg.errorReason = errorReason; + msg.errorDetail = errorDetail; + 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, errorReason, errorDetail) { + var msg = {}; + msg.type = self.Types.ABORT_RECORDING; + msg.msgId = context.JK.generateUUID(); + msg.recordingId = recordingId; + msg.errorReason = errorReason; + msg.errorDetail = errorDetail; + 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..0255734c2 --- /dev/null +++ b/web/app/assets/javascripts/fakeJamClientRecordings.js @@ -0,0 +1,232 @@ +// 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 requestStopCallbackName = null; + + var startingSessionState = null; + var stoppingSessionState = null; + + var currentRecordingId = null; + var currentRecordingCreatorClientId = null; + + function timeoutStartRecordingTimer() { + eval(startRecordingResultCallbackName).call(this, startingSessionState.recordingId, false, 'client-no-response', startingSessionState.groupedClientTracks); + startingSessionState = null; + } + + function timeoutStopRecordingTimer() { + eval(stopRecordingResultCallbackName).call(this, stoppingSessionState.recordingId, false, 'client-no-response', stoppingSessionState.groupedClientTracks); + } + + function StartRecording(recordingId, groupedClientTracks) { + 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 = copyTracks(groupedClientTracks, app.clientId); // we will manipulate this new one + + // store the current recording's data + currentRecordingId = recordingId; + currentRecordingCreatorClientId = app.clientId; + + if(context.JK.dlen(startingSessionState.groupedClientTracks) == 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 clientId in startingSessionState.groupedClientTracks) { + context.JK.JamServer.sendP2PMessage(clientId, JSON.stringify(p2pMessageFactory.startRecording(recordingId))); + } + } + } + + function StopRecording(recordingId, groupedClientTracks, errorReason, errorDetail) { + + if(startingSessionState) { + // we are currently starting a session. + // TODO + } + + 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 = copyTracks(groupedClientTracks, app.clientId); + + if(context.JK.dlen(stoppingSessionState.groupedClientTracks) == 0) { + finishSuccessfulStop(recordingId); + } + else { + // signal all other connected clients that the recording has started + for(var clientId in stoppingSessionState.groupedClientTracks) { + context.JK.JamServer.sendP2PMessage(clientId, JSON.stringify(p2pMessageFactory.stopRecording(recordingId, errorReason, errorDetail))); + } + } + + //eval(stopRecordingResultCallbackName).call(this, recordingId, true, null, null); + } + + 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, from, payload.recordingId); + } + } + + 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) { + delete startingSessionState.groupedClientTracks[from]; + + if(context.JK.dlen(startingSessionState.groupedClientTracks) == 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, !payload.errorReason, payload.errorReason, payload.errorDetail); + } + + 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) { + delete stoppingSessionState.groupedClientTracks[from]; + + if(context.JK.dlen(stoppingSessionState.groupedClientTracks) == 0) { + finishSuccessfulStop(payload.recordingId); + } + } + else { + // TOOD: a client responded with error; what now? + } + } + 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 + eval(requestStopCallbackName).call(this, payload.errorReason, payload.errorDetail); + } + else { + logger.warn("only the creator currently deals with the abort request. abort request sent from:" + from + " with a reason of: " + payload.errorReason); + } + } + + function RecordingRegisterCallbacks(startRecordingCallbackName, + stopRecordingCallbackName, + startedRecordingCallbackName, + stoppedRecordingCallbackName, + _requestStopCallbackName) { + startRecordingResultCallbackName = startRecordingCallbackName; + stopRecordingResultCallbackName = stopRecordingCallbackName; + startedRecordingResultCallbackName = startedRecordingCallbackName; + stoppedRecordingEventCallbackName = stoppedRecordingCallbackName; + requestStopCallbackName = _requestStopCallbackName; + } + + // copies all tracks, but removes current client ID because we don't want to message that user + function copyTracks(tracks, myClientId) { + var newTracks = {}; + for(var clientId in tracks) { + if(clientId != myClientId) { + newTracks[clientId] = tracks[clientId]; + } + } + return newTracks; + } + + function finishSuccessfulStart(recordingId) { + // all clients have responded. + clearTimeout(startingSessionState.aggegratingStartResultsTimer); + startingSessionState = null; + eval(startRecordingResultCallbackName).call(this, recordingId, true); + } + + function finishSuccessfulStop(recordingId, errorReason) { + // all clients have responded. + clearTimeout(stoppingSessionState.aggegratingStopResultsTimer); + stoppingSessionState = null; + eval(stopRecordingResultCallbackName).call(this, recordingId, true, errorReason); + } + + + // 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.RecordingRegisterCallbacks = RecordingRegisterCallbacks; + } + + })(window, jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/globals.js b/web/app/assets/javascripts/globals.js index 117e99960..8f757a911 100644 --- a/web/app/assets/javascripts/globals.js +++ b/web/app/assets/javascripts/globals.js @@ -72,4 +72,8 @@ 240: { "server_id": "mandolin" }, 250: { "server_id": "other" } }; + + 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 ea66bcbec..4a58c5f2d 100644 --- a/web/app/assets/javascripts/jam_rest.js +++ b/web/app/assets/javascripts/jam_rest.js @@ -308,6 +308,39 @@ }); } + 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 initialize() { return self; } @@ -338,6 +371,9 @@ this.createJoinRequest = createJoinRequest; this.updateJoinRequest = updateJoinRequest; this.updateUser = updateUser; + this.startRecording = startRecording; + this.stopRecording = stopRecording; + this.getRecording = getRecording; return this; }; diff --git a/web/app/assets/javascripts/jamkazam.js b/web/app/assets/javascripts/jamkazam.js index 5870b3564..99d642b9c 100644 --- a/web/app/assets/javascripts/jamkazam.js +++ b/web/app/assets/javascripts/jamkazam.js @@ -176,7 +176,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 = "