diff --git a/admin/Gemfile b/admin/Gemfile index 716543c5e..21e2c819a 100644 --- a/admin/Gemfile +++ b/admin/Gemfile @@ -117,4 +117,4 @@ end gem 'pry' gem 'pry-remote' gem 'pry-stack_explorer' -gem 'pry-debugger' +#gem 'pry-debugger' diff --git a/admin/app/admin/connections.rb b/admin/app/admin/connections.rb index b97e71164..92cfb668e 100644 --- a/admin/app/admin/connections.rb +++ b/admin/app/admin/connections.rb @@ -91,6 +91,7 @@ ActiveAdmin.register JamRuby::Connection, :as => 'Connection' do row :locidispid row :aasm_state row :udp_reachable + row :is_network_testing row :scoring_failures row :scoring_timeout_occurrences row :scoring_failures_offset diff --git a/admin/app/admin/scoring_load.rb b/admin/app/admin/scoring_load.rb index e020a4316..f6f3e3b2c 100644 --- a/admin/app/admin/scoring_load.rb +++ b/admin/app/admin/scoring_load.rb @@ -5,7 +5,7 @@ ActiveAdmin.register_page "Current Scoring Load" do table_for GetWork.summary do column "Work", :work_count column "Who", Proc.new { |connection| "#{connection.first_name} #{connection.last_name} - #{connection.email}" } - column "Errors", Proc.new { |connection| "#{connection.udp_reachable != false ? "" : "No STUN,"} #{connection.in_timeout != 'f' ? "Timeout," : ""} #{connection.in_session != 'f' ? "In-Session," : ""}" } + column "Errors", Proc.new { |connection| "#{connection.udp_reachable != false ? "" : "No STUN,"} #{connection.is_network_testing != false ? "NETWORK TESTING" : ""} #{connection.in_timeout != 'f' ? "Timeout," : ""} #{connection.in_session != 'f' ? "In-Session," : ""}" } column "Total Timeouts", :scoring_timeout_occurrences column "Current Scoring Failures", :scoring_failures column "Offset", :scoring_failures_offset diff --git a/db/manifest b/db/manifest index ae595ece9..5b66bc20d 100755 --- a/db/manifest +++ b/db/manifest @@ -216,4 +216,7 @@ fix_find_session_sorting_2216c.sql entabulate_current_network_scores.sql discard_scores_changed.sql emails_from_update.sql - +add_active_feed.sql +connection_network_testing.sql +video_sources.sql +recorded_videos.sql diff --git a/db/up/add_active_feed.sql b/db/up/add_active_feed.sql new file mode 100644 index 000000000..725b36ec6 --- /dev/null +++ b/db/up/add_active_feed.sql @@ -0,0 +1 @@ +alter table feeds add column active BOOLEAN DEFAULT FALSE; \ No newline at end of file diff --git a/db/up/connection_network_testing.sql b/db/up/connection_network_testing.sql new file mode 100644 index 000000000..3cd9e78df --- /dev/null +++ b/db/up/connection_network_testing.sql @@ -0,0 +1,44 @@ +-- let the server know if the client is network testing. If so, then also remove them from work +ALTER TABLE connections ADD COLUMN is_network_testing BOOLEAN DEFAULT FALSE NOT NULL; + +DROP FUNCTION IF EXISTS get_work (my_client_id VARCHAR(64), mylocidispid BIGINT, myaddr BIGINT, return_rows INT, stale_score INTERVAL); +CREATE FUNCTION get_work (my_client_id VARCHAR(64), mylocidispid BIGINT, myaddr BIGINT, return_rows INT, stale_score INTERVAL) RETURNS TABLE (client_id VARCHAR(64)) VOLATILE AS $$ +BEGIN + RETURN QUERY WITH + scorable_locations AS ( + SELECT DISTINCT locidispid FROM connections WHERE client_type = 'client' AND connections.client_id != my_client_id AND addr != myaddr AND udp_reachable AND is_network_testing = FALSE AND NOW() > scoring_timeout AND connections.music_session_id IS NULL AND + locidispid NOT IN (SELECT DISTINCT blocidispid FROM most_recent_scores WHERE alocidispid = mylocidispid AND (current_timestamp - score_dt) < stale_score) AND + locidispid/1000000 IN (SELECT locid FROM geoiplocations WHERE geog && st_buffer((SELECT geog FROM geoiplocations WHERE locid = mylocidispid/1000000), 4023360)) + ) + + SELECT tmp.client_id FROM (SELECT connections.client_id, random() AS r, row_number() OVER (PARTITION BY connections.locidispid) AS rownum FROM connections, scorable_locations + WHERE connections.locidispid = scorable_locations.locidispid AND client_type = 'client' AND connections.client_id != my_client_id AND addr != myaddr AND udp_reachable AND NOW() > scoring_timeout AND connections.music_session_id IS NULL ) tmp WHERE rownum <= 1 ORDER BY r LIMIT return_rows; + + RETURN; + +END; +$$ LANGUAGE plpgsql; + + +DROP FUNCTION IF EXISTS get_work_summary (stale_score INTERVAL); +CREATE FUNCTION get_work_summary (stale_score INTERVAL) RETURNS TABLE (work_count BIGINT, client_id VARCHAR(64), email VARCHAR, first_name VARCHAR, last_name VARCHAR, user_id VARCHAR(64), udp_reachable BOOLEAN, in_timeout BOOLEAN, in_session BOOLEAN, scoring_failures INT, scoring_failures_offset INT, scoring_timeout_occurrences INT, is_network_testing BOOLEAN) VOLATILE AS $$ +BEGIN + RETURN QUERY + SELECT SUM(CASE WHEN tmp.test_client_id IS NULL OR tmp.in_session OR tmp.in_timeout OR tmp.udp_reachable = FALSE OR tmp.is_network_testing = TRUE THEN 0 ELSE 1 END) AS work_count, tmp.client_id AS client_id, users.email, users.first_name, users.last_name, users.id AS user_id, tmp.udp_reachable, tmp.in_timeout, tmp.in_session, tmp.scoring_failures, tmp.scoring_failures_offset, tmp.scoring_timeout_occurrences, tmp.is_network_testing FROM + (SELECT connections.client_type, scorable_locations.client_id AS test_client_id, connections.client_id AS client_id, connections.user_id AS user_id, connections.udp_reachable, connections.scoring_timeout > NOW() as in_timeout, connections.music_session_id IS NOT NULL AS in_session, connections.scoring_failures, connections.scoring_failures_offset, connections.scoring_timeout_occurrences, connections.is_network_testing, scorable_locations.client_id IS NULL AS same_client, row_number() OVER (PARTITION BY connections.locidispid) AS rownum FROM connections LEFT OUTER JOIN scorable_locations(connections.client_id, connections.locidispid, connections.addr, stale_score) + ON connections.locidispid != scorable_locations.locidispid) tmp INNER JOIN users ON tmp.user_id = users.id WHERE tmp.client_type = 'client' GROUP BY tmp.client_id, users.email, users.first_name, users.last_name, users.id, tmp.same_client, tmp.udp_reachable, tmp.in_timeout, tmp.in_session, tmp.scoring_failures, tmp.scoring_failures_offset, tmp.scoring_timeout_occurrences, tmp.is_network_testing ORDER BY work_count DESC; + RETURN; +END; +$$ LANGUAGE plpgsql; + +DROP FUNCTION IF EXISTS get_work_summary_no_agg(stale_score INTERVAL); +-- useful for debugging get_work_summary +CREATE FUNCTION get_work_summary_no_agg (stale_score INTERVAL) RETURNS TABLE (client_id VARCHAR(64), test_client_id VARCHAR(64), email VARCHAR, first_name VARCHAR, last_name VARCHAR, user_id VARCHAR(64), udp_reachable BOOLEAN, in_timeout BOOLEAN, scoring_failures INT, scoring_failures_offset INT, scoring_timeout_occurrences INT, is_network_testing BOOLEAN) VOLATILE AS $$ +BEGIN + RETURN QUERY + SELECT tmp.client_id AS client_id, tmp.test_client_id, users.email, users.first_name, users.last_name, users.id AS user_id, tmp.udp_reachable, tmp.in_timeout, tmp.scoring_failures, tmp.scoring_failures_offset, tmp.scoring_timeout_occurrences, tmp.is_network_testing FROM + (SELECT scorable_locations.client_id AS test_client_id, connections.client_id AS client_id, connections.user_id AS user_id, connections.udp_reachable, connections.scoring_timeout > NOW() as in_timeout, connections.scoring_failures, connections.scoring_failures_offset, connections.scoring_timeout_occurrences, connections.is_network_testing, scorable_locations.client_id IS NULL AS same_client, row_number() OVER (PARTITION BY connections.locidispid) AS rownum FROM connections LEFT OUTER JOIN scorable_locations(connections.client_id, connections.locidispid, connections.addr, stale_score) + ON connections.locidispid != scorable_locations.locidispid AND connections.client_type = 'client') tmp INNER JOIN users ON tmp.user_id = users.id ORDER BY tmp.client_id; + RETURN; +END; +$$ LANGUAGE plpgsql; \ No newline at end of file diff --git a/db/up/recorded_videos.sql b/db/up/recorded_videos.sql new file mode 100644 index 000000000..fc8e22814 --- /dev/null +++ b/db/up/recorded_videos.sql @@ -0,0 +1,17 @@ +CREATE TABLE recorded_videos ( + id BIGINT PRIMARY KEY, + user_id VARCHAR(64) REFERENCES users(id) ON DELETE CASCADE, + fully_uploaded BOOLEAN NOT NULL DEFAULT FALSE, + recording_id VARCHAR(64) NOT NULL, + length BIGINT, + client_video_source_id VARCHAR(64) NOT NULL, + url VARCHAR(1024), + file_offset BIGINT, + upload_failures INTEGER NOT NULL DEFAULT 0, + discard BOOLEAN, + + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +ALTER TABLE recorded_videos ALTER COLUMN id SET DEFAULT nextval('tracks_next_tracker_seq'); diff --git a/db/up/video_sources.sql b/db/up/video_sources.sql new file mode 100644 index 000000000..cab646c94 --- /dev/null +++ b/db/up/video_sources.sql @@ -0,0 +1,9 @@ +CREATE TABLE video_sources ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + connection_id VARCHAR(64) NOT NULL, + client_video_source_id VARCHAR(64) NOT NULL, + + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + diff --git a/ruby/Gemfile b/ruby/Gemfile index 8352f613f..8457d4649 100644 --- a/ruby/Gemfile +++ b/ruby/Gemfile @@ -31,7 +31,7 @@ gem 'sendgrid', '1.2.0' gem 'aws-sdk' #, '1.29.1' gem 'carrierwave', '0.9.0' gem 'aasm', '3.0.16' -gem 'devise', '>= 1.1.2' +gem 'devise', '3.3.0' # 3.4.0 causes: uninitialized constant ActionController::Metal (NameError) gem 'postgres-copy' gem 'geokit' gem 'geokit-rails' diff --git a/ruby/lib/jam_ruby.rb b/ruby/lib/jam_ruby.rb index eb8fc345e..9daaa1e8d 100755 --- a/ruby/lib/jam_ruby.rb +++ b/ruby/lib/jam_ruby.rb @@ -173,6 +173,8 @@ require "jam_ruby/models/chat_message" require "jam_ruby/models/generic_state" require "jam_ruby/models/score_history" require "jam_ruby/models/jam_company" +require "jam_ruby/models/video_source" +require "jam_ruby/models/recorded_video" include Jampb diff --git a/ruby/lib/jam_ruby/app/mailers/user_mailer.rb b/ruby/lib/jam_ruby/app/mailers/user_mailer.rb index ccc0cf52c..46275db4a 100644 --- a/ruby/lib/jam_ruby/app/mailers/user_mailer.rb +++ b/ruby/lib/jam_ruby/app/mailers/user_mailer.rb @@ -412,26 +412,28 @@ end end - def scheduled_session_comment(user, msg, comment, session) - return if !user.subscribe_email + def scheduled_session_comment(target_user, sender, msg, comment, session) + return if !target_user.subscribe_email - email = user.email + email = target_user.email subject = "New Session Comment" unique_args = {:type => "scheduled_session_comment"} @body = msg @session_name = session.name @session_date = session.pretty_scheduled_start(true) @comment = comment + @sender = sender + @suppress_user_has_account_footer = true @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session.id}/details" sendgrid_category "Notification" sendgrid_unique_args :type => unique_args[:type] sendgrid_recipients([email]) - sendgrid_substitute('@USERID', [user.id]) + sendgrid_substitute('@USERID', [target_user.id]) mail(:to => email, :subject => subject) do |format| format.text - format.html + format.html { render :layout => "from_user_mailer" } end end diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_comment.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_comment.html.erb index 7ebe56c8f..a367a7444 100644 --- a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_comment.html.erb +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_comment.html.erb @@ -1,7 +1,11 @@ -<% provide(:title, 'Scheduled Session Comment') %> +<% provide(:title, "Scheduled Session Comment from #{@sender.name}") %> +<% provide(:photo_url, @sender.resolved_photo_url) %> -

<%= @body %>

+<% content_for :note do %> +

<%= @comment %>

-

<%= @comment %>

+

<%= @session_name %>

+

<%= @session_date %>

-

View Session Details

\ No newline at end of file +

View Session Details

+<% end %> \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_comment.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_comment.text.erb index 6a7dc5f59..5bb7377a2 100644 --- a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_comment.text.erb +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_comment.text.erb @@ -5,4 +5,6 @@ <%= @comment %> +- <%= @sender.name %> + See session details at <%= @session_url %>. \ No newline at end of file diff --git a/ruby/lib/jam_ruby/connection_manager.rb b/ruby/lib/jam_ruby/connection_manager.rb index e8349c97d..c2f22df30 100644 --- a/ruby/lib/jam_ruby/connection_manager.rb +++ b/ruby/lib/jam_ruby/connection_manager.rb @@ -89,7 +89,7 @@ module JamRuby udp_reachable_value = udp_reachable.nil? ? 'udp_reachable' : udp_reachable sql =< "JamRuby::ActiveMusicSession", foreign_key: :music_session_id has_one :latency_tester, class_name: 'JamRuby::LatencyTester', foreign_key: :client_id, primary_key: :client_id has_many :tracks, :class_name => "JamRuby::Track", :inverse_of => :connection, :foreign_key => 'connection_id', :dependent => :delete_all + has_many :video_sources, :class_name => "JamRuby::VideoSource", :inverse_of => :connection, :foreign_key => 'connection_id', :dependent => :delete_all - validates :as_musician, :inclusion => {:in => [true, false]} + validates :as_musician, :inclusion => {:in => [true, false, nil]} validates :client_type, :inclusion => {:in => [TYPE_CLIENT, TYPE_BROWSER, TYPE_LATENCY_TESTER]} validates_numericality_of :last_jam_audio_latency, greater_than:0, :allow_nil => true validate :can_join_music_session, :if => :joining_session? @@ -162,12 +163,13 @@ module JamRuby true end - def join_the_session(music_session, as_musician, tracks, user, audio_latency) + def join_the_session(music_session, as_musician, tracks, user, audio_latency, videos=nil) self.music_session_id = music_session.id self.as_musician = as_musician self.joining_session = true self.joined_session_at = Time.now associate_tracks(tracks) unless tracks.nil? + associate_videos(videos) unless videos.nil? self.save # if user joins the session as a musician, update their addr and location @@ -192,6 +194,19 @@ module JamRuby end end + def associate_videos(videos) + unless videos.nil? + self.video_sources.clear() + videos.each do |video| + v = VideoSource.new + v.connection = self + v.client_video_source_id = video["client_video_source_id"] + v.save # todo what if it fails? + self.video_sources << v + end + end + end + def self.update_locidispids(use_copied = true) # using addr, we can rebuild locidispid diff --git a/ruby/lib/jam_ruby/models/feed.rb b/ruby/lib/jam_ruby/models/feed.rb index cd68b8bd4..c2448e511 100644 --- a/ruby/lib/jam_ruby/models/feed.rb +++ b/ruby/lib/jam_ruby/models/feed.rb @@ -47,13 +47,13 @@ module JamRuby # handle sort if sort == 'date' query = query.where("feeds.id < #{start}") - query = query.order('feeds.id DESC') + query = query.order('feeds.active DESC, feeds.id DESC') elsif sort == 'plays' query = query.offset(start) - query = query.order("COALESCE(recordings.play_count, music_sessions.play_count) DESC ") + query = query.order("feeds.active DESC, COALESCE(recordings.play_count, music_sessions.play_count) DESC") elsif sort == 'likes' query = query.offset(start) - query = query.order("COALESCE(recordings.like_count, music_sessions.like_count) DESC ") + query = query.order("feeds.active DESC, COALESCE(recordings.like_count, music_sessions.like_count) DESC") else raise "sort not implemented: #{sort}" end diff --git a/ruby/lib/jam_ruby/models/get_work.rb b/ruby/lib/jam_ruby/models/get_work.rb index 8946770fd..df5003925 100644 --- a/ruby/lib/jam_ruby/models/get_work.rb +++ b/ruby/lib/jam_ruby/models/get_work.rb @@ -12,6 +12,7 @@ module JamRuby def self.get_work_list(connection, rows = 25, staleness_hours = 120) + return [] if connection.is_network_testing # short-circuit 0 results if is_network_testing return [] unless connection.udp_reachable # short-circuit 0 results if udp_reachable return [] if connection.scoring_timeout > Time.now # short-circuit 0 results if in scoring timeout return [] if connection.in_session? @@ -25,7 +26,7 @@ module JamRuby end def self.summary(staleness_hours = 120) - r = GetWork.select([:work_count, :client_id, :email, :first_name, :last_name, :user_id, :udp_reachable, :in_timeout, :in_session, :scoring_failures, :scoring_failures_offset, :scoring_timeout_occurrences]).find_by_sql("select work_count, client_id, email, first_name, last_name, user_id, udp_reachable, in_timeout, in_session, scoring_failures, scoring_failures_offset, scoring_timeout_occurrences FROM get_work_summary(INTERVAL '#{staleness_hours} hours')" ) + r = GetWork.select([:work_count, :client_id, :email, :first_name, :last_name, :user_id, :udp_reachable, :in_timeout, :in_session, :scoring_failures, :scoring_failures_offset, :scoring_timeout_occurrences, :is_network_testing]).find_by_sql("select work_count, client_id, email, first_name, last_name, user_id, udp_reachable, in_timeout, in_session, scoring_failures, scoring_failures_offset, scoring_timeout_occurrences, is_network_testing FROM get_work_summary(INTERVAL '#{staleness_hours} hours')" ) end end end diff --git a/ruby/lib/jam_ruby/models/music_session.rb b/ruby/lib/jam_ruby/models/music_session.rb index 67e3eebe2..0095eb652 100644 --- a/ruby/lib/jam_ruby/models/music_session.rb +++ b/ruby/lib/jam_ruby/models/music_session.rb @@ -74,6 +74,7 @@ module JamRuby def add_to_feed feed = Feed.new feed.music_session = self + feed.active = true end @@ -604,6 +605,12 @@ module JamRuby hist.end_history if hist + feed = Feed.find_by_music_session_id(session_id) + unless feed.nil? + feed.active = false + feed.save + end + Notification.send_session_ended(session_id) end diff --git a/ruby/lib/jam_ruby/models/notification.rb b/ruby/lib/jam_ruby/models/notification.rb index 217205337..777f4853c 100644 --- a/ruby/lib/jam_ruby/models/notification.rb +++ b/ruby/lib/jam_ruby/models/notification.rb @@ -930,14 +930,13 @@ module JamRuby rsvp_requests = RsvpRequest.index(music_session) target_users = send_to_cancelled ? rsvp_requests.map { |r| r.user } : rsvp_requests.where(:canceled => false).map { |r| r.user } target_users = target_users.concat([music_session.creator]) + source_user = creator pending_invites = music_session.pending_invitations # remove the creator from the array target_users = target_users.concat(pending_invites).uniq - [creator] target_users.each do |target_user| - source_user = creator - notification = Notification.new notification.description = NotificationTypes::SCHEDULED_SESSION_COMMENT notification.source_user_id = source_user.id @@ -964,7 +963,7 @@ module JamRuby end begin - UserMailer.scheduled_session_comment(target_user, notification_msg, comment, music_session).deliver + UserMailer.scheduled_session_comment(target_user, source_user, notification_msg, comment, music_session).deliver rescue => e @@log.error("Unable to send SCHEDULED_SESSION_COMMENT email to user #{target_user.email} #{e}") end diff --git a/ruby/lib/jam_ruby/models/recorded_video.rb b/ruby/lib/jam_ruby/models/recorded_video.rb new file mode 100644 index 000000000..63837c53a --- /dev/null +++ b/ruby/lib/jam_ruby/models/recorded_video.rb @@ -0,0 +1,18 @@ +module JamRuby + # Video analog to JamRuby::RecordedTrack + class RecordedVideo < ActiveRecord::Base + belongs_to :user, :class_name => "JamRuby::User", :inverse_of => :recorded_videos + belongs_to :recording, :class_name => "JamRuby::Recording", :inverse_of => :recorded_videos + + validates :client_video_source_id, :presence => true + + def self.create_from_video_source(video_source, recording) + recorded_video_source = self.new + recorded_video_source.recording = recording + recorded_video_source.client_video_source_id = video_source.id + recorded_video_source.user = video_source.connection.user + recorded_video_source.save + recorded_video_source + end + end +end diff --git a/ruby/lib/jam_ruby/models/recording.rb b/ruby/lib/jam_ruby/models/recording.rb index edd2a4d1a..4368c3234 100644 --- a/ruby/lib/jam_ruby/models/recording.rb +++ b/ruby/lib/jam_ruby/models/recording.rb @@ -9,6 +9,7 @@ module JamRuby has_many :claimed_recordings, :class_name => "JamRuby::ClaimedRecording", :inverse_of => :recording, :foreign_key => 'recording_id', :dependent => :destroy has_many :mixes, :class_name => "JamRuby::Mix", :inverse_of => :recording, :foreign_key => 'recording_id', :dependent => :destroy has_many :recorded_tracks, :class_name => "JamRuby::RecordedTrack", :foreign_key => :recording_id, :dependent => :destroy + has_many :recorded_videos, :class_name => "JamRuby::RecordedVideo", :foreign_key => :recording_id, :dependent => :destroy has_many :comments, :class_name => "JamRuby::RecordingComment", :foreign_key => "recording_id", :dependent => :destroy has_many :likes, :class_name => "JamRuby::RecordingLiker", :foreign_key => "recording_id", :dependent => :destroy has_many :plays, :class_name => "JamRuby::PlayablePlay", :as => :playable, :dependent => :destroy @@ -152,6 +153,10 @@ module JamRuby connection.tracks.each do |track| recording.recorded_tracks << RecordedTrack.create_from_track(track, recording) end + + connection.video_sources.each do |video| + recording.recorded_videos << RecordedVideo.create_from_video_source(video, recording) + end end end end @@ -298,22 +303,82 @@ module JamRuby since = 0 unless since || since == '' # guard against nil uploads = [] - RecordedTrack - .joins(:recording) - .where(:user_id => user.id) - .where(:fully_uploaded => false) - .where('recorded_tracks.id > ?', since) - .where("upload_failures <= #{APP_CONFIG.max_track_upload_failures}") - .where("duration IS NOT NULL") - .where('all_discarded = false') - .order('recorded_tracks.id') - .limit(limit).each do |recorded_track| - uploads.push({ - :type => "recorded_track", - :client_track_id => recorded_track.client_track_id, - :recording_id => recorded_track.recording_id, - :next => recorded_track.id - }) + + # Uploads now include videos in addition to the tracks. + # This is accomplished using a SQL union via arel, as follows: + + # Select fields from track. Note the reorder, which removes + # the default scope sort as it b0rks the union. Also note the + # alias so that we can differentiate tracks and videos when + # processing the results: + track_arel = RecordedTrack.select([ + :id, + :recording_id, + :url, + :fully_uploaded, + :upload_failures, + :client_track_id, + Arel::Nodes::As.new('track', Arel.sql('item_type')) + ]).reorder("") + + # Select fields for video. Note that it must include + # the same number of fields as the track in order for + # the union to work: + vid_arel = RecordedVideo.select([ + :id, + :recording_id, + :url, + :fully_uploaded, + :upload_failures, + :client_video_source_id, + Arel::Nodes::As.new('video', Arel.sql('item_type')) + ]).reorder("") + + # Glue them together: + union = track_arel.union(vid_arel) + + # Create a table alias from the union so we can get back to arel: + utable = RecordedTrack.arel_table.create_table_alias(union, :recorded_items) + arel = track_arel.from(utable) + arel = arel.select([ + :id, + :recording_id, + :url, + :fully_uploaded, + :upload_failures, + :client_track_id, + :item_type + ]) + + # Further joining and criteria for the unioned object: + arel = arel.joins("INNER JOIN (SELECT id as rec_id, all_discarded, duration FROM recordings) recs ON rec_id=recorded_items.recording_id") \ + .where('recorded_items.fully_uploaded =?', false) \ + .where('recorded_items.id > ?', since) \ + .where("upload_failures <= #{APP_CONFIG.max_track_upload_failures}") \ + .where("duration IS NOT NULL") \ + .where('all_discarded = false') \ + .order('recorded_items.id') \ + .limit(limit) + + # Load into array: + arel.each do |recorded_item| + if(recorded_item.item_type=='video') + # A video: + uploads << ({ + :type => "recorded_video", + :client_video_source_id => recorded_item.client_track_id, + :recording_id => recorded_item.recording_id, + :next => recorded_item.id + }) + else + # A track: + uploads << ({ + :type => "recorded_track", + :client_track_id => recorded_item.client_track_id, + :recording_id => recorded_item.recording_id, + :next => recorded_item.id + }) + end end next_value = uploads.length > 0 ? uploads[-1][:next].to_s : nil @@ -321,7 +386,6 @@ module JamRuby next_value = since # echo back to the client the same value they passed in, if there are no results end - { "uploads" => uploads, "next" => next_value.to_s diff --git a/ruby/lib/jam_ruby/models/rsvp_request.rb b/ruby/lib/jam_ruby/models/rsvp_request.rb index 689aad72e..f30851efe 100644 --- a/ruby/lib/jam_ruby/models/rsvp_request.rb +++ b/ruby/lib/jam_ruby/models/rsvp_request.rb @@ -182,7 +182,7 @@ module JamRuby end if rsvp_slot.chosen && r[:accept] - raise StateError, "The #{rsvp_slot.instrument_id} slot has already been approved for another user." + raise StateError, "All RSVP slots for the #{rsvp_slot.instrument_id} have been already approved." end if r[:accept] diff --git a/ruby/lib/jam_ruby/models/user.rb b/ruby/lib/jam_ruby/models/user.rb index 9cdc68a1c..b33a1d2c0 100644 --- a/ruby/lib/jam_ruby/models/user.rb +++ b/ruby/lib/jam_ruby/models/user.rb @@ -103,6 +103,7 @@ module JamRuby # saved tracks has_many :recorded_tracks, :foreign_key => "user_id", :class_name => "JamRuby::RecordedTrack", :inverse_of => :user + has_many :recorded_videos, :foreign_key => "user_id", :class_name => "JamRuby::RecordedVideo", :inverse_of => :user # invited users has_many :invited_users, :foreign_key => "sender_id", :class_name => "JamRuby::InvitedUser" @@ -1271,8 +1272,7 @@ module JamRuby if audio_latency > 2 # updating the connection is best effort if connection - connection.last_jam_audio_latency = audio_latency - connection.save + Connection.where(:id => connection.id).update_all(:last_jam_audio_latency => audio_latency) end self.last_jam_audio_latency = audio_latency diff --git a/ruby/lib/jam_ruby/models/video_source.rb b/ruby/lib/jam_ruby/models/video_source.rb new file mode 100644 index 000000000..4517bf4bf --- /dev/null +++ b/ruby/lib/jam_ruby/models/video_source.rb @@ -0,0 +1,10 @@ +module JamRuby + # Video analog to JamRuby::Track + class VideoSource < ActiveRecord::Base + self.table_name = "video_sources" + self.primary_key = 'id' + default_scope order('created_at ASC') + belongs_to :connection, :class_name => "JamRuby::Connection", :inverse_of => :video_sources, :foreign_key => 'connection_id' + validates :connection, presence: true + end +end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/resque/scheduled/active_music_session_cleaner.rb b/ruby/lib/jam_ruby/resque/scheduled/active_music_session_cleaner.rb index 18722217d..e700a0427 100644 --- a/ruby/lib/jam_ruby/resque/scheduled/active_music_session_cleaner.rb +++ b/ruby/lib/jam_ruby/resque/scheduled/active_music_session_cleaner.rb @@ -38,6 +38,7 @@ module JamRuby stale_sessions.each do |s| if s.connections.count == 0 + s.before_destroy s.delete end end diff --git a/ruby/spec/factories.rb b/ruby/spec/factories.rb index 249cd8247..2d030b752 100644 --- a/ruby/spec/factories.rb +++ b/ruby/spec/factories.rb @@ -231,6 +231,10 @@ FactoryGirl.define do sequence(:client_track_id) { |n| "client_track_id#{n}"} end + factory :video_source, :class => JamRuby::VideoSource do + client_video_source_id "test_source_id" + end + factory :recorded_track, :class => JamRuby::RecordedTrack do instrument JamRuby::Instrument.first sound 'stereo' @@ -244,6 +248,12 @@ FactoryGirl.define do association :recording, factory: :recording end + factory :recorded_video, :class => JamRuby::RecordedVideo do + sequence(:client_id) { |n| "client_id-#{n}"} + sequence(:track_id) { |n| "track_id-#{n}"} + sequence(:client_track_id) { |n| "client_track_id-#{n}"} + end + factory :instrument, :class => JamRuby::Instrument do description { |n| "Instrument #{n}" } end diff --git a/ruby/spec/jam_ruby/models/active_music_session_spec.rb b/ruby/spec/jam_ruby/models/active_music_session_spec.rb index 28ac362cc..ac0288f23 100644 --- a/ruby/spec/jam_ruby/models/active_music_session_spec.rb +++ b/ruby/spec/jam_ruby/models/active_music_session_spec.rb @@ -711,5 +711,38 @@ describe ActiveMusicSession do @music_session.get_connection_ids(exclude_client_id: @connection2.client_id, as_musician: true).should == [@connection1.client_id] end end + + describe "join_the_session" do + let(:creator_1) { FactoryGirl.create(:user, last_jam_locidispid: 4, last_jam_audio_latency: 8) } + let(:creator_conn_1) { FactoryGirl.create(:connection, user: creator_1, ip_address: '4.4.4.4', locidispid: 4, addr:4) } + let!(:music_session_1) { FactoryGirl.create(:active_music_session, :creator => creator_1, genre: Genre.find('african'), language: 'eng', description: "Bunny Jumps" ) } + let(:tracks) { [{'sound' => 'mono', 'client_track_id' => 'abc', 'instrument_id' => 'piano'}] } + let(:videos) { [{'client_video_source_id' => 'abc'}] } + + it "joins the session with no video" do + creator_conn_1.join_the_session(music_session_1.music_session, true, tracks, creator_1, 10) + creator_conn_1.errors.any?.should be_false + + music_sessions = ActiveMusicSession.index(creator_1) + music_sessions.should_not be_nil + music_sessions.length.should == 1 + music_sessions[0].connections.should have(1).items + music_sessions[0].connections.should have(1).items + music_sessions[0].connections[0].tracks.should have(1).items + music_sessions[0].connections[0].video_sources.should have(0).items + end + + it "joins the session with video" do + creator_conn_1.join_the_session(music_session_1.music_session, true, tracks, creator_1, 10, videos) + creator_conn_1.errors.any?.should be_false + music_sessions = ActiveMusicSession.index(creator_1) + music_sessions.should_not be_nil + music_sessions.length.should == 1 + creator_conn_1.video_sources.should have(1).items + music_sessions[0].connections.should have(1).items + music_sessions[0].connections[0].video_sources.should have(1).items + music_sessions[0].connections[0].tracks.should have(1).items + end + end end diff --git a/ruby/spec/jam_ruby/models/connection_spec.rb b/ruby/spec/jam_ruby/models/connection_spec.rb index c391c7f29..dc7d8cdba 100644 --- a/ruby/spec/jam_ruby/models/connection_spec.rb +++ b/ruby/spec/jam_ruby/models/connection_spec.rb @@ -9,6 +9,8 @@ describe JamRuby::Connection do :ip_address => "1.1.1.1", :client_id => "1") } + let(:tracks) { [{'sound' => 'mono', 'client_track_id' => 'abc', 'instrument_id' => 'piano'}] } + it 'starts in the correct state' do connection = FactoryGirl.create(:connection, :user => user, @@ -100,4 +102,5 @@ describe JamRuby::Connection do conn.locidispid.should == 0 end end + end diff --git a/ruby/spec/jam_ruby/models/feed_spec.rb b/ruby/spec/jam_ruby/models/feed_spec.rb index dc790f13c..f63d03116 100644 --- a/ruby/spec/jam_ruby/models/feed_spec.rb +++ b/ruby/spec/jam_ruby/models/feed_spec.rb @@ -54,16 +54,16 @@ describe Feed do end describe "sorting" do - it "sorts by index (date) DESC" do + it "sorts by active flag / index (date) DESC" do claimed_recording = FactoryGirl.create(:claimed_recording) feeds, start = Feed.index(user1) feeds.length.should == 2 - feeds[0].recording.should == claimed_recording.recording - feeds[1].music_session.should == claimed_recording.recording.music_session.music_session + feeds[1].recording.should == claimed_recording.recording + feeds[0].music_session.should == claimed_recording.recording.music_session.music_session end - it "sort by plays DESC" do + it "sort by active flag / plays DESC" do claimed_recording1 = FactoryGirl.create(:claimed_recording) claimed_recording2 = FactoryGirl.create(:claimed_recording) @@ -77,8 +77,8 @@ describe Feed do feeds, start = Feed.index(user1, :sort => 'plays') feeds.length.should == 4 - feeds[0].recording.should == claimed_recording2.recording - feeds[1].recording.should == claimed_recording1.recording + feeds[2].recording.should == claimed_recording2.recording + feeds[3].recording.should == claimed_recording1.recording FactoryGirl.create(:playable_play, playable: claimed_recording1.recording.music_session.music_session, user: user1) FactoryGirl.create(:playable_play, playable: claimed_recording1.recording.music_session.music_session, user: user2) @@ -88,11 +88,11 @@ describe Feed do feeds, start = Feed.index(user1, :sort => 'plays') feeds.length.should == 4 feeds[0].music_session.should == claimed_recording1.recording.music_session.music_session - feeds[1].recording.should == claimed_recording2.recording - feeds[2].recording.should == claimed_recording1.recording + feeds[2].recording.should == claimed_recording2.recording + feeds[3].recording.should == claimed_recording1.recording end - it "sort by likes DESC" do + it "sort by active flag / likes DESC" do claimed_recording1 = FactoryGirl.create(:claimed_recording) claimed_recording2 = FactoryGirl.create(:claimed_recording) @@ -106,6 +106,7 @@ describe Feed do feeds, start = Feed.index(user1, :sort => 'likes') feeds.length.should == 4 + feeds = feeds.where("feeds.music_session_id is null") feeds[0].recording.should == claimed_recording2.recording feeds[1].recording.should == claimed_recording1.recording @@ -116,8 +117,8 @@ describe Feed do feeds, start = Feed.index(user1, :sort => 'likes') feeds.length.should == 4 feeds[0].music_session.should == claimed_recording1.recording.music_session.music_session - feeds[1].recording.should == claimed_recording2.recording - feeds[2].recording.should == claimed_recording1.recording + feeds[2].recording.should == claimed_recording2.recording + feeds[3].recording.should == claimed_recording1.recording end end @@ -195,6 +196,9 @@ describe Feed do it "supports date pagination" do claimed_recording = FactoryGirl.create(:claimed_recording) + ams = ActiveMusicSession.find(claimed_recording.recording.music_session.music_session.id) + ams.before_destroy + options = {limit: 1} feeds, start = Feed.index(user1, options) feeds.length.should == 1 diff --git a/ruby/spec/jam_ruby/models/get_work_spec.rb b/ruby/spec/jam_ruby/models/get_work_spec.rb index b47b7041d..e05c6cb95 100644 --- a/ruby/spec/jam_ruby/models/get_work_spec.rb +++ b/ruby/spec/jam_ruby/models/get_work_spec.rb @@ -176,6 +176,16 @@ describe GetWork do GetWork.get_work_list(other_connection2).should == [my_connection.client_id] end + it "excludes network testing clients" do + my_connection = FactoryGirl.create(:connection, locidispid: austin_geo[:locidispid], addr: 1) + other_connection = FactoryGirl.create(:connection, locidispid: dallas_geo[:locidispid], addr: 2, is_network_testing: true) + other_connection2 = FactoryGirl.create(:connection, locidispid: houston_geoip[:locidispid], addr: 3) + + GetWork.get_work_list(my_connection).should == [other_connection2.client_id] + GetWork.get_work_list(other_connection).should == [] + GetWork.get_work_list(other_connection2).should == [my_connection.client_id] + end + it "excludes scoring_timeout clients (1)" do my_connection = FactoryGirl.create(:connection, locidispid: austin_geo[:locidispid], addr: 1) other_connection = FactoryGirl.create(:connection, locidispid: dallas_geo[:locidispid], addr: 2, scoring_timeout: 1.days.from_now) diff --git a/ruby/spec/jam_ruby/models/recorded_video_spec.rb b/ruby/spec/jam_ruby/models/recorded_video_spec.rb new file mode 100644 index 000000000..e8043f24a --- /dev/null +++ b/ruby/spec/jam_ruby/models/recorded_video_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' +require 'rest-client' + +describe RecordedVideo do + include UsesTempFiles + let (:user) {FactoryGirl.create(:user)} + let (:connection) {FactoryGirl.create(:connection, :user => user)} + let (:music_session){FactoryGirl.create(:active_music_session, :creator => user, :musician_access => true)} + let (:recording) {FactoryGirl.create(:recording, :music_session => music_session, :owner => user)} + let (:video_source) {FactoryGirl.create(:video_source, :connection => connection)} + + it "should create from a video source" do + recorded_video_source = RecordedVideo.create_from_video_source(video_source, recording) + recorded_video_source.should_not be_nil + recorded_video_source.user.id.should == video_source.connection.user.id + recorded_video_source.fully_uploaded.should == false + recorded_video_source.client_video_source_id.should == video_source.id + end + +end + diff --git a/ruby/spec/jam_ruby/models/recording_spec.rb b/ruby/spec/jam_ruby/models/recording_spec.rb index 0bc9702e0..d7a5cf709 100644 --- a/ruby/spec/jam_ruby/models/recording_spec.rb +++ b/ruby/spec/jam_ruby/models/recording_spec.rb @@ -7,10 +7,9 @@ describe Recording do @instrument = FactoryGirl.create(:instrument, :description => 'a great instrument') @music_session = FactoryGirl.create(:active_music_session, :creator => @user, :musician_access => true) @connection = FactoryGirl.create(:connection, :user => @user, :music_session => @music_session) - @track = FactoryGirl.create(:track, :connection => @connection, :instrument => @instrument) + @track = FactoryGirl.create(:track, :connection => @connection, :instrument => @instrument) end - - + it "should allow finding of recorded tracks" do user2 = FactoryGirl.create(:user) connection2 = FactoryGirl.create(:connection, :user => user2, :music_session => @music_session) @@ -39,7 +38,7 @@ describe Recording do @recorded_tracks.length.should == 1 @recorded_tracks.first.instrument_id == @track.instrument_id @recorded_tracks.first.user_id == @track.connection.user_id - end + end it "should not start a recording if the session is already being recorded" do Recording.start(@music_session, @user).errors.any?.should be_false @@ -286,6 +285,64 @@ describe Recording do @recording2.errors.any?.should be_false end end + + + it "set video from source" do + @video_source = FactoryGirl.create(:video_source, :connection => @connection) + + @music_session.is_recording?.should be_false + @recording = Recording.start(@music_session, @user) + @music_session.reload + @music_session.recordings[0].should == @recording + @recording.owner_id.should == @user.id + + @recorded_videos = RecordedVideo.where(:recording_id => @recording.id) + @recorded_videos.should have(1).items + @recorded_videos[0].client_video_source_id.should eq(@video_source.id) + end + + it "should include video when listing uploads" do + @video_source = FactoryGirl.create(:video_source, :connection => @connection) + @recording = Recording.start(@music_session, @user) + @recording.stop + @recording.reload + @genre = FactoryGirl.create(:genre) + @recording.claim(@user, "Recording", "Recording Description", @genre, true) + + # We should have 2 items; a track and a video: + uploads = Recording.list_uploads(@user) + uploads["uploads"].should have(2).items + uploads["uploads"][0][:type].should eq("recorded_track") + uploads["uploads"][1][:type].should eq("recorded_video") + uploads["uploads"][0].should include(:client_track_id) + uploads["uploads"][1].should include(:client_video_source_id) + + # Next page should have nothing: + uploads = Recording.list_uploads(@user, 10, uploads["next"]) + uploads["uploads"].should have(0).items + end + + it "should paginate with video" do + @video_source = FactoryGirl.create(:video_source, :connection => @connection) + @recording = Recording.start(@music_session, @user) + @recording.stop + @recording.reload + @genre = FactoryGirl.create(:genre) + @recording.claim(@user, "Recording", "Recording Description", @genre, true) + + # Limit to 1, so we can test pagination: + uploads = Recording.list_uploads(@user, 1) + uploads["uploads"].should have(1).items # First page + + # Second page: + uploads = Recording.list_uploads(@user, 1, uploads["next"]) + uploads["uploads"].should have(1).items + + # Last page (should be empty): + uploads = Recording.list_uploads(@user, 10, uploads["next"]) + uploads["uploads"].should have(0).items + end + end diff --git a/ruby/spec/jam_ruby/models/video_source_spec.rb b/ruby/spec/jam_ruby/models/video_source_spec.rb new file mode 100644 index 000000000..166443c22 --- /dev/null +++ b/ruby/spec/jam_ruby/models/video_source_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +describe VideoSource do + + let (:user) {FactoryGirl.create(:user) } + let (:music_session) { FactoryGirl.create(:active_music_session, :creator => user)} + let (:connection) { FactoryGirl.create(:connection, :user => user, :music_session => music_session) } + let (:msuh) {FactoryGirl.create(:music_session_user_history, :history => music_session.music_session, :user => user, :client_id => connection.client_id) } + + + before(:each) do + msuh.touch + end + + describe "simple create" do + it "create a video source" do + video_source = FactoryGirl.create(:video_source, :connection => connection) + video_source.should_not be_nil + end + end +end \ No newline at end of file diff --git a/web/Gemfile b/web/Gemfile index ad080e413..c97941bd1 100644 --- a/web/Gemfile +++ b/web/Gemfile @@ -18,7 +18,7 @@ else gem 'jam_websockets', "0.1.#{ENV["BUILD_NUMBER"]}" ENV['NOKOGIRI_USE_SYSTEM_LIBRARIES'] ||= "true" end -gem 'oj' +gem 'oj', '2.10.2' gem 'builder' gem 'rails', '~>3.2.11' gem 'railties', '~>3.2.11' @@ -55,7 +55,7 @@ gem 'carrierwave_direct' gem 'fog' gem 'haml-rails' gem 'unf' #optional fog dependency -gem 'devise', '>= 1.1.2' +gem 'devise', '3.3.0' #3.4.0 causes uninitialized constant ActionController::Metal (NameError) gem 'postgres-copy' #group :libv8 do # gem 'libv8', "~> 3.11.8" diff --git a/web/app/assets/javascripts/accounts_session_detail.js b/web/app/assets/javascripts/accounts_session_detail.js index c43a8b537..a93d0b1c7 100644 --- a/web/app/assets/javascripts/accounts_session_detail.js +++ b/web/app/assets/javascripts/accounts_session_detail.js @@ -101,7 +101,18 @@ rest.updateRsvpRequest(rsvpId, params) .done(refreshSessionDetail) - .fail(app.ajaxError); + .fail(function(jqXHR, textStatus, errorMessage) { + if (jqXHR.status === 400) { + app.notify( + { + title: "Unable to Approve RSVP", + text: jqXHR.responseJSON.message + }); + } + else { + app.ajaxError(jqXHR, textStatus, errorMessage); + } + }); } function declineRsvpRequest(e) { @@ -132,6 +143,9 @@ }); context.JK.bindHoverEvents(); + // context.JK.bindInstrumentHover($('#pendingRSVPs')); + // context.JK.bindInstrumentHover($('#session-rsvps')); + // context.JK.bindInstrumentHover($('#still-needed')); } function loadSessionData() { @@ -243,7 +257,7 @@ $.each(pending_rsvp_request.instrument_list, function (index, instrument) { var instrumentId = instrument == null ? null : instrument.id; var inst = context.JK.getInstrumentIcon24(instrumentId); - instrumentLogoHtml += ' '; + instrumentLogoHtml += ' '; }) } @@ -281,7 +295,7 @@ $.each(approved_rsvp.instrument_list, function(index, instrument) { var instrumentId = instrument == null ? null : instrument.id; var inst = context.JK.getInstrumentIcon24(instrumentId); - instrumentLogoHtml += ' '; + instrumentLogoHtml += ' '; }); } @@ -312,7 +326,7 @@ rsvpHtml = context._.template( $("#template-account-session-rsvp").html(), - {id: approved_rsvp.id, user_id: approved_rsvp.user_id, avatar_url: avatar_url, + {id: approved_rsvp.id, avatar_url: avatar_url, user_name: approved_rsvp.name, instruments: instrumentLogoHtml, latency: latencyHtml, is_owner: sessionData.isOwner, request_id: request_id}, {variable: 'data'} @@ -346,7 +360,7 @@ resultHtml += context._.template( $("#template-account-invited").html(), - {avatar_url: avatar_url, user_id: invitation.reciever_id}, + {avatar_url: avatar_url, user_id: invitation.receiver_id}, {variable: 'data'} ); }); diff --git a/web/app/assets/javascripts/dialog/networkTestDialog.js b/web/app/assets/javascripts/dialog/networkTestDialog.js index 5d64ceb64..da0d7130e 100644 --- a/web/app/assets/javascripts/dialog/networkTestDialog.js +++ b/web/app/assets/javascripts/dialog/networkTestDialog.js @@ -54,13 +54,14 @@ } function beforeShow() { + networkTest.haltScoring(); if(!networkTest.isScoring()) { networkTest.reset(); } } function afterHide() { - + networkTest.resumeScoring(); } function initialize() { diff --git a/web/app/assets/javascripts/everywhere/everywhere.js b/web/app/assets/javascripts/everywhere/everywhere.js index e90049966..71fd1169a 100644 --- a/web/app/assets/javascripts/everywhere/everywhere.js +++ b/web/app/assets/javascripts/everywhere/everywhere.js @@ -140,6 +140,9 @@ } function updateScoringIntervals() { + // make sure latency testing is still going on, in case a refresh occurred during network test + context.jamClient.SetLatencyTestBlocked(false) + // set scoring intervals if(context.jamClient.SetScoreWorkTimingInterval){ var success = context.jamClient.SetScoreWorkTimingInterval( diff --git a/web/app/assets/javascripts/fakeJamClient.js b/web/app/assets/javascripts/fakeJamClient.js index f6af7789f..393501e1b 100644 --- a/web/app/assets/javascripts/fakeJamClient.js +++ b/web/app/assets/javascripts/fakeJamClient.js @@ -362,6 +362,18 @@ return 8; } + function SetLatencyTestBlocked(blocked) { + + } + + function isLatencyTestBlocked() { + return false; + } + + function GetLastLatencyTestTimes() { + return { initiated: 10000, requested: 10000} + } + function GetASIODevices() { var response =[{"device_id":0,"device_name":"Realtek High Definition Audio","device_type": 0,"interfaces":[{"interface_id":0,"interface_name":"Realtek HDA SPDIF Out","pins":[{"is_input":false,"pin_id":0,"pin_name":"PC Speaker"}]},{"interface_id":1,"interface_name":"Realtek HD Audio rear output","pins":[{"is_input":false,"pin_id":0,"pin_name":"PC Speaker"}]},{"interface_id":2,"interface_name":"Realtek HD Audio Mic input","pins":[{"is_input":true,"pin_id":0,"pin_name":"Recording Control"}]},{"interface_id":3,"interface_name":"Realtek HD Audio Line input","pins":[{"is_input":true,"pin_id":0,"pin_name":"Recording Control"}]},{"interface_id":4,"interface_name":"Realtek HD Digital input","pins":[{"is_input":true,"pin_id":0,"pin_name":"Capture"}]},{"interface_id":5,"interface_name":"Realtek HD Audio Stereo input","pins":[{"is_input":true,"pin_id":0,"pin_name":"Recording Control"}]}],"wavert_supported":false},{"device_id":1,"device_name":"M-Audio FW Audiophile","device_type": 1,"interfaces":[{"interface_id":0,"interface_name":"FW AP Multi","pins":[{"is_input":false,"pin_id":0,"pin_name":"Output"},{"is_input":true,"pin_id":1,"pin_name":"Input"}]},{"interface_id":1,"interface_name":"FW AP 1/2","pins":[{"is_input":false,"pin_id":0,"pin_name":"Output"},{"is_input":true,"pin_id":1,"pin_name":"Input"}]},{"interface_id":2,"interface_name":"FW AP SPDIF","pins":[{"is_input":false,"pin_id":0,"pin_name":"Output"},{"is_input":true,"pin_id":1,"pin_name":"Input"}]},{"interface_id":3,"interface_name":"FW AP 3/4","pins":[{"is_input":false,"pin_id":0,"pin_name":"Output"}]}],"wavert_supported":false},{"device_id":2,"device_name":"Virtual Audio Cable","device_type": 2,"interfaces":[{"interface_id":0,"interface_name":"Virtual Cable 2","pins":[{"is_input":true,"pin_id":0,"pin_name":"Capture"},{"is_input":false,"pin_id":1,"pin_name":"Output"}]},{"interface_id":1,"interface_name":"Virtual Cable 1","pins":[{"is_input":true,"pin_id":0,"pin_name":"Capture"},{"is_input":false,"pin_id":1,"pin_name":"Output"}]}],"wavert_supported":false},{"device_id":3,"device_name":"WebCamDV WDM Audio Capture","device_type": 3,"interfaces":[{"interface_id":0,"interface_name":"WebCamDV Audio","pins":[{"is_input":true,"pin_id":0,"pin_name":"Recording Control"},{"is_input":false,"pin_id":1,"pin_name":"Volume Control"}]}],"wavert_supported":false}]; return response; @@ -823,6 +835,9 @@ this.IsMyNetworkWireless = IsMyNetworkWireless; this.SetNetworkTestScore = SetNetworkTestScore; this.GetNetworkTestScore = GetNetworkTestScore; + this.SetLatencyTestBlocked = SetLatencyTestBlocked; + this.isLatencyTestBlocked = isLatencyTestBlocked; + this.GetLastLatencyTestTimes = GetLastLatencyTestTimes; this.RegisterQuitCallback = RegisterQuitCallback; this.LeaveSessionAndMinimize = LeaveSessionAndMinimize; this.GetAutoStart = GetAutoStart; diff --git a/web/app/assets/javascripts/findSession.js b/web/app/assets/javascripts/findSession.js index df765fb3a..98db4761f 100644 --- a/web/app/assets/javascripts/findSession.js +++ b/web/app/assets/javascripts/findSession.js @@ -85,6 +85,9 @@ context.JK.bindHoverEvents(); $ssSpinner.hide(); }); + + // context.JK.bindInstrumentHover($(CATEGORY.ACTIVE.id)); + // context.JK.bindInstrumentHover($(CATEGORY.SCHEDULED.id)); } /***************** ACTIVE SESSIONS *****************/ diff --git a/web/app/assets/javascripts/jam_rest.js b/web/app/assets/javascripts/jam_rest.js index 3c1e95027..ff031af99 100644 --- a/web/app/assets/javascripts/jam_rest.js +++ b/web/app/assets/javascripts/jam_rest.js @@ -503,6 +503,19 @@ }); } + function updateNetworkTesting(options) { + var id = getId(options); + + return $.ajax({ + type: "POST", + dataType: "json", + contentType: 'application/json', + url: "/api/users/" + id + "/is_network_testing", + data: JSON.stringify(options), + processData: false + }); + } + function updateAvatar(options) { var id = getId(options); @@ -1222,6 +1235,7 @@ this.getInstruments = getInstruments; this.getGenres = getGenres; this.updateUdpReachable = updateUdpReachable; + this.updateNetworkTesting = updateNetworkTesting; this.updateAvatar = updateAvatar; this.deleteAvatar = deleteAvatar; this.getFilepickerPolicy = getFilepickerPolicy; diff --git a/web/app/assets/javascripts/jamkazam.js b/web/app/assets/javascripts/jamkazam.js index 08d79d34a..d8fb6e4aa 100644 --- a/web/app/assets/javascripts/jamkazam.js +++ b/web/app/assets/javascripts/jamkazam.js @@ -123,11 +123,11 @@ */ function ajaxError(jqXHR, textStatus, errorMessage) { - if (jqXHR.status == 404) { + if (jqXHR.status === 404) { logger.error("Unexpected ajax error: " + textStatus + ", msg:" + errorMessage); app.notify({title: "Oops!", text: "What you were looking for is gone now."}); } - else if (jqXHR.status = 422) { + else if (jqXHR.status === 422) { logger.error("Unexpected ajax error: " + textStatus + ", msg: " + errorMessage + ", response: " + jqXHR.responseText); // present a nicer message try { diff --git a/web/app/assets/javascripts/networkTestHelper.js b/web/app/assets/javascripts/networkTestHelper.js index 0e6d3add8..601ecd860 100644 --- a/web/app/assets/javascripts/networkTestHelper.js +++ b/web/app/assets/javascripts/networkTestHelper.js @@ -167,6 +167,35 @@ return lastNetworkFailure; } + function haltScoring() { + context.jamClient.SetLatencyTestBlocked(true) + rest.updateNetworkTesting({client_id: app.clientId, is_network_testing: true}) + .fail(function(jqXHR) { + + if(jqXHR.status == 404) { + // assume connection is missing + app.notifyAlert("Not Connected", "You must be connected to the server to run the network test.") + } + else { + app.notifyServerError(jqXHR, "Unable to tell server that we are beginning the network test") + } + }) + } + + function resumeScoring() { + context.jamClient.SetLatencyTestBlocked(false) + rest.updateNetworkTesting({client_id: app.clientId, is_network_testing: false}) + .fail(function(jqXHR) { + if(jqXHR.status == 404) { + // assume connection is missing + // do nothing in this case + } + else { + app.notifyServerError(jqXHR, "Unable to tell server that we are ending the network test") + } + }) + } + function storeLastNetworkFailure(reason, data) { if (!trackedPass) { lastNetworkFailure = {reason: reason, data: data}; @@ -471,63 +500,125 @@ } } + function pauseForRecentScoresTime() { + var lastScoreTimes = context.jamClient.GetLastLatencyTestTimes() + + console.log(lastScoreTimes) + + return 0; + + var noPause = 0; + var longAgo = 1000000; + var initiated = lastScoreTimes.initiatied; + var requested = lastScoreTimes.requested; + + if(initiated === null || initiated === undefined) { + logger.warn("lastScoreTimes.initiated is not set"); + initiated = longAgo; + } + if(requested === null || requested === undefined) { + logger.warn("lastScoreTimes.requested is not set"); + requested = longAgo; + } + + if(initiated == 0) { + logger.debug("lastScoreTimes.initiated is zero"); + initiated = longAgo; + } + if(requested == 0) { + logger.debug("lastScoreTimes.requested is zero"); + requested = longAgo; + } + + if(initiated < 0) { + logger.debug("lastScoreTimes.initiated is less than zero"); + initiated = longAgo; + } + if(requested < 0) { + logger.debug("lastScoreTimes.requested is less than zero"); + requested = longAgo; + } + + var mostRecentValue = initiated < requested ? initiated : requested; + + if(mostRecentValue > gon.globalftue_network_test_min_wait_since_last_score * 1000) { + return noPause; // our last score was past our min wait; so no delay necessary + } + else { + // pause for the remainder of the min wait threshold + var remainder = gon.globalftue_network_test_min_wait_since_last_score * 1000 - mostRecentValue; + + if(remainder > 1500) { + // we need to update the UI because this is a long time for a mystery pause + $startNetworkTestBtn.text('SHORT QUIET PERIOD...') + } + + return remainder; + } + } function prepareNetworkTest() { if (scoring) return false; - logger.info("starting network test"); - resetTestState(); - scoring = true; - $self.triggerHandler(NETWORK_TEST_START); - renderStartTest(); - rest.getLatencyTester() - .done(function (response) { - // ensure there are no tests ongoing - serverClientId = response.client_id; - testSummary.serverClientId = serverClientId; + setTimeout(function() { - logger.info("beginning network test against client_id: " + serverClientId); + logger.info("starting network test"); + resetTestState(); + scoring = true; + $self.triggerHandler(NETWORK_TEST_START); + renderStartTest(); + rest.getLatencyTester() + .done(function (response) { + // ensure there are no tests ongoing - primePump() - .done(function () { - postPumpRun(); - }) - .fail(function () { - logger.debug("unable to determine user's network type. primePump failed.") - context.JK.Banner.showAlert({ - title: 'Unable to Determine Network Type', - buttons: [ - {name: 'CANCEL', click: function () { - cancelTest(); - }}, - {name: 'RUN NETWORK TEST ANYWAY', click: function () { - attemptTestPass(); - ; - }} - ], - html: "

We are unable to determine if your computer is connected to your network using WiFi.

" + - "

We strongly advise against running the JamKazam application on a WiFi connection. " + - "We recommend using a wired Ethernet connection from your computer to your router. " + - "A WiFi connection is likely to cause significant issues in both latency and audio quality.

"}) - }); - }) - .fail(function (jqXHR) { - if (jqXHR.status == 404) { - // means there are no network testers available. - // we have to skip this part of the UI - testSummary.final = {reason: 'no_servers'} - } - else { - if (context.JK.isNetworkError(arguments)) { - testSummary.final = {reason: 'no_network'} + serverClientId = response.client_id; + + testSummary.serverClientId = serverClientId; + + logger.info("beginning network test against client_id: " + serverClientId); + + primePump() + .done(function () { + postPumpRun(); + }) + .fail(function () { + logger.debug("unable to determine user's network type. primePump failed.") + context.JK.Banner.showAlert({ + title: 'Unable to Determine Network Type', + buttons: [ + {name: 'CANCEL', click: function () { + cancelTest(); + }}, + {name: 'RUN NETWORK TEST ANYWAY', click: function () { + attemptTestPass(); + ; + }} + ], + html: "

We are unable to determine if your computer is connected to your network using WiFi.

" + + "

We strongly advise against running the JamKazam application on a WiFi connection. " + + "We recommend using a wired Ethernet connection from your computer to your router. " + + "A WiFi connection is likely to cause significant issues in both latency and audio quality.

"}) + }); + }) + .fail(function (jqXHR) { + if (jqXHR.status == 404) { + // means there are no network testers available. + // we have to skip this part of the UI + testSummary.final = {reason: 'no_servers'} } else { - testSummary.final = {reason: 'rest_api_error'} + if (context.JK.isNetworkError(arguments)) { + testSummary.final = {reason: 'no_network'} + } + else { + testSummary.final = {reason: 'rest_api_error'} + } } - } - testFinished(); - }) + testFinished(); + }) + }, pauseForRecentScoresTime()) + return false; } @@ -844,6 +935,8 @@ this.reset = reset; this.cancel = cancel; this.getLastNetworkFailure = getLastNetworkFailure; + this.haltScoring = haltScoring; + this.resumeScoring = resumeScoring; this.NETWORK_TEST_START = NETWORK_TEST_START; this.NETWORK_TEST_DONE = NETWORK_TEST_DONE; diff --git a/web/app/assets/javascripts/sessionList.js b/web/app/assets/javascripts/sessionList.js index 06d26efa6..d58218dbf 100644 --- a/web/app/assets/javascripts/sessionList.js +++ b/web/app/assets/javascripts/sessionList.js @@ -369,7 +369,7 @@ var track = participant.tracks[j]; logger.debug("Find:Finding instruments. Participant tracks:", participant.tracks); var inst = context.JK.getInstrumentIcon24(track.instrument_id); - instrumentLogoHtml += ' '; + instrumentLogoHtml += ' '; } var id = participant.user.id; @@ -400,7 +400,7 @@ for (j=0; j < user.instrument_list.length; j++) { var instrument = user.instrument_list[j]; var inst = context.JK.getInstrumentIcon24(instrument.id); - instrumentLogoHtml += ' '; + instrumentLogoHtml += ' '; } } diff --git a/web/app/assets/javascripts/utils.js b/web/app/assets/javascripts/utils.js index b83d901c7..2afdd2f53 100644 --- a/web/app/assets/javascripts/utils.js +++ b/web/app/assets/javascripts/utils.js @@ -239,6 +239,9 @@ var instrumentId = $element.attr('data-instrument-id'); if(instrumentId) { + if (instrumentId === "null") { + instrumentId = "not specified"; + } context.JK.hoverBubble($element, instrumentId, options); } else { diff --git a/web/app/assets/javascripts/wizard/gear/step_network_test.js b/web/app/assets/javascripts/wizard/gear/step_network_test.js index b82a622ae..17f28d06a 100644 --- a/web/app/assets/javascripts/wizard/gear/step_network_test.js +++ b/web/app/assets/javascripts/wizard/gear/step_network_test.js @@ -53,11 +53,13 @@ } function beforeShow() { + networkTest.haltScoring(); networkTest.cancel(); updateButtons(); } function beforeHide() { + networkTest.resumeScoring(); networkTest.cancel(); } diff --git a/web/app/controllers/api_users_controller.rb b/web/app/controllers/api_users_controller.rb index 955d0677d..57255ab66 100644 --- a/web/app/controllers/api_users_controller.rb +++ b/web/app/controllers/api_users_controller.rb @@ -672,8 +672,17 @@ class ApiUsersController < ApiController def udp_reachable Connection.transaction do @connection = Connection.find_by_client_id!(params[:client_id]) - @connection.udp_reachable = params[:udp_reachable] - @connection.save + # deliberately don't updated_at on connection! only heartbeats do that + Connection.where(:id => @connection.id).update_all(:udp_reachable => params[:udp_reachable]) + respond_with_model(@connection) + end + end + + def is_network_testing + Connection.transaction do + @connection = Connection.find_by_client_id!(params[:client_id]) + # deliberately don't updated_at on connection! only heartbeats do that + Connection.where(:id => @connection.id).update_all(:is_network_testing => params[:is_network_testing]) respond_with_model(@connection) end end diff --git a/web/config/application.rb b/web/config/application.rb index 575654d27..6947079af 100644 --- a/web/config/application.rb +++ b/web/config/application.rb @@ -248,6 +248,8 @@ if defined?(Bundler) config.ftue_network_test_packet_size = 60 # number of times that the backend retries before giving up config.ftue_network_test_backend_retries = 10 + # amount of time that we want passed until we run the next network test + config.ftue_network_test_min_wait_since_last_score = 5 # the maximum amount of allowable latency config.ftue_maximum_gear_latency = 20 diff --git a/web/config/initializers/gon.rb b/web/config/initializers/gon.rb index e2b1feb79..567b1e8e0 100644 --- a/web/config/initializers/gon.rb +++ b/web/config/initializers/gon.rb @@ -4,4 +4,5 @@ Gon.global.ftue_network_test_backend_retries = Rails.application.config.ftue_net Gon.global.twitter_public_account = Rails.application.config.twitter_public_account Gon.global.scoring_get_work_interval = Rails.application.config.scoring_get_work_interval Gon.global.scoring_get_work_backoff_interval = Rails.application.config.scoring_get_work_backoff_interval +Gon.global.ftue_network_test_min_wait_since_last_score = Rails.application.config.ftue_network_test_min_wait_since_last_score Gon.global.env = Rails.env diff --git a/web/config/routes.rb b/web/config/routes.rb index 723298515..5bdab1017 100644 --- a/web/config/routes.rb +++ b/web/config/routes.rb @@ -297,6 +297,7 @@ SampleApp::Application.routes.draw do # udp reachable (can stun?) match '/users/:id/udp_reachable' => 'api_users#udp_reachable', :via => :post + match '/users/:id/is_network_testing' => 'api_users#is_network_testing', :via => :post # social match '/users/:id/share/session/:provider' => 'api_users#share_session', :via => :get diff --git a/web/spec/controllers/api_feeds_controller_spec.rb b/web/spec/controllers/api_feeds_controller_spec.rb index e6b73e23f..78a60b021 100644 --- a/web/spec/controllers/api_feeds_controller_spec.rb +++ b/web/spec/controllers/api_feeds_controller_spec.rb @@ -94,6 +94,9 @@ describe ApiFeedsController do claimed_recording.recording.created_at = 3.days.ago claimed_recording.recording.save! + ams = ActiveMusicSession.find(claimed_recording.recording.music_session.music_session.id) + ams.before_destroy + get :index, { limit: 1 } json = JSON.parse(response.body, :symbolize_names => true) json[:entries].length.should == 1 diff --git a/websocket-gateway/Gemfile b/websocket-gateway/Gemfile index b0f63d310..06d45c942 100644 --- a/websocket-gateway/Gemfile +++ b/websocket-gateway/Gemfile @@ -34,7 +34,7 @@ gem 'rb-readline' gem 'aasm', '3.0.16' gem 'carrierwave' gem 'fog' -gem 'devise' +gem 'devise', '3.3.0' # 3.4.0 causes uninitialized constant ActionController::Metal (NameError) gem 'postgres-copy' gem 'aws-sdk' #, '1.29.1' gem 'bugsnag'