1446 lines
55 KiB
Ruby
1446 lines
55 KiB
Ruby
require 'iso-639'
|
|
|
|
module JamRuby
|
|
class MusicSession < ActiveRecord::Base
|
|
include HtmlSanitize
|
|
html_sanitize strict: [:name, :description]
|
|
|
|
@@log = Logging.logger[MusicSession]
|
|
|
|
NO_RECURRING = 'once'
|
|
RECURRING_WEEKLY = 'weekly'
|
|
|
|
RECURRING_MODES = [NO_RECURRING, RECURRING_WEEKLY]
|
|
|
|
UNSTARTED_INTERVAL_DAYS_SKIP = '14' # days past scheduled start to skip in query
|
|
UNSTARTED_INTERVAL_DAYS_PURGE = '28' # days past scheduled start to purge session
|
|
UNSTARTED_INTERVAL_DAYS_PURGE_RECUR = '28' # days past scheduled start to purge recurddingsession
|
|
|
|
CREATE_TYPE_START_SCHEDULED = 'start-scheduled'
|
|
CREATE_TYPE_SCHEDULE_FUTURE = 'schedule-future'
|
|
CREATE_TYPE_RSVP = 'rsvp'
|
|
CREATE_TYPE_IMMEDIATE = 'immediately'
|
|
CREATE_TYPE_QUICK_START = 'quick-start'
|
|
CREATE_TYPE_LESSON = 'lesson'
|
|
CREATE_TYPE_QUICK_PUBLIC = 'quick-public'
|
|
|
|
attr_accessor :legal_terms, :language_description, :access_description, :scheduling_info_changed
|
|
|
|
attr_accessor :approved_rsvps, :open_slots, :pending_invitations
|
|
|
|
self.table_name = "music_sessions"
|
|
|
|
self.primary_key = 'id'
|
|
|
|
belongs_to :creator,:class_name => 'JamRuby::User', :foreign_key => :user_id, :inverse_of => :music_session_histories
|
|
belongs_to :band, :class_name => 'JamRuby::Band', :foreign_key => :band_id, :inverse_of => :music_sessions
|
|
belongs_to :active_music_session, :class_name => 'JamRuby::ActiveMusicSession', foreign_key: :music_session_id
|
|
belongs_to :session_controller, :class_name => 'JamRuby::User', :foreign_key => :session_controller_id, :inverse_of => :controlled_sessions
|
|
belongs_to :lesson_session, :class_name => "JamRuby::LessonSession"
|
|
|
|
has_many :music_session_user_histories, :class_name => "JamRuby::MusicSessionUserHistory", :foreign_key => "music_session_id", :dependent => :delete_all
|
|
has_many :comments, :class_name => "JamRuby::MusicSessionComment", :foreign_key => "music_session_id"
|
|
has_many :session_info_comments, :class_name => "JamRuby::SessionInfoComment", :foreign_key => "music_session_id"
|
|
has_many :likes, :class_name => "JamRuby::MusicSessionLiker", :foreign_key => "session_id"
|
|
has_many :plays, :class_name => "JamRuby::PlayablePlay", :as => :playable, :dependent => :destroy
|
|
has_one :share_token, :class_name => "JamRuby::ShareToken", :inverse_of => :shareable, :foreign_key => 'shareable_id'
|
|
has_one :feed, :class_name => "JamRuby::Feed", :inverse_of => :music_session, :foreign_key => 'music_session_id', :dependent => :destroy
|
|
belongs_to :genre, :class_name => "JamRuby::Genre", :inverse_of => :music_sessions, :foreign_key => 'genre_id'
|
|
has_many :join_requests, :foreign_key => "music_session_id", :inverse_of => :music_session, :class_name => "JamRuby::JoinRequest"
|
|
has_many :invitations, :foreign_key => "music_session_id", :inverse_of => :music_session, :class_name => "JamRuby::Invitation"
|
|
has_many :invited_musicians, :through => :invitations, :class_name => "JamRuby::User", :foreign_key => "receiver_id", :source => :receiver
|
|
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_many :rsvp_slots, :class_name => "JamRuby::RsvpSlot", :foreign_key => "music_session_id", :dependent => :destroy
|
|
has_many :rsvp_requests, :class_name => "JamRuby::RsvpRequest", :foreign_key => "music_session_id", :dependent => :destroy
|
|
has_many :music_notations, :class_name => "JamRuby::MusicNotation", :foreign_key => "music_session_id"
|
|
has_many :jam_track_session, :class_name => "JamRuby::JamTrackSession"
|
|
has_many :broadcasts, :class_name => "JamRuby::Broadcast"
|
|
|
|
validates :genre, :presence => true
|
|
validates :description, :presence => true, :no_profanity => true
|
|
validates :fan_chat, :inclusion => {:in => [true, false]}
|
|
validates :fan_access, :inclusion => {:in => [true, false]}
|
|
validates :approval_required, :inclusion => {:in => [true, false]}
|
|
validates :musician_access, :inclusion => {:in => [true, false]}
|
|
validates :friends_can_join, :inclusion => {:in => [true, false]}
|
|
validates :is_unstructured_rsvp, :inclusion => {:in => [true, false]}
|
|
validates :legal_terms, :inclusion => {:in => [true]}, :on => :create
|
|
validates :creator, :presence => true
|
|
validates :timezone, presence: true, if: Proc.new { |session| session.scheduled_start }
|
|
validates :scheduled_duration, presence: true, if: Proc.new { |session| session.scheduled_start }
|
|
|
|
validate :creator_is_musician
|
|
validate :validate_timezone
|
|
|
|
before_create :generate_share_token
|
|
#before_save :update_scheduled_start
|
|
before_save :check_scheduling_info_changed
|
|
|
|
SHARE_TOKEN_LENGTH = 8
|
|
|
|
SEPARATOR = '|'
|
|
|
|
def current_broadcast
|
|
Broadcast.current_broadcast(self)
|
|
end
|
|
|
|
def unlink_broadcast
|
|
Broadcast.unlink_broadcast(self)
|
|
end
|
|
|
|
def create_broadcast(user, broadcast_options, google_client = GoogleClient.new)
|
|
|
|
broadcast = current_broadcast
|
|
|
|
if broadcast.nil?
|
|
broadcast = create_youtube_broadcast(user, broadcast_options, google_client)
|
|
else
|
|
result = refresh_youtube_broadcast(user, broadcast, broadcast_options, google_client)
|
|
# check against Youtube the real state of broadcast, to see if we need a new one?
|
|
|
|
if result.nil?
|
|
unlink_broadcast # user probably deleted it, or marked it complete.
|
|
broadcast = create_youtube_broadcast(user, broadcast_options, google_client)
|
|
end
|
|
end
|
|
|
|
broadcast
|
|
end
|
|
|
|
def create_stream(user, broadcast_options, google_client = JamRuby::GoogleClient.new)
|
|
|
|
broadcast = create_broadcast(user, broadcast_options, google_client)
|
|
|
|
stream = current_stream(broadcast)
|
|
|
|
if stream.nil?
|
|
create_youtube_stream(user, broadcast, broadcast_options, google_client)
|
|
bind_broadcast(user, broadcast, google_client)
|
|
else
|
|
bind_broadcast(user, broadcast, google_client)
|
|
end
|
|
end
|
|
|
|
def current_stream(broadcast)
|
|
broadcast.stream_id
|
|
end
|
|
|
|
def refresh_stream(user, broadcast)
|
|
stream_data = get_livestream(user)
|
|
broadcast.stream_id = stream_data["id"]
|
|
broadcast.stream_status = stream_data["status"]["streamStatus"]
|
|
broadcast.stream_name = stream_data["cdn"]["ingestionInfo"]["streamName"]
|
|
broadcast.stream_address = stream_data["cdn"]["ingestionInfo"]["ingestionAddress"]
|
|
broadcast.stream_data = stream_data.to_json
|
|
broadcast.save
|
|
end
|
|
|
|
def refresh_youtube_broadcast(user, broadcast, broadcast_data = nil, google_client = GoogleClient.new)
|
|
if broadcast_data.nil?
|
|
broadcast_data = google_client.get_broadcast(user, broadcast.broadcast_id)
|
|
end
|
|
|
|
if broadcast_data
|
|
broadcast.update_broadcast_data(broadcast_data)
|
|
broadcast.save
|
|
true
|
|
else
|
|
# this path makes sense if the user deleted the video on the server, but we do not yet know it
|
|
nil
|
|
end
|
|
|
|
end
|
|
|
|
def get_livestream(user, google_client = GoogleClient.new)
|
|
broadcast = current_broadcast
|
|
|
|
if broadcast.nil?
|
|
nil
|
|
else
|
|
stream_id = current_stream(broadcast)
|
|
|
|
if stream_id.nil?
|
|
nil
|
|
else
|
|
return google_client.get_livestream(user, stream_id)
|
|
end
|
|
end
|
|
end
|
|
|
|
def get_broadcast(user, google_client = UserManager.new.get_google_client)
|
|
broadcast = current_broadcast
|
|
|
|
if broadcast.nil?
|
|
nil
|
|
else
|
|
broadcast_data = google_client.get_broadcast(user, broadcast.broadcast_id)
|
|
broadcast.update_broadcast_data(broadcast_data)
|
|
broadcast.save
|
|
broadcast
|
|
end
|
|
end
|
|
|
|
def set_livestream_live(user, google_client = GoogleClient.new)
|
|
livestream = get_livestream(user, google_client)
|
|
|
|
if livestream
|
|
# if livestream["status"]["streamStatus"] == "active"
|
|
transition_broadcast(user, broadcast, 'live', google_client)
|
|
# end
|
|
else
|
|
end
|
|
|
|
end
|
|
|
|
|
|
# https://developers.google.com/youtube/v3/live/docs/liveStreams#resource
|
|
def create_youtube_stream(user, broadcast, broadcast_options, google_client = GoogleClient.new)
|
|
|
|
# https://developers.google.com/youtube/v3/live/docs/liveStreams/insert
|
|
# required
|
|
# snippet.title
|
|
# cdn.format
|
|
# cdn.ingestionType (deprecated - use resolution/framerate)
|
|
|
|
stream_options = {}
|
|
stream_options[:snippet] ||= {}
|
|
stream_options[:snippet][:title] ||= name
|
|
stream_options[:snippet][:isDefaultStream] ||= false
|
|
#broadcast_options[:snippet][:scheduledEndTime] = end_time.utc.iso8601
|
|
|
|
stream_options[:cdn] ||= {}
|
|
stream_options[:cdn][:frameRate] ||= '30fps'
|
|
stream_options[:cdn][:resolution] ||= '360p'
|
|
stream_options[:cdn][:ingestionType] ||= 'rtmp'
|
|
|
|
stream_options[:contentDetails] ||= {}
|
|
stream_options[:contentDetails][:isReusable] ||= false
|
|
stream_options[:contentDetails][:monitorStream] ||= {}
|
|
stream_options[:contentDetails][:monitorStream][:enableMonitorStream] ||= false
|
|
|
|
stream_options = google_client.create_stream(user, stream_options)
|
|
|
|
broadcast.stream_id = stream_options["id"]
|
|
broadcast.stream_status = stream_options["status"]["streamStatus"]
|
|
broadcast.stream_name = stream_options["cdn"]["ingestionInfo"]["streamName"]
|
|
broadcast.stream_address = stream_options["cdn"]["ingestionInfo"]["ingestionAddress"]
|
|
broadcast.stream_data = stream_options.to_json
|
|
broadcast.save!
|
|
broadcast
|
|
end
|
|
|
|
def create_youtube_broadcast(user, broadcast_options, google_client = GoogleClient.new)
|
|
|
|
start_time, end_time = youtube_times
|
|
broadcast_options ||= {}
|
|
broadcast_options[:snippet] ||= {}
|
|
broadcast_options[:snippet][:title] ||= name
|
|
broadcast_options[:snippet][:description] ||= description
|
|
broadcast_options[:snippet][:scheduledStartTime] = start_time.utc.iso8601
|
|
#broadcast_options[:snippet][:scheduledEndTime] = end_time.utc.iso8601
|
|
|
|
broadcast_options[:status] ||= {}
|
|
broadcast_options[:status][:privacyStatus] ||= (fan_access ? 'public' : 'private')
|
|
|
|
broadcast_options[:contentDetails] ||= {}
|
|
|
|
# if false, this causes a 'request not authorized error'
|
|
# From: https://developers.google.com/youtube/v3/live/docs/liveBroadcasts
|
|
# If your channel does not have permission to disable recordings, and you attempt to insert a broadcast with the recordFromStart property set to false, the API will return a Forbidden error.
|
|
#broadcast_options[:contentDetails][:recordFromStart] ||= false
|
|
|
|
broadcast_data = google_client.create_broadcast(user, broadcast_options)
|
|
|
|
broadcast = Broadcast.new
|
|
broadcast.music_session_id = self.id
|
|
broadcast.user_id = user.id
|
|
broadcast.broadcast_id = broadcast_data["id"]
|
|
broadcast.update_broadcast_data(broadcast_data)
|
|
broadcast.save!
|
|
broadcast
|
|
end
|
|
|
|
def bind_broadcast(user, broadcast, google_client = GoogleClient.new)
|
|
|
|
bind_data = google_client.bind_broadcast(user, broadcast.broadcast_id, broadcast.stream_id)
|
|
broadcast.update_broadcast_data(bind_data)
|
|
broadcast.save!
|
|
broadcast
|
|
end
|
|
|
|
# broadcastStatus one of complete, live, testing
|
|
def transition_broadcast(user, broadcast, broadcastStatus, google_client = GoogleClient.new)
|
|
|
|
if broadcastStatus == 'testing' && (broadcast.broadcast_status == 'testing' || broadcast.broadcast_status == 'live' || broadcast.broadcast_status == 'testStarting' || broadcast.broadcast_status == 'liveStarting')
|
|
# short cut out; this in unnecessary
|
|
puts "SHORT CUT OUT OF TRANSITION TESTING TESTING"
|
|
return broadcast
|
|
end
|
|
|
|
if broadcastStatus == 'live' && broadcast.broadcast_status == 'live'
|
|
# short cut out; this in unnecessary
|
|
puts "SHORT CUT OUT OF TRANSITION TESTING LIVE"
|
|
return broadcast
|
|
end
|
|
|
|
bind_data = google_client.transition_broadcast(user, broadcast.broadcast_id, broadcastStatus)
|
|
broadcast.update_broadcast_data(bind_data)
|
|
broadcast.save!
|
|
broadcast
|
|
end
|
|
|
|
def youtube_times
|
|
start = scheduled_start_time
|
|
|
|
if start < Time.now
|
|
start = Time.now
|
|
end_time = start + safe_scheduled_duration
|
|
return [start, end_time]
|
|
else
|
|
return [start, scheduled_end_time]
|
|
end
|
|
|
|
end
|
|
|
|
def check_scheduling_info_changed
|
|
@scheduling_info_changed = scheduled_start_changed?
|
|
true
|
|
end
|
|
|
|
def update_scheduled_start
|
|
|
|
# it's very important that this only run if timezone changes, or scheduled_start changes
|
|
if self.scheduled_start && (self.scheduled_start_changed? || self.timezone_changed?)
|
|
self.scheduled_start = MusicSession.parse_scheduled_start(self.scheduled_start, self.timezone)
|
|
end
|
|
end
|
|
|
|
def comment_count
|
|
self.comments.size
|
|
end
|
|
|
|
# copies all relevant info for the recurring session
|
|
def copy
|
|
|
|
MusicSession.transaction do
|
|
# copy base music_session data
|
|
new_session = MusicSession.new
|
|
new_session.description = self.description
|
|
new_session.user_id = self.user_id
|
|
new_session.band_id = self.band_id
|
|
new_session.fan_access = self.fan_access
|
|
new_session.scheduled_start = self.scheduled_start + 1.week
|
|
new_session.scheduled_duration = self.scheduled_duration
|
|
new_session.musician_access = self.musician_access
|
|
new_session.approval_required = self.approval_required
|
|
new_session.friends_can_join = self.friends_can_join
|
|
new_session.fan_chat = self.fan_chat
|
|
new_session.genre_id = self.genre_id
|
|
new_session.legal_policy = self.legal_policy
|
|
new_session.language = self.language
|
|
new_session.name = self.name
|
|
new_session.recurring_mode = self.recurring_mode
|
|
new_session.timezone = self.timezone
|
|
new_session.open_rsvps = self.open_rsvps
|
|
new_session.is_unstructured_rsvp = self.is_unstructured_rsvp
|
|
new_session.legal_terms = true
|
|
new_session.session_controller = self.session_controller
|
|
new_session.school_id = self.school_id
|
|
new_session.is_platform_instructor = self.is_platform_instructor
|
|
|
|
# copy rsvp_slots, rsvp_requests, and rsvp_requests_rsvp_slots
|
|
RsvpSlot.where("music_session_id = '#{self.id}'").find_each do |slot|
|
|
new_slot = RsvpSlot.new
|
|
new_slot.instrument_id = slot.instrument_id
|
|
new_slot.proficiency_level = slot.proficiency_level
|
|
new_slot.is_unstructured_rsvp = slot.is_unstructured_rsvp
|
|
new_session.rsvp_slots << new_slot
|
|
|
|
# get the request for this slot that was approved (should only be ONE)
|
|
rsvp_request_slot = RsvpRequestRsvpSlot.where("chosen = true AND rsvp_slot_id = ?", slot.id).first
|
|
|
|
unless rsvp_request_slot.nil?
|
|
rsvp = RsvpRequest.find_by_id(rsvp_request_slot.rsvp_request_id)
|
|
new_rsvp = RsvpRequest.new
|
|
new_rsvp.user_id = rsvp.user_id
|
|
new_rsvp.music_session_id = rsvp.music_session_id
|
|
|
|
new_rsvp_req_slot = RsvpRequestRsvpSlot.new
|
|
new_rsvp_req_slot.rsvp_request = new_rsvp
|
|
new_rsvp_req_slot.rsvp_slot = new_slot
|
|
new_rsvp_req_slot.chosen = true
|
|
|
|
# .last => new_slot above
|
|
new_session.rsvp_slots.last.rsvp_requests_rsvp_slots << new_rsvp_req_slot
|
|
|
|
# if this slot was not chosen, try to get any RSVPs that were 1-time cancellations and copy those
|
|
else
|
|
rejected_req_slots = RsvpRequestRsvpSlot.where("(chosen is null OR chosen = FALSE) AND rsvp_slot_id = ?", slot.id).order("created_at ASC")
|
|
rejected_req_slots.each do |req_slot|
|
|
# get RsvpRequest corresponding to this RsvpRequestRsvpSlot
|
|
rsvp = RsvpRequest.find_by_id(req_slot.rsvp_request_id)
|
|
|
|
# if the RSVP was canceled (but all future sessions were NOT canceled), then copy this one and break
|
|
if rsvp.canceled && !rsvp.cancel_all
|
|
new_rsvp = RsvpRequest.new
|
|
new_rsvp.user_id = rsvp.user_id
|
|
new_rsvp.music_session_id = rsvp.music_session_id
|
|
|
|
new_rsvp_req_slot = RsvpRequestRsvpSlot.new
|
|
new_rsvp_req_slot.rsvp_request = new_rsvp
|
|
new_rsvp_req_slot.rsvp_slot = new_slot
|
|
new_rsvp_req_slot.chosen = nil
|
|
|
|
# .last => new_slot above
|
|
new_session.rsvp_slots.last.rsvp_requests_rsvp_slots << new_rsvp_req_slot
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
# copy music_notations
|
|
MusicNotation.where("music_session_id = '#{self.id}'").find_each do |notation|
|
|
new_notation = MusicNotation.new
|
|
new_notation.user_id = notation.user_id
|
|
new_notation.music_session = new_session
|
|
new_notation.file_url = notation.file_url
|
|
new_notation.file_name = notation.file_name
|
|
new_notation.size = notation.size
|
|
new_session.music_notations << new_notation
|
|
end
|
|
|
|
new_session.save!
|
|
|
|
# mark the next session as scheduled
|
|
self.next_session_scheduled = true
|
|
self.save
|
|
|
|
return new_session
|
|
end
|
|
end
|
|
|
|
def is_lesson?
|
|
!!lesson_session
|
|
end
|
|
|
|
# checks if this is a lesson and if the person indicated is a teacher or student
|
|
def is_lesson_member?(user)
|
|
is_lesson? && (lesson_session.teacher.id == user.id || lesson_session.student.id == user.id)
|
|
end
|
|
|
|
def grouped_tracks
|
|
tracks = []
|
|
self.music_session_user_histories.each do |msuh|
|
|
user = User.find(msuh.user_id)
|
|
|
|
# see if user already exists in array
|
|
t = tracks.select { |track| track.musician.id == user.id }.first
|
|
|
|
if t.blank?
|
|
t = Track.new
|
|
t.musician = user
|
|
t.instrument_ids = []
|
|
|
|
# this treats each track as a "user", which has 1 or more instruments in the session
|
|
unless msuh.instruments.blank?
|
|
instruments = msuh.instruments.split(SEPARATOR)
|
|
instruments.each do |instrument|
|
|
if !t.instrument_ids.include? instrument
|
|
t.instrument_ids << instrument
|
|
end
|
|
end
|
|
end
|
|
tracks << t
|
|
# this handles case where user is duplicated in MSUH for the same session
|
|
else
|
|
unless msuh.instruments.blank?
|
|
instruments = msuh.instruments.split(SEPARATOR)
|
|
instruments.each do |instrument|
|
|
if !t.instrument_ids.include? instrument
|
|
t.instrument_ids << instrument
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
tracks
|
|
end
|
|
|
|
# is the viewer 'friensd with the session?'
|
|
# practically, to be a friend with the session, you have to be a friend with the creator
|
|
# we could loosen to be friend of friends or friends with anyone in the session
|
|
def friends_with_session(user)
|
|
self.creator.friends?(user)
|
|
end
|
|
|
|
def can_join? user, as_musician
|
|
if as_musician
|
|
unless user.musician
|
|
return false # "a fan can not join a music session as a musician"
|
|
end
|
|
|
|
if self.musician_access
|
|
if self.approval_required
|
|
return self.invited_musicians.exists?(user.id) || self.approved_rsvps.include?(user)
|
|
else
|
|
return true
|
|
end
|
|
else
|
|
# the creator can always join, and the invited users can join
|
|
return self.creator == user || self.invited_musicians.exists?(user.id) || self.approved_rsvps.include?(user) || (self.friends_can_join && self.friends_with_session(user))
|
|
end
|
|
else
|
|
# it's a fan, and the only way a fan can join is if fan_access is true
|
|
self.fan_access
|
|
end
|
|
end
|
|
|
|
def can_see? user
|
|
if self.musician_access || self.fan_access
|
|
true
|
|
else
|
|
self.creator == user || self.invited_musicians.exists?(user.id) || self.approved_rsvps.include?(user) || self.creator.friends?(user) || self.has_lesson_access?(user)
|
|
end
|
|
end
|
|
|
|
def has_lesson_access?(user)
|
|
if is_lesson?
|
|
puts "HAS LESSON ACCESS"
|
|
result = lesson_session.has_access?(user)
|
|
puts "HAS LESSON ACCESS #{ result}"
|
|
result
|
|
else
|
|
false
|
|
end
|
|
|
|
end
|
|
|
|
def set_session_controller(current_user, user)
|
|
|
|
# only allow update of session controller by the creator or the currently marked user
|
|
|
|
should_tick = false
|
|
|
|
if current_user != creator && current_user != self.session_controller
|
|
return should_tick
|
|
end
|
|
|
|
if active_music_session
|
|
if user
|
|
if active_music_session.users.exists?(user)
|
|
self.session_controller = user
|
|
should_tick = save
|
|
end
|
|
else
|
|
self.session_controller = nil
|
|
should_tick = save
|
|
end
|
|
end
|
|
should_tick
|
|
end
|
|
|
|
def self.index(current_user, user_id, band_id = nil, genre = nil)
|
|
hide_private = false
|
|
if current_user.id != user_id
|
|
hide_private = false # TODO: change to true once public flag exists
|
|
end
|
|
|
|
query = MusicSession
|
|
.joins(
|
|
%Q{
|
|
LEFT OUTER JOIN
|
|
music_sessions_user_history
|
|
ON
|
|
music_sessions.id = music_sessions_user_history.music_session_id
|
|
}
|
|
)
|
|
.where(
|
|
%Q{
|
|
music_sessions.user_id = '#{user_id}'
|
|
}
|
|
)
|
|
|
|
#query = query.where("public = false") unless !hide_private
|
|
query = query.where("music_sessions.band_id = '#{band_id}") unless band_id.nil?
|
|
query = query.where("music_sessions.genres like '%#{genre}%'") unless genre.nil?
|
|
return query
|
|
end
|
|
|
|
def self.scheduled user, only_public = false
|
|
# keep unstarted sessions around for 12 hours after scheduled_start
|
|
session_not_started = "(music_sessions.scheduled_start > NOW() - '12 hour'::INTERVAL AND music_sessions.started_at IS NULL)"
|
|
|
|
# keep started sessions that are not finished yet
|
|
session_started_not_finished = "(music_sessions.started_at IS NOT NULL AND music_sessions.session_removed_at IS NULL)"
|
|
|
|
# let session be restarted for up to 2 hours after finishing
|
|
session_finished = "(music_sessions.session_removed_at > NOW() - '2 hour'::INTERVAL)"
|
|
|
|
query = MusicSession.select('distinct music_sessions.*')
|
|
query = query.joins(
|
|
%Q{
|
|
LEFT OUTER JOIN
|
|
rsvp_requests
|
|
ON rsvp_requests.music_session_id = music_sessions.id AND rsvp_requests.user_id = '#{user.id}' AND rsvp_requests.chosen = TRUE
|
|
|
|
LEFT OUTER JOIN
|
|
invitations
|
|
ON
|
|
music_sessions.id = invitations.music_session_id AND invitations.receiver_id = '#{user.id}'
|
|
}
|
|
)
|
|
query = query.where("music_sessions.old = FALSE")
|
|
query = query.where("music_sessions.canceled = FALSE")
|
|
query = query.where('music_sessions.fan_access = TRUE or music_sessions.musician_access = TRUE') if only_public
|
|
#query = query.where("music_sessions.user_id = '#{user.id}' OR invitations.id IS NOT NULL")
|
|
query = query.where("(rsvp_requests.id IS NOT NULL) OR (invitations.id IS NOT NULL) OR (music_sessions.user_id = '#{user.id}') ")
|
|
|
|
query = Search.scope_schools_together_sessions(query, user, 'music_sessions')
|
|
|
|
query = query.where("music_sessions.scheduled_start IS NULL OR #{session_not_started} OR #{session_finished} OR #{session_started_not_finished}")
|
|
query = query.where("music_sessions.create_type IS NULL OR (music_sessions.create_type != '#{CREATE_TYPE_QUICK_START}' AND music_sessions.create_type != '#{CREATE_TYPE_QUICK_PUBLIC}')")
|
|
query = query.order("music_sessions.scheduled_start ASC")
|
|
|
|
query
|
|
end
|
|
|
|
# if only_approved is set, then only return sessions where the current user has been chosen
|
|
def self.scheduled_rsvp(user, only_approved = false)
|
|
|
|
filter_approved = only_approved ? 'AND rsvp_requests_rsvp_slots.chosen = true' : ''
|
|
|
|
query = MusicSession.joins(
|
|
%Q{
|
|
INNER JOIN
|
|
rsvp_slots
|
|
ON
|
|
music_sessions.id = rsvp_slots.music_session_id
|
|
INNER JOIN
|
|
rsvp_requests_rsvp_slots
|
|
ON
|
|
rsvp_requests_rsvp_slots.rsvp_slot_id = rsvp_slots.id
|
|
INNER JOIN
|
|
rsvp_requests
|
|
ON rsvp_requests.id = rsvp_requests_rsvp_slots.rsvp_request_id
|
|
}
|
|
)
|
|
|
|
query = Search.scope_schools_together_sessions(query, user, 'music_sessions')
|
|
|
|
query = query.where(%Q{music_sessions.old = FALSE AND music_sessions.canceled = FALSE AND
|
|
(music_sessions.create_type is NULL OR (music_sessions.create_type != '#{CREATE_TYPE_QUICK_START}' AND music_sessions.create_type != '#{CREATE_TYPE_QUICK_PUBLIC}')) AND
|
|
(music_sessions.scheduled_start is NULL OR music_sessions.scheduled_start > NOW() - '4 hour'::INTERVAL) AND rsvp_requests.user_id = '#{user.id}' #{filter_approved}}
|
|
).order(:scheduled_start)
|
|
query
|
|
end
|
|
|
|
def self.create user, options
|
|
band = Band.where(id: options[:band]).first if options[:band].present?
|
|
|
|
ms = MusicSession.new
|
|
ms.name = options[:name]
|
|
ms.description = options[:description]
|
|
ms.genre_id = (options[:genres].length > 0 ? options[:genres][0] : nil) if options[:genres]
|
|
ms.musician_access = options[:musician_access]
|
|
ms.friends_can_join = options[:friends_can_join] || false
|
|
ms.approval_required = options[:approval_required]
|
|
ms.fan_access = options[:fan_access]
|
|
ms.fan_chat = options[:fan_chat]
|
|
ms.band = band
|
|
ms.legal_policy = options[:legal_policy]
|
|
ms.language = options[:language]
|
|
ms.scheduled_duration = options[:duration].to_i * 1.minutes if options[:duration]
|
|
ms.recurring_mode = options[:recurring_mode] if options[:recurring_mode]
|
|
ms.timezone = options[:timezone] if options[:timezone]
|
|
ms.legal_terms = true
|
|
ms.open_rsvps = options[:open_rsvps] if options[:open_rsvps]
|
|
ms.creator = user
|
|
ms.session_controller = user
|
|
ms.create_type = options[:create_type]
|
|
ms.is_unstructured_rsvp = options[:isUnstructuredRsvp] if options[:isUnstructuredRsvp]
|
|
ms.scheduled_start = parse_scheduled_start(options[:start], options[:timezone]) if options[:start] && options[:timezone]
|
|
ms.scheduled_start = options[:scheduled_start] if options[:scheduled_start]
|
|
ms.school_id = user.school_id
|
|
ms.is_platform_instructor = user.is_platform_instructor
|
|
if options[:lesson_session]
|
|
ms.lesson_session = options[:lesson_session]
|
|
end
|
|
|
|
|
|
ms.save
|
|
|
|
unless ms.errors.any?
|
|
ms.reload
|
|
|
|
rsvp_slot_ids = []
|
|
self_rsvp_slot_ids = []
|
|
options[:rsvp_slots].each do |rs|
|
|
rsvp = RsvpSlot.new
|
|
rsvp.instrument = Instrument.find(rs[:instrument_id])
|
|
rsvp.proficiency_level = rs[:proficiency_level]
|
|
rsvp.music_session = ms
|
|
rsvp.save
|
|
|
|
ms.rsvp_slots << rsvp
|
|
|
|
if rs[:approve] == true
|
|
self_rsvp_slot_ids.push rsvp.id
|
|
else
|
|
rsvp_slot_ids.push rsvp.id
|
|
end
|
|
end if options[:rsvp_slots]
|
|
RsvpRequest.create({session_id: ms.id, rsvp_slots: self_rsvp_slot_ids, :autoapprove => true}, user)
|
|
|
|
options[:invitations].each do |invite_id|
|
|
invitation = Invitation.new
|
|
receiver = User.find(invite_id)
|
|
invitation.sender = user
|
|
invitation.receiver = receiver
|
|
invitation.music_session = ms
|
|
invitation.save
|
|
|
|
ms.invitations << invitation
|
|
|
|
# send scheduled session notification or join session notification
|
|
case ms.create_type
|
|
when CREATE_TYPE_RSVP, CREATE_TYPE_SCHEDULE_FUTURE
|
|
Notification.send_scheduled_session_invitation(ms, receiver)
|
|
when CREATE_TYPE_LESSON
|
|
if ms.lesson_session.is_active?
|
|
Notification.send_jamclass_invitation_teacher(ms, receiver)
|
|
end
|
|
else
|
|
Notification.send_session_invitation(receiver, user, ms.id)
|
|
end
|
|
|
|
end if options[:invitations]
|
|
|
|
options[:music_notations].each do |notation|
|
|
notation = MusicNotation.find(notation[:id])
|
|
notation.music_session = ms
|
|
notation.save
|
|
|
|
ms.music_notations << notation
|
|
end if options[:music_notations]
|
|
|
|
ms.save
|
|
end
|
|
|
|
ms
|
|
end
|
|
|
|
def self.update user, options
|
|
music_session = MusicSession.find(options[:id])
|
|
if music_session.creator == current_user
|
|
Notification.send_scheduled_session_cancelled music_session
|
|
music_session.destroy
|
|
|
|
respond_with responder: ApiResponder, :status => 204
|
|
else
|
|
render :json => { :message => ValidationMessages::PERMISSION_VALIDATION_ERROR }, :status => 404
|
|
end
|
|
end
|
|
|
|
def unique_users
|
|
User
|
|
.joins(:music_session_user_histories)
|
|
.group("users.id")
|
|
.order("users.id")
|
|
.where(%Q{ music_sessions_user_history.music_session_id = '#{id}'})
|
|
end
|
|
|
|
# returns one user history per user, with instruments all crammed together, and with total duration
|
|
def unique_user_histories
|
|
|
|
# only get the active users if the session is in progress
|
|
user_filter = "music_sessions_user_history.session_removed_at is null" if self.session_removed_at.nil?
|
|
|
|
MusicSessionUserHistory
|
|
.joins(:user)
|
|
.select("STRING_AGG(instruments, '|') AS total_instruments,
|
|
SUM(date_part('epoch', COALESCE(music_sessions_user_history.session_removed_at, music_sessions_user_history.created_at) - music_sessions_user_history.created_at)) AS total_duration,
|
|
music_sessions_user_history.user_id, music_sessions_user_history.music_session_id, users.first_name, users.last_name, users.photo_url")
|
|
.group("music_sessions_user_history.user_id, music_sessions_user_history.music_session_id, users.first_name, users.last_name, users.photo_url")
|
|
.order("music_sessions_user_history.user_id")
|
|
.where(%Q{ music_sessions_user_history.music_session_id = '#{id}'})
|
|
.where(user_filter)
|
|
end
|
|
|
|
def duration_minutes
|
|
end_time = self.session_removed_at || Time.now.utc
|
|
(end_time - self.created_at) / 60.0
|
|
end
|
|
|
|
def music_session_user_histories
|
|
@msuh ||= JamRuby::MusicSessionUserHistory
|
|
.where(:music_session_id => self.id)
|
|
.order('created_at DESC')
|
|
end
|
|
|
|
def comments
|
|
@comments ||= JamRuby::MusicSessionComment
|
|
.where(:music_session_id => self.id)
|
|
.order('created_at DESC')
|
|
end
|
|
|
|
def likes
|
|
@likes ||= JamRuby::MusicSessionLiker
|
|
.where(:music_session_id => self.music_session_id)
|
|
end
|
|
|
|
# these are 'users that are a part of this session'
|
|
# which means are currently in the music_session, or, rsvp'ed, or creator
|
|
def part_of_session? user
|
|
# XXX check RSVP'ed
|
|
user == self.creator || (active_music_session ? active_music_session.users.exists?(user.id) : false)
|
|
end
|
|
|
|
def is_over?
|
|
active_music_session.nil?
|
|
end
|
|
|
|
def has_mount?
|
|
!active_music_session.nil? && !active_music_session.mount.nil?
|
|
end
|
|
|
|
def can_cancel? user
|
|
self.creator == user
|
|
end
|
|
|
|
def legal_policy_url
|
|
# TODO: move to DB or config file or helper
|
|
case legal_policy.downcase
|
|
when "standard"
|
|
return "session-legal-policies/standard"
|
|
when "creative"
|
|
return "session-legal-policies/creativecommons"
|
|
when "offline"
|
|
return "session-legal-policies/offline"
|
|
when "jamtracks"
|
|
return "session-legal-policies/jamtracks"
|
|
else
|
|
return ""
|
|
end
|
|
end
|
|
|
|
def language_description
|
|
if self.language.blank?
|
|
self.language = "en"
|
|
end
|
|
|
|
iso639Details = ISO_639.find_by_code(self.language)
|
|
|
|
unless iso639Details.blank?
|
|
return iso639Details.english_name
|
|
else
|
|
return "English"
|
|
end
|
|
end
|
|
|
|
def scheduled_end_time
|
|
start = scheduled_start_time
|
|
duration = safe_scheduled_duration
|
|
start + duration
|
|
end
|
|
|
|
def timezone_id
|
|
MusicSession.split_timezone(timezone)[0]
|
|
end
|
|
|
|
def timezone_description
|
|
MusicSession.split_timezone(timezone)[1]
|
|
end
|
|
|
|
def musician_access_description
|
|
if self.musician_access && self.approval_required
|
|
"Musicians may join by approval"
|
|
elsif self.musician_access && !self.approval_required
|
|
"Musicians may join at will"
|
|
elsif !self.musician_access && !self.approval_required
|
|
"Only RSVP musicians may join"
|
|
end
|
|
end
|
|
|
|
def fan_access_description
|
|
if self.fan_access && self.fan_chat
|
|
"Fans may listen, chat with the band"
|
|
elsif self.fan_access && !self.fan_chat
|
|
"Fans may listen, chat with each other"
|
|
elsif !self.fan_access && !self.fan_chat
|
|
"Fans may not listen to session"
|
|
end
|
|
end
|
|
|
|
def access_description
|
|
"#{musician_access_description}. #{fan_access_description}."
|
|
end
|
|
|
|
# retrieve users that have approved RSVPs
|
|
def approved_rsvps
|
|
User.find_by_sql(%Q{select distinct ON(u.id) u.id, u.photo_url, u.first_name, u.last_name, u.last_jam_audio_latency, json_agg(ii.id) as instrument_ids, json_agg(ii.description) as instrument_descriptions, json_agg(rs.proficiency_level) as instrument_proficiencies, json_agg(rr.id) as rsvp_request_ids
|
|
from rsvp_slots rs
|
|
inner join rsvp_requests_rsvp_slots rrrs on rrrs.rsvp_slot_id = rs.id
|
|
inner join rsvp_requests rr on rrrs.rsvp_request_id = rr.id
|
|
left join instruments ii on ii.id = rs.instrument_id
|
|
inner join users u on u.id = rr.user_id
|
|
where rrrs.chosen = true AND rs.music_session_id = '#{self.id}' AND rr.canceled != TRUE
|
|
group by u.id order by u.id}
|
|
)
|
|
end
|
|
|
|
# get all slots for this session and perform a set difference with all chosen slots;
|
|
# this will return those that are not filled yet
|
|
# this method excludes rsvp_slots marked as 'is_unstructured_rsvp = true'
|
|
def open_slots
|
|
RsvpSlot.find_by_sql(%Q{select rs.*, ii.description
|
|
from rsvp_slots rs
|
|
inner join instruments ii on ii.id = rs.instrument_id
|
|
where rs.music_session_id = '#{self.id}'
|
|
and rs.is_unstructured_rsvp = false
|
|
except
|
|
select distinct rs.*, iii.description
|
|
from rsvp_slots rs
|
|
inner join instruments iii on iii.id = rs.instrument_id
|
|
inner join rsvp_requests_rsvp_slots rrrs on rrrs.rsvp_slot_id = rs.id
|
|
where rs.music_session_id = '#{self.id}'
|
|
and rrrs.chosen = true
|
|
}
|
|
)
|
|
end
|
|
|
|
# retrieve users that have invitations but have not submitted an RSVP request for this session
|
|
def pending_invitations
|
|
User.find_by_sql(%Q{select distinct u.id, u.email, u.photo_url, u.first_name, u.last_name, u.online
|
|
from users u
|
|
inner join invitations i on u.id = i.receiver_id
|
|
left join rsvp_requests rr on rr.user_id = i.receiver_id
|
|
where i.music_session_id = '#{self.id}'
|
|
and rr.user_id is null}
|
|
)
|
|
end
|
|
|
|
# retrieve pending RsvpRequests
|
|
def pending_rsvp_requests
|
|
RsvpRequest.index(self, nil, {status: 'pending'})
|
|
end
|
|
|
|
def running_recordings
|
|
recordings.where(duration: nil)
|
|
end
|
|
def recordings
|
|
Recording.where(music_session_id: self.id)
|
|
end
|
|
|
|
def self.cleanup_old_sessions
|
|
old_scheduled_start = "(create_type is NOT NULL AND music_sessions.scheduled_start < NOW() - '24 hour'::INTERVAL)"
|
|
|
|
old_adhoc_sessions = "(create_type IS NULL and music_sessions.created_at < NOW() - '24 hour'::INTERVAL)"
|
|
|
|
MusicSession.where("#{old_scheduled_start} OR #{old_adhoc_sessions}").where(old:false).update_all(old: true)
|
|
end
|
|
|
|
def end_history
|
|
self.update_attribute(:session_removed_at, Time.now)
|
|
|
|
|
|
# ensure all user histories are closed
|
|
music_session_user_histories.each do |music_session_user_history|
|
|
music_session_user_history.end_history
|
|
|
|
# then update any users that need their user progress updated
|
|
if music_session_user_history.duration_minutes > 15 && music_session_user_history.max_concurrent_connections.to_i >= 3
|
|
music_session_user_history.user.update_progression_field(:first_real_music_session_at)
|
|
end
|
|
end
|
|
|
|
end
|
|
|
|
def self.removed_music_session(session_id)
|
|
hist = self
|
|
.where(:id => session_id)
|
|
.limit(1)
|
|
.first
|
|
|
|
hist.end_history if hist
|
|
|
|
Notification.send_session_ended(session_id)
|
|
end
|
|
|
|
def remove_non_alpha_num(token)
|
|
token.gsub(/[^0-9A-Za-z]/, '')
|
|
end
|
|
|
|
def tag
|
|
nil unless has_attribute?(:tag)
|
|
a = read_attribute(:tag)
|
|
a.nil? ? nil : a.to_i
|
|
end
|
|
|
|
def latency
|
|
nil unless has_attribute?(:latency)
|
|
a = read_attribute(:latency)
|
|
a.nil? ? nil : a.to_i
|
|
end
|
|
|
|
|
|
# initialize the two temporary tables we use to drive sms_index and sms_users
|
|
def self.sms_init(current_user, options = {}, include_pending=false)
|
|
session_id = options[:session_id] || 'any'
|
|
|
|
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' # Have to pass in zero; NULL fails silently in the stored proc
|
|
|
|
self.connection.execute("SELECT sms_index('#{current_user.id}'::varchar, #{locidispid_expr}, #{my_audio_latency}::integer, #{ActiveRecord::Base.connection.quote(session_id)}::varchar, #{include_pending}::boolean)").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. sms_init must be called
|
|
# first.
|
|
def self.sms_query(current_user, options = {})
|
|
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 sms_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 sms_music_session_tmp into the processing
|
|
# then we can join sms_music_session_tmp and not join active_music_sessions
|
|
query = query.joins(
|
|
%Q{
|
|
INNER JOIN
|
|
sms_music_session_tmp
|
|
ON
|
|
sms_music_session_tmp.music_session_id = music_sessions.id
|
|
}
|
|
)
|
|
.select('sms_music_session_tmp.tag, sms_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.create_type IS NULL OR (music_sessions.create_type != ? AND music_sessions.create_type != ? AND music_sessions.create_type != ?)", MusicSession::CREATE_TYPE_QUICK_START, MusicSession::CREATE_TYPE_IMMEDIATE, MusicSession::CREATE_TYPE_QUICK_PUBLIC)
|
|
query = query.where("music_sessions.genre_id = ?", genre) unless genre.blank?
|
|
query = query.where('music_sessions.language = ?', lang) unless lang.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 = '' # no offset to specify in this case
|
|
elsif 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
|
|
else
|
|
sql =<<SQL
|
|
music_sessions.started_at IS NULL AND
|
|
(music_sessions.created_at > NOW() - interval '#{UNSTARTED_INTERVAL_DAYS_SKIP} days' OR
|
|
music_sessions.scheduled_start > NOW() - interval '#{UNSTARTED_INTERVAL_DAYS_SKIP} days')
|
|
SQL
|
|
query = query.where(sql)
|
|
end
|
|
return query
|
|
end
|
|
|
|
# returns the set of users in a music_sessions and the music_session they are in and their latency.
|
|
# sms_init must be called first.
|
|
# user.audio_latency / 2 , + other_user.audio_latency of them / 2, + network latency /2
|
|
def self.sms_users
|
|
return User.select('users.*, sms_users_tmp.music_session_id, sms_users_tmp.full_score, sms_users_tmp.audio_latency, sms_users_tmp.internet_score')
|
|
.joins(
|
|
%Q{
|
|
INNER JOIN
|
|
sms_users_tmp
|
|
ON
|
|
sms_users_tmp.user_id = users.id
|
|
}
|
|
)
|
|
.order('sms_users_tmp.music_session_id, sms_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.sms_index(current_user, params)
|
|
MusicSession.sms_init(current_user, params)
|
|
|
|
music_sessions = MusicSession.sms_query(current_user, params).all
|
|
|
|
music_session_users = MusicSession.sms_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.scheduled_index(user, options)
|
|
session_id = options[:session_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.*')
|
|
query = query.where("old = FALSE")
|
|
query = query.where("scheduled_start IS NULL OR scheduled_start > (NOW() - (interval '15 minute'))")
|
|
query = query.where("music_sessions.canceled = FALSE")
|
|
query = query.where("description != 'Jam Track Session'")
|
|
query = query.where("music_sessions.id NOT IN (SELECT id FROM active_music_sessions)")
|
|
# one flaw in the join below is that we only consider if the creator of the session has asked you to be your friend, but you have not accepted. While this means you may always see a session by someone you haven't friended, the goal is to match up new users more quickly
|
|
query = query.joins(
|
|
%Q{
|
|
|
|
LEFT OUTER JOIN
|
|
rsvp_requests
|
|
ON rsvp_requests.music_session_id = music_sessions.id and rsvp_requests.user_id = '#{user.id}' AND rsvp_requests.chosen = true
|
|
|
|
LEFT OUTER JOIN
|
|
invitations
|
|
ON
|
|
music_sessions.id = invitations.music_session_id AND invitations.receiver_id = '#{user.id}'
|
|
LEFT OUTER JOIN
|
|
friendships
|
|
ON
|
|
music_sessions.user_id = friendships.user_id AND friendships.friend_id = '#{user.id}'
|
|
LEFT OUTER JOIN
|
|
friendships as friendships_2
|
|
ON
|
|
music_sessions.user_id = friendships_2.friend_id AND friendships_2.user_id = '#{user.id}'
|
|
}
|
|
)
|
|
|
|
# keep only rsvp/invitation/friend results. Nice tailored active list now!
|
|
query = query.where("open_rsvps = TRUE OR rsvp_requests.id IS NOT NULL OR invitations.id IS NOT NULL or music_sessions.user_id = '#{user.id}' OR (friendships.id IS NOT NULL AND friendships_2.id IS NOT NULL)")
|
|
|
|
query = Search.scope_schools_together_sessions(query, user, 'music_sessions')
|
|
|
|
# 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?
|
|
query = query.group("music_sessions.id, music_sessions.scheduled_start")
|
|
query = query.order("music_sessions.scheduled_start DESC")
|
|
|
|
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 = '' # no offset to specify in this case
|
|
elsif 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
|
|
else
|
|
sql =<<SQL
|
|
music_sessions.started_at IS NULL AND
|
|
(music_sessions.created_at > NOW() - interval '#{UNSTARTED_INTERVAL_DAYS_SKIP} days' OR
|
|
music_sessions.scheduled_start > NOW() - interval '#{UNSTARTED_INTERVAL_DAYS_SKIP} days')
|
|
SQL
|
|
query = query.where(sql)
|
|
end
|
|
|
|
#FROM music_sessions
|
|
# WHERE old = FALSE AND (scheduled_start IS NULL OR scheduled_start > (NOW() - (interval '15 minute')))
|
|
# AND canceled = FALSE AND description != 'Jam Track Session'
|
|
# AND id NOT IN (SELECT id FROM active_music_sessions);
|
|
|
|
query
|
|
end
|
|
|
|
# returns a single session, but populates any other user info with latency scores, so that show_history.rabl can do it's business
|
|
def self.session_with_scores(current_user, music_session_id, include_pending=false)
|
|
MusicSession.sms_init(current_user, {session_id: music_session_id}, include_pending)
|
|
|
|
music_session = MusicSession.find(music_session_id)
|
|
|
|
music_session_users = MusicSession.sms_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_session, user_scores]
|
|
end
|
|
|
|
def self.upcoming_sessions
|
|
|
|
end
|
|
# converts the passed scheduled_start into the database timezone using the specified timezone offset.
|
|
# timezone comes in as TIMEZONE DISPLAY, TIMEZONE ID
|
|
def self.parse_scheduled_start(scheduled_start, timezone_param)
|
|
|
|
result = scheduled_start
|
|
|
|
tz_identifier = split_timezone(timezone_param)[0]
|
|
begin
|
|
timezone = ActiveSupport::TimeZone.new(tz_identifier)
|
|
rescue Exception => e
|
|
@@log.error("unable to find timezone=#{tz_identifier}, e=#{e}")
|
|
puts "unable to find timezone=#{tz_identifier}, e=#{e}"
|
|
end
|
|
|
|
if timezone
|
|
begin
|
|
# first convert the time provided, and convert to the specified timezone (local_to_utc)
|
|
# then, convert that to the system timezone, under the ASSUMPTION that the database is configured to use the system timezone
|
|
# you can get into trouble if your dev database is not using the system timezone of the web machine
|
|
|
|
result = timezone.parse(scheduled_start)
|
|
rescue Exception => e
|
|
@@log.error("unable to convert #{scheduled_start} to #{timezone}, e=#{e}")
|
|
puts "unable to convert #{scheduled_start} to #{timezone}, e=#{e}"
|
|
end
|
|
end
|
|
|
|
result
|
|
end
|
|
|
|
def scheduled_start_date
|
|
if self.scheduled_start_time.blank?
|
|
""
|
|
else
|
|
self.scheduled_start.strftime "%a %e %B %Y"
|
|
end
|
|
end
|
|
|
|
def scheduled_start_time
|
|
if scheduled_start && scheduled_duration
|
|
start_time = scheduled_start
|
|
tz_identifier, tz_display = MusicSession.split_timezone(timezone)
|
|
begin
|
|
tz = TZInfo::Timezone.get(tz_identifier)
|
|
rescue Exception => e
|
|
@@log.error("unable to find timezone=#{tz_identifier}, e=#{e}")
|
|
end
|
|
|
|
if tz
|
|
begin
|
|
start_time = tz.utc_to_local(scheduled_start.utc)
|
|
rescue Exception => e
|
|
@@log.error("unable to convert #{scheduled_start} to #{tz}, e=#{e}")
|
|
puts "unable to convert #{e}"
|
|
end
|
|
end
|
|
|
|
start_time
|
|
else
|
|
""
|
|
end
|
|
end
|
|
|
|
# takes our stored timezone which is DISPLAY,ID and returns an array of the two values (id first, then description)
|
|
def self.split_timezone(tz)
|
|
result = [nil, nil]
|
|
if tz
|
|
index = tz.rindex(',')
|
|
if index
|
|
tz_display = tz[0, index]
|
|
tz_identifier = tz[(index + 1)..-1]
|
|
result = [tz_identifier, tz_display]
|
|
end
|
|
end
|
|
result
|
|
end
|
|
|
|
def safe_scheduled_duration
|
|
duration = scheduled_duration
|
|
# you can put seconds into the scheduled_duration field, but once stored, it comes back out as a string
|
|
if scheduled_duration.class == String
|
|
duration = scheduled_duration.to_i.seconds
|
|
end
|
|
if duration == 0
|
|
duration = 30 * 60
|
|
end
|
|
duration
|
|
end
|
|
|
|
def pretty_scheduled_start(with_timezone = true, shorter = false, user_tz = nil)
|
|
|
|
if scheduled_start && scheduled_duration
|
|
start_time = scheduled_start
|
|
timezone_display = 'UTC'
|
|
utc_offset_display = '00:00'
|
|
tz_identifier, tz_display = MusicSession.split_timezone(timezone)
|
|
short_tz = 'GMT'
|
|
|
|
if user_tz
|
|
tz_identifier = user_tz
|
|
end
|
|
begin
|
|
tz = TZInfo::Timezone.get(tz_identifier)
|
|
rescue Exception => e
|
|
@@log.error("unable to find timezone=#{tz_identifier}, e=#{e}")
|
|
end
|
|
|
|
if tz
|
|
begin
|
|
start_time = tz.utc_to_local(scheduled_start.utc)
|
|
timezone_display = tz.pretty_name
|
|
utc_offset_hours = tz.current_period.utc_total_offset / (60*60)
|
|
hour = sprintf '%02d', utc_offset_hours.abs
|
|
minutes = sprintf '%02d', ((tz.current_period.utc_total_offset.abs % 3600) / 3600) * 60
|
|
utc_offset_display = "#{utc_offset_hours < 0 ? '-' : ' '}#{hour}:#{minutes}"
|
|
short_tz = start_time.strftime("%Z")
|
|
if short_tz == 'UTC'
|
|
short_tz = 'GMT'
|
|
end
|
|
|
|
rescue Exception => e
|
|
@@log.error("unable to convert #{scheduled_start} to #{tz}, e=#{e}")
|
|
puts "unable to convert #{e}"
|
|
end
|
|
end
|
|
|
|
|
|
duration = safe_scheduled_duration
|
|
end_time = start_time + duration
|
|
if with_timezone
|
|
if shorter
|
|
#"#{start_time.strftime("%a, %b %e %Y")}, #{start_time.strftime("%l:%M").strip}-#{end_time.strftime("%l:%M %p").strip} (#{short_tz}#{utc_offset_display})"
|
|
"#{start_time.strftime("%a, %b %e %Y")}, #{start_time.strftime("%l:%M").strip}-#{end_time.strftime("%l:%M %p").strip} (#{timezone_display})"
|
|
else
|
|
"#{start_time.strftime("%A, %B %e")}, #{start_time.strftime("%l:%M").strip}-#{end_time.strftime("%l:%M %p").strip} #{timezone_display}"
|
|
end
|
|
else
|
|
"#{start_time.strftime("%A, %B %e")} - #{end_time.strftime("%l:%M%P").strip}"
|
|
end
|
|
|
|
else
|
|
"Date and time TBD"
|
|
end
|
|
|
|
end
|
|
|
|
def self.purgeable_sessions
|
|
sessions = []
|
|
sql =<<SQL
|
|
(started_at IS NULL AND
|
|
(created_at <= NOW() - interval '#{UNSTARTED_INTERVAL_DAYS_PURGE} days' OR
|
|
scheduled_start <= NOW() - interval '#{UNSTARTED_INTERVAL_DAYS_PURGE} days'))
|
|
OR
|
|
(recurring_mode = '#{NO_RECURRING}' AND
|
|
scheduled_start <= NOW() - interval '#{UNSTARTED_INTERVAL_DAYS_PURGE} days')
|
|
OR
|
|
(recurring_mode = '#{RECURRING_WEEKLY}' AND
|
|
scheduled_start <= NOW() - interval '#{UNSTARTED_INTERVAL_DAYS_PURGE_RECUR} days')
|
|
SQL
|
|
self.where("lesson_session_id is NULL").where("started_at IS NULL").where(sql).find_each do |ms|
|
|
block_given? ? yield(ms) : sessions << ms
|
|
end
|
|
sessions
|
|
end
|
|
|
|
def admin_url
|
|
APP_CONFIG.admin_root_url + "/admin/music_sessions/" + id
|
|
end
|
|
|
|
private
|
|
def generate_share_token
|
|
|
|
token = loop do
|
|
token = SecureRandom.urlsafe_base64(SHARE_TOKEN_LENGTH, false)
|
|
token = remove_non_alpha_num(token)
|
|
token.upcase!
|
|
break token unless ShareToken.exists?(token: token)
|
|
end
|
|
|
|
self.share_token = ShareToken.new
|
|
self.share_token.token = token
|
|
self.share_token.shareable_type = "session"
|
|
end
|
|
|
|
def creator_is_musician
|
|
unless creator && creator.musician?
|
|
errors.add(:creator, ValidationMessages::MUST_BE_A_MUSICIAN)
|
|
end
|
|
end
|
|
|
|
def validate_timezone
|
|
if timezone
|
|
index = timezone.rindex(',')
|
|
if index
|
|
tz_identifier = timezone[(index + 1)..-1]
|
|
begin
|
|
TZInfo::Timezone.get(tz_identifier)
|
|
rescue Exception => e
|
|
@@log.error("unable to find timezone=#{tz_identifier}, e=#{e}")
|
|
errors.add(:timezone, ValidationMessages::MUST_BE_KNOWN_TIMEZONE)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|