module JamRuby class ActiveMusicSession < ActiveRecord::Base @@log = Logging.logger[ActiveMusicSession] self.primary_key = 'id' self.table_name = 'active_music_sessions' attr_accessor :legal_terms, :max_score, :opening_jam_track, :opening_recording, :opening_backing_track, :opening_metronome, :jam_track_id belongs_to :claimed_recording, :class_name => "JamRuby::ClaimedRecording", :foreign_key => "claimed_recording_id", :inverse_of => :playing_sessions belongs_to :claimed_recording_initiator, :class_name => "JamRuby::User", :inverse_of => :playing_claimed_recordings, :foreign_key => "claimed_recording_initiator_id" belongs_to :jam_track, :class_name => "JamRuby::JamTrack", :foreign_key => "jam_track_id", :inverse_of => :playing_sessions belongs_to :jam_track_initiator, :class_name => "JamRuby::User", :inverse_of => :playing_jam_tracks, :foreign_key => "jam_track_initiator_id" belongs_to :backing_track_initiator, :class_name => "JamRuby::User", :inverse_of => :playing_jam_tracks, :foreign_key => "backing_track_initiator_id" belongs_to :metronome_initiator, :class_name => "JamRuby::User", :inverse_of => :playing_jam_tracks, :foreign_key => "metronome_initiator_id" has_one :music_session, :class_name => "JamRuby::MusicSession", :foreign_key => 'music_session_id' has_one :mount, :class_name => "JamRuby::IcecastMount", :inverse_of => :music_session, :foreign_key => 'music_session_id' belongs_to :creator, :class_name => 'JamRuby::User', :foreign_key => :user_id has_many :connections, :class_name => "JamRuby::Connection", foreign_key: :music_session_id has_many :users, :through => :connections, :class_name => "JamRuby::User" has_many :recordings, :class_name => "JamRuby::Recording", :inverse_of => :music_session, foreign_key: :music_session_id has_many :chats, :class_name => "JamRuby::ChatMessages", :foreign_key => "session_id" validates :creator, :presence => true validate :creator_is_musician validate :validate_opening_recording, :if => :opening_recording validate :validate_opening_jam_track, :if => :opening_jam_track validate :validate_opening_backing_track, :if => :opening_backing_track # not sure if this is helpful since if one opens, it always stays open validate :validate_opening_metronome, :if => :opening_metronome after_create :started_session after_destroy do |obj| JamRuby::MusicSession.removed_music_session(obj.id) end #default_scope :select => "*, 0 as score" def attributes super.merge('max_score' => self.max_score) end def max_score nil unless has_attribute?(:max_score) read_attribute(:max_score).to_i end before_create :create_uuid def create_uuid #self.id = SecureRandom.uuid end def before_destroy feed = Feed.find_by_music_session_id(self.id) unless feed.nil? feed.active = false feed.save end self.mount.destroy if self.mount end def creator_is_musician unless creator.musician? errors.add(:creator, ValidationMessages::MUST_BE_A_MUSICIAN) end end def validate_opening_recording unless claimed_recording_id_was.nil? errors.add(:claimed_recording, ValidationMessages::CLAIMED_RECORDING_ALREADY_IN_PROGRESS) end if is_jam_track_open? errors.add(:claimed_recording, ValidationMessages::JAM_TRACK_ALREADY_OPEN) end if is_backing_track_open? errors.add(:claimed_recording, ValidationMessages::BACKING_TRACK_ALREADY_OPEN) end if is_metronome_open? errors.add(:claimed_recording, ValidationMessages::METRONOME_ALREADY_OPEN) end end def validate_opening_jam_track validate_other_audio(:jam_track) end def validate_opening_backing_track validate_other_audio(:backing_track) end def validate_opening_metronome validate_other_audio(:metronome) end def validate_other_audio(error_key) # validate that there is no backing track already open in this session if backing_track_path_was.present? errors.add(error_key, ValidationMessages::BACKING_TRACK_ALREADY_OPEN) end # validate that there is no jam track already open in this session if jam_track_id_was.present? errors.add(error_key, ValidationMessages::JAM_TRACK_ALREADY_OPEN) end # validate that there is no recording being made if is_recording? errors.add(error_key, ValidationMessages::RECORDING_ALREADY_IN_PROGRESS) end # validate that there is no recording being played back to the session if is_playing_recording? errors.add(error_key, ValidationMessages::CLAIMED_RECORDING_ALREADY_IN_PROGRESS) end end # returns an array of client_id's that are in this session # if as_musician is nil, all connections in the session ,regardless if it's a musician or not or not # you can also exclude a client_id from the returned set by setting exclude_client_id def get_connection_ids(options = {}) as_musician = options[:as_musician] exclude_client_id = options[:exclude_client_id] where = { :music_session_id => self.id } where[:as_musician] = as_musician unless as_musician.nil? exclude = "client_id != '#{exclude_client_id}'"unless exclude_client_id.nil? Connection.select(:client_id).where(where).where(exclude).map(&:client_id) end # This is a little confusing. You can specify *BOTH* friends_only and my_bands_only to be true # If so, then it's an OR condition. If both are false, you can get sessions with anyone. def self.index(current_user, options = {}) participants = options[:participants] genres = options[:genres] keyword = options[:keyword] friends_only = options[:friends_only].nil? ? false : options[:friends_only] my_bands_only = options[:my_bands_only].nil? ? false : options[:my_bands_only] as_musician = options[:as_musician].nil? ? true : options[:as_musician] query = ActiveMusicSession .joins( %Q{ INNER JOIN music_sessions ON active_music_sessions.id = music_sessions.id } ) .joins( %Q{ INNER JOIN connections ON active_music_sessions.id = connections.music_session_id } ) .joins( %Q{ LEFT OUTER JOIN friendships ON connections.user_id = friendships.user_id AND friendships.friend_id = '#{current_user.id}' } ) .joins( %Q{ LEFT OUTER JOIN invitations ON invitations.music_session_id = active_music_sessions.id AND invitations.receiver_id = '#{current_user.id}' } ) .group( %Q{ active_music_sessions.id } ) .order( %Q{ SUM(CASE WHEN invitations.id IS NULL THEN 0 ELSE 1 END) DESC, SUM(CASE WHEN friendships.user_id IS NULL THEN 0 ELSE 1 END) DESC, active_music_sessions.created_at DESC } ) if as_musician query = query.where( %Q{ musician_access = true OR invitations.id IS NOT NULL } ) else # if you are trying to join the session as a fan/listener, # we have to have a mount, fan_access has to be true, and we have to allow for the reload of icecast to have taken effect query = query.joins('INNER JOIN icecast_mounts ON icecast_mounts.music_session_id = active_music_sessions.id INNER JOIN icecast_servers ON icecast_mounts.icecast_server_id = icecast_servers.id') query = query.where('music_sessions.fan_access = true') query = query.where("(active_music_sessions.created_at < icecast_servers.config_updated_at)") end query = query.where("music_sessions.description like '%#{keyword}%'") unless keyword.nil? query = query.where("connections.user_id" => participants.split(',')) unless participants.nil? query = query.where("music_sessions.genre_id in (?)", genres) unless genres.nil? if my_bands_only query = query.joins( %Q{ LEFT OUTER JOIN bands_musicians ON bands_musicians.user_id = '#{current_user.id}' } ) end if my_bands_only || friends_only query = query.where( %Q{ #{friends_only ? "friendships.user_id IS NOT NULL" : "false"} OR #{my_bands_only ? "bands_musicians.band_id = music_sessions.band_id" : "false"} } ) end return query end # This is a little confusing. You can specify *BOTH* friends_only and my_bands_only to be true # If so, then it's an OR condition. If both are false, you can get sessions with anyone. # note, this is mostly the same as above but includes paging through the result and and scores. # thus it needs the client_id... def self.nindex(current_user, options = {}) client_id = options[:client_id] participants = options[:participants] genres = options[:genres] keyword = options[:keyword] friends_only = options[:friends_only].nil? ? false : options[:friends_only] my_bands_only = options[:my_bands_only].nil? ? false : options[:my_bands_only] as_musician = options[:as_musician].nil? ? true : options[:as_musician] offset = options[:offset] limit = options[:limit] connection = Connection.where(client_id: client_id).first! locidispid = connection.locidispid query = ActiveMusicSession .select("active_music_sessions.*, max(coalesce(current_scores.score, 1000)) as max_score") # 1000 is higher than the allowed max of 999 .joins( %Q{ INNER JOIN music_sessions ON active_music_sessions.id = music_sessions.id } ) .joins( %Q{ INNER JOIN connections ON active_music_sessions.id = connections.music_session_id } ) .joins( %Q{ LEFT OUTER JOIN current_scores ON current_scores.alocidispid = connections.locidispid AND current_scores.blocidispid = #{locidispid} } ) .joins( %Q{ LEFT OUTER JOIN friendships ON connections.user_id = friendships.user_id AND friendships.friend_id = '#{current_user.id}' } ) .joins( %Q{ LEFT OUTER JOIN invitations ON invitations.music_session_id = active_music_sessions.id AND invitations.receiver_id = '#{current_user.id}' } ) .group( %Q{ active_music_sessions.id } ) .order( %Q{ SUM(CASE WHEN invitations.id IS NULL THEN 0 ELSE 1 END) DESC, SUM(CASE WHEN friendships.user_id IS NULL THEN 0 ELSE 1 END) DESC, active_music_sessions.created_at DESC } ) query = query.offset(offset) if offset query = query.limit(limit) if limit if as_musician query = query.where( %Q{ musician_access = true OR music_sessions.user_id = '#{current_user.id}' OR invitations.id IS NOT NULL } ) else # if you are trying to join the session as a fan/listener, # we have to have a mount, fan_access has to be true, and we have to allow for the reload of icecast to have taken effect query = query.joins('INNER JOIN icecast_mounts ON icecast_mounts.music_session_id = active_music_sessions.id INNER JOIN icecast_servers ON icecast_mounts.icecast_server_id = icecast_servers.id') query = query.where('music_sessions.fan_access = true') query = query.where("(active_music_sessions.created_at < icecast_servers.config_updated_at)") end query = query.where("music_sessions.description like '%#{keyword}%'") unless keyword.nil? query = query.where("connections.user_id" => participants.split(',')) unless participants.nil? query = query.where("music_sessions.genre_id in (?)", genres) unless genres.nil? if my_bands_only query = query.joins( %Q{ LEFT OUTER JOIN bands_musicians ON bands_musicians.user_id = '#{current_user.id}' } ) end if my_bands_only || friends_only query = query.where( %Q{ #{friends_only ? "friendships.user_id IS NOT NULL" : "false"} OR #{my_bands_only ? "bands_musicians.band_id = music_sessions.band_id" : "false"} } ) end return query end # initialize the two temporary tables we use to drive ams_index and ams_users def self.ams_init(current_user, options = {}) my_locidispid = current_user.last_jam_locidispid # 13 is an average audio gear value we use if they have not qualified any gear my_audio_latency = current_user.last_jam_audio_latency || 13 locidispid_expr = my_locidispid ? "#{my_locidispid}::bigint" : '0::bigint' self.connection.execute("select ams_index('#{current_user.id}'::varchar, #{locidispid_expr}, #{my_audio_latency}::integer)").check end # Generate a list of music sessions (that are active) filtered by genre, language, keyword, and sorted # (and tagged) by rsvp'd (1st), invited (2nd), and musician can join (3rd). within a group tagged the # same, sorted by score. date seems irrelevant as these are active sessions. ams_init must be called # first. def self.ams_query(current_user, options = {}) session_id = options[:session_id] client_id = options[:client_id] genre = options[:genre] lang = options[:lang] keyword = options[:keyword] offset = options[:offset] limit = options[:limit] day = options[:day] timezone_offset = options[:timezone_offset] query = MusicSession .select('music_sessions.*') # this is not really needed when ams_music_session_tmp is joined # unless there is something specific we need out of active_music_sessions # query = query.joins( # %Q{ # INNER JOIN # active_music_sessions # ON # active_music_sessions.id = music_sessions.id # } # ) # .select('1::integer as tag, 15::integer as latency') # integrate ams_music_session_tmp into the processing # then we can join ams_music_session_tmp and not join active_music_sessions query = query.joins( %Q{ INNER JOIN ams_music_session_tmp ON ams_music_session_tmp.music_session_id = music_sessions.id } ) .select('ams_music_session_tmp.tag, ams_music_session_tmp.latency') query = query.order( %Q{ tag, latency, music_sessions.id } ) .group( %Q{ tag, latency, music_sessions.id } ) # if not specified, default offset to 0 offset ||= 0 offset = offset.to_i # if not specified, default limit to 20 limit ||= 20 limit = limit.to_i query = query.offset(offset) query = query.limit(limit) query = query.where("music_sessions.genre_id = ?", genre) unless genre.blank? query = query.where('music_sessions.language = ?', lang) unless lang.blank? query = query.where('music_sessions.id = ?', session_id) unless session_id.blank? query = query.where("(description_tsv @@ to_tsquery('jamenglish', ?))", ActiveRecord::Base.connection.quote(keyword) + ':*') unless keyword.blank? if !day.blank? && !timezone_offset.blank? begin day = Date.parse(day) next_day = day + 1 timezone_offset = timezone_offset.to_i if timezone_offset > 0 timezone_offset = "+#{timezone_offset}" end query = query.where("scheduled_start BETWEEN TIMESTAMP WITH TIME ZONE '#{day} 00:00:00#{timezone_offset}' AND TIMESTAMP WITH TIME ZONE '#{next_day} 00:00:00#{timezone_offset}'") rescue Exception => e # do nothing. bad date probably @@log.warn("unable to parse day=#{day}, timezone_offset=#{timezone_offset}, e=#{e}") end end return query end # returns the set of users in a music_sessions and the music_session they are in and their latency. # ams_init must be called first. # user.audio_latency / 2 , + other_user.audio_latency of them / 2, + network latency /2 def self.ams_users return User.select('users.*, ams_users_tmp.music_session_id, ams_users_tmp.full_score, ams_users_tmp.audio_latency, ams_users_tmp.internet_score') .joins( %Q{ INNER JOIN ams_users_tmp ON ams_users_tmp.user_id = users.id } ) .order('ams_users_tmp.music_session_id, ams_users_tmp.user_id') end # wrap me in a transaction! # note that these queries must be actualized before the end of the transaction # else the temporary tables created by sms_init will be gone. def self.ams_index(current_user, params) ActiveMusicSession.ams_init(current_user, params) music_sessions = ActiveMusicSession.ams_query(current_user, params).all music_session_users = ActiveMusicSession.ams_users.all user_scores = {} music_session_users.each do |user| user_scores[user.id] = {full_score: user.full_score, audio_latency: user.audio_latency, internet_score: user.internet_score} end [music_sessions, user_scores] end def self.participant_create(user, music_session_id, client_id, as_musician, tracks, audio_latency, video_sources=nil) music_session = MusicSession.find(music_session_id) # USERS ARE ALREADY IN SESSION if music_session.active_music_session connection = nil active_music_session = music_session.active_music_session ActiveRecord::Base.transaction do active_music_session.with_lock do # VRFS-1297 active_music_session.tick_track_changes connection = ConnectionManager.new.join_music_session(user, client_id, active_music_session, as_musician, tracks, audio_latency, video_sources) if connection.errors.any? # rollback the transaction to make sure nothing is disturbed in the database raise ActiveRecord::Rollback end end end unless connection.errors.any? user.update_progression_field(:first_music_session_at) MusicSessionUserHistory.save(music_session_id, user.id, client_id, tracks) if as_musician # send to session participants Notification.send_session_join(active_music_session, connection, user) # send "musician joined session" notification only if it's not a band session since there will be a "band joined session" notification if music_session.band.nil? Notification.send_musician_session_join(music_session, user) end end end connection # FIRST USER TO JOIN SESSION else return_value = nil time = Benchmark.realtime do ActiveRecord::Base.transaction do # we need to lock the icecast server in this transaction for writing, to make sure thath IcecastConfigWriter # doesn't dumpXML as we are changing the server's configuraion icecast_server = IcecastServer.find_best_server_for_user(user) if music_session.fan_access icecast_server.lock! if icecast_server # check if we are connected to rabbitmq active_music_session = ActiveMusicSession.new active_music_session.id = music_session.id # copy the .id from music_session to active_music_session active_music_session.creator = user if music_session.fan_access # create an icecast mount since regular users can listen in to the broadcast active_music_session.mount = IcecastMount.build_session_mount(music_session, active_music_session, icecast_server) end active_music_session.save unless active_music_session.errors.any? music_session.started_at = active_music_session.created_at music_session.save(:validate => false) # save session parameters for next session User.save_session_settings(user, music_session) # auto-join this user into the newly created session as_musician = true connection = ConnectionManager.new.join_music_session(user, client_id, active_music_session, as_musician, tracks, audio_latency, video_sources) unless connection.errors.any? user.update_progression_field(:first_music_session_at) MusicSessionUserHistory.save(active_music_session.id, user.id, client_id, tracks) # only send this notification if it's a band session unless music_session.band.nil? Notification.send_band_session_join(music_session, music_session.band) else Notification.send_musician_session_join(music_session, user) end return_value = connection else return_value = connection # rollback the transaction to make sure nothing is disturbed in the database raise ActiveRecord::Rollback end else return_value = active_music_session # rollback the transaction to make sure nothing is disturbed in the database raise ActiveRecord::Rollback end end end if time > 2 Logging.logger[self].warn "creating a music session took #{time*1000} milliseconds" end return_value end end # Verifies that the specified user can delete this music session def can_delete? user # the creator can delete self.creator == user end def access? user music_session.part_of_session? user end def most_recent_recording recordings.where(:music_session_id => self.id).order('created_at desc').limit(1).first end def is_jam_track_open? !self.jam_track.nil? end def is_backing_track_open? self.backing_track_path.present? end def is_metronome_open? self.metronome_active.present? end # is this music session currently recording? def is_recording? recordings.where(:duration => nil).count > 0 end def is_playing_recording? !self.claimed_recording.nil? 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 claimed_recording_start(owner, claimed_recording) self.claimed_recording = claimed_recording self.claimed_recording_initiator = owner self.opening_recording = true self.save self.opening_recording = false end def claimed_recording_stop self.claimed_recording = nil self.claimed_recording_initiator = nil self.save end def invitations music_session.invitations end def invited_musicians music_session.invited_musicians end def join_requests music_session.join_requests end def fan_invitations music_session.fan_invitations end def to_s description end def musician_access music_session.musician_access end def fan_access music_session.fan_access end def description music_session.description end def name music_session.name end def genre music_session.genre end def fan_chat music_session.fan_chat end def band music_session.band end def approval_required music_session.approval_required end def music_notations music_session.music_notations end # Verifies that the specified user can join this music session def can_join? user, as_musician music_session.can_join? user, as_musician end # Verifies that the specified user can see this music session def can_see? user music_session.can_see? user end def tick_track_changes self.track_changes_counter += 1 self.save!(:validate => false) end def connected_participant_count Connection.where(:music_session_id => self.id, :aasm_state => Connection::CONNECT_STATE.to_s, :as_musician => true) .count end def started_session raise "active_music_sessions.id must be set by caller" unless self.id # associate this active_music_session with the music_session formally session = MusicSession.find(self.id) session.active_music_session = self self.music_session = session # needed because of the Google Analytics below in some test cases session.scheduled_start = self.created_at unless session.scheduled_start session.save! feed = Feed.find_by_music_session_id(self.id) # this should never be hit since the feed entry is created when the music_session record is created if feed.nil? feed = Feed.new feed.music_session_id = self.id end feed.active = true feed.save GoogleAnalyticsEvent.track_session_duration(self) GoogleAnalyticsEvent.track_band_real_session(self) end def open_jam_track(user, jam_track) self.jam_track = jam_track self.jam_track_initiator = user self.opening_jam_track = true self.save self.opening_jam_track = false #self.tick_track_changes end def close_jam_track self.jam_track = nil self.jam_track_initiator = nil self.save end # @param backing_track_path is a relative path: def open_backing_track(user, backing_track_path) self.backing_track_path = backing_track_path self.backing_track_initiator = user self.opening_backing_track = true self.save self.opening_backing_track = false end def close_backing_track self.backing_track_path = nil self.backing_track_initiator = nil self.save end def open_metronome(user) self.metronome_active = true self.metronome_initiator = user self.opening_metronome = true self.save self.opening_metronome = false end def close_metronome self.metronome_active = false self.metronome_initiator = nil self.save end def self.sync(session_history) music_session = MusicSession.find_by_id(session_history.id) if music_session.nil? music_session = MusicSession.new music_session.id = session_history.id end music_session.user_id = session_history.creator.id music_session.band_id = session_history.band.id unless session_history.band.nil? session_history.save! end def self.stats stats = {} result = ActiveMusicSession.select('count(distinct(id)) AS total, count(distinct(jam_track_initiator_id)) as jam_track_count, count(distinct(backing_track_initiator_id)) as backing_track_count, count(distinct(metronome_initiator_id)) as metronome_count, count(distinct(claimed_recording_initiator_id)) as recording_count').first stats['count'] = result['total'].to_i stats['jam_track_count'] = result['jam_track_count'].to_i stats['backing_track_count'] = result['backing_track_count'].to_i stats['metronome_count'] = result['metronome_count'].to_i stats['recording_count'] = result['recording_count'].to_i stats end end end