diff --git a/admin/Gemfile b/admin/Gemfile index b5e4ca236..7502a288e 100644 --- a/admin/Gemfile +++ b/admin/Gemfile @@ -49,7 +49,7 @@ gem 'unf', '0.1.3' #optional fog dependency gem 'country-select' gem 'aasm', '3.0.16' gem 'postgres-copy', '0.6.0' -gem 'aws-sdk' #, '1.29.1' +gem 'aws-sdk', '~> 1' gem 'bugsnag' gem 'gon' gem 'cocoon' diff --git a/admin/spec/factories.rb b/admin/spec/factories.rb index 08f668ff3..156fb1a6f 100644 --- a/admin/spec/factories.rb +++ b/admin/spec/factories.rb @@ -223,7 +223,6 @@ FactoryGirl.define do factory :jam_track, :class => JamRuby::JamTrack do sequence(:name) { |n| "jam-track-#{n}" } sequence(:description) { |n| "description-#{n}" } - bpm 100.1 time_signature '4/4' status 'Production' recording_type 'Cover' diff --git a/db/manifest b/db/manifest index e51930a82..55f1373b5 100755 --- a/db/manifest +++ b/db/manifest @@ -242,4 +242,14 @@ active_jam_track.sql bpms_on_tap_in.sql jamtracks_job.sql text_messages.sql -text_message_migration.sql \ No newline at end of file +text_message_migration.sql +user_model_about_changes.sql +performance_samples.sql +user_presences.sql +discard_scores_optimized.sql +backing_tracks.sql +metronome.sql +recorded_backing_tracks.sql +recorded_backing_tracks_add_filename.sql +user_syncs_include_backing_tracks.sql +remove_bpm_from_jamtracks.sql \ No newline at end of file diff --git a/db/up/backing_tracks.sql b/db/up/backing_tracks.sql new file mode 100644 index 000000000..328b36820 --- /dev/null +++ b/db/up/backing_tracks.sql @@ -0,0 +1,2 @@ +ALTER TABLE active_music_sessions ADD COLUMN backing_track_path VARCHAR(1024); +ALTER TABLE active_music_sessions ADD COLUMN backing_track_initiator_id VARCHAR(64); diff --git a/db/up/discard_scores_optimized.sql b/db/up/discard_scores_optimized.sql new file mode 100644 index 000000000..e33c22aa1 --- /dev/null +++ b/db/up/discard_scores_optimized.sql @@ -0,0 +1,89 @@ +DROP FUNCTION IF EXISTS discard_scores(); + +CREATE FUNCTION discard_scores (keep INTEGER) RETURNS VOID AS $$ +BEGIN + + DELETE FROM scores WHERE score_dt < + (SELECT score_dt FROM scores s WHERE s.alocidispid = scores.alocidispid AND s.blocidispid = scores.blocidispid ORDER BY score_dt DESC LIMIT 1 OFFSET (keep - 1)); + + RETURN; +END; +$$ LANGUAGE plpgsql; + + +CREATE OR REPLACE FUNCTION update_current_network_scores(aloc BIGINT, bloc BIGINT) RETURNS VOID +STRICT VOLATILE AS $$ + DECLARE + newscore INTEGER; + newscore_dt TIMESTAMP; + newscore_limited BOOL; + sum INTEGER; + kount INTEGER; + r RECORD; + avgscore INTEGER; + maxscore INTEGER; + minscore INTEGER; + BEGIN + -- find the 6 most recent scores + -- (supposedly newscore is the first...) + -- hybrid scheme: compute the average of some recent scores, then limit newscore to be between 4/5 and 6/5 of the average + newscore := NULL; + newscore_dt := NULL; + newscore_limited := FALSE; + sum := 0; + kount := 0; + FOR r IN SELECT score, score_dt FROM scores WHERE alocidispid = aloc AND blocidispid = bloc ORDER BY score_dt DESC LIMIT 6 LOOP + IF newscore IS NULL THEN + newscore := r.score; + newscore_dt := r.score_dt; + ELSE + sum := sum + r.score; + kount := kount + 1; + END IF; + END LOOP; + + -- if no scores in query at all, then delete any current entry + IF newscore IS NULL THEN + DELETE FROM current_network_scores WHERE alocidispid = aloc AND blocidispid = bloc; + IF aloc != bloc THEN + DELETE FROM current_network_scores WHERE alocidispid = bloc AND blocidispid = aloc; + END IF; + END IF; + + -- if there are scores older than newscore, then use their average to limit the range of newscore + IF kount > 0 THEN + avgscore := sum / kount; + maxscore := avgscore*6/5; + minscore := avgscore*4/5; + -- the score newscore will be inserted as the current value in current_network_scores, but we will limit it + -- to be no greater than 120% of the average and no less than 80% of the average. this will dampen wild + -- swings in the scores. + IF newscore > maxscore THEN + newscore := maxscore; + newscore_limited := TRUE; + ELSEIF newscore < minscore THEN + newscore := minscore; + newscore_limited := TRUE; + END IF; + END IF; + + UPDATE current_network_scores SET score = newscore, limited = newscore_limited, score_dt = newscore_dt WHERE alocidispid = aloc AND blocidispid = bloc; + IF NOT FOUND THEN + INSERT INTO current_network_scores (alocidispid, blocidispid, score, limited, score_dt) VALUES (aloc, bloc, newscore, newscore_limited, newscore_dt); + END IF; + + IF aloc != bloc THEN + UPDATE current_network_scores SET score = newscore, limited = newscore_limited, score_dt = newscore_dt WHERE alocidispid = bloc AND blocidispid = aloc; + IF NOT FOUND THEN + INSERT INTO current_network_scores (alocidispid, blocidispid, score, limited, score_dt) VALUES (bloc, aloc, newscore, newscore_limited, newscore_dt); + END IF; + END IF; + + -- keep the scores table clean, meaning only up to the most 5 recent scores per group & direction (scorer) + DELETE FROM scores WHERE alocidispid = aloc AND blocidispid = bloc AND scorer = 0 AND score_dt < + (SELECT score_dt FROM scores s WHERE s.alocidispid = aloc AND s.blocidispid = bloc AND s.scorer = 0 ORDER BY score_dt DESC LIMIT 1 OFFSET 4); + DELETE FROM scores WHERE alocidispid = bloc AND blocidispid = aloc AND scorer = 1 AND score_dt < + (SELECT score_dt FROM scores s WHERE s.alocidispid = bloc AND s.blocidispid = aloc AND s.scorer = 1 ORDER BY score_dt DESC LIMIT 1 OFFSET 4); + + END; +$$ LANGUAGE plpgsql; \ No newline at end of file diff --git a/db/up/metronome.sql b/db/up/metronome.sql new file mode 100644 index 000000000..25d18dc57 --- /dev/null +++ b/db/up/metronome.sql @@ -0,0 +1,2 @@ +ALTER TABLE active_music_sessions ADD COLUMN metronome_active BOOLEAN NOT NULL DEFAULT FALSE; +ALTER TABLE active_music_sessions ADD COLUMN metronome_initiator_id VARCHAR(64); \ No newline at end of file diff --git a/db/up/performance_samples.sql b/db/up/performance_samples.sql new file mode 100644 index 000000000..60c0cb38e --- /dev/null +++ b/db/up/performance_samples.sql @@ -0,0 +1,10 @@ +CREATE TABLE performance_samples ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id VARCHAR(64) NOT NULL REFERENCES users(id) ON DELETE CASCADE, + url VARCHAR(4000) NULL, + type VARCHAR(100) NOT NULL, + claimed_recording_id VARCHAR(64) REFERENCES claimed_recordings(id) ON DELETE CASCADE, + service_id VARCHAR(100) NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); \ No newline at end of file diff --git a/db/up/recorded_backing_tracks.sql b/db/up/recorded_backing_tracks.sql new file mode 100644 index 000000000..6657dee1a --- /dev/null +++ b/db/up/recorded_backing_tracks.sql @@ -0,0 +1,38 @@ +CREATE UNLOGGED TABLE backing_tracks ( + id VARCHAR(64) NOT NULL PRIMARY KEY DEFAULT uuid_generate_v4(), + filename VARCHAR(1024) NOT NULL, + + connection_id VARCHAR(64) NOT NULL REFERENCES connections(id) ON DELETE CASCADE, + client_track_id VARCHAR(64) NOT NULL, + client_resource_id VARCHAR(100), + + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE recorded_backing_tracks ( + id BIGINT PRIMARY KEY, + user_id VARCHAR(64) REFERENCES users(id) ON DELETE CASCADE, + backing_track_id VARCHAR(64), + recording_id VARCHAR(64) NOT NULL, + + client_track_id VARCHAR(64) NOT NULL, + is_part_uploading BOOLEAN NOT NULL DEFAULT FALSE, + next_part_to_upload INTEGER NOT NULL DEFAULT 0, + upload_id CHARACTER VARYING(1024), + part_failures INTEGER NOT NULL DEFAULT 0, + discard BOOLEAN, + download_count INTEGER NOT NULL DEFAULT 0, + md5 CHARACTER VARYING(100), + length BIGINT, + client_id VARCHAR(64) NOT NULL, + file_offset BIGINT, + + url VARCHAR(1024) NOT NULL, + fully_uploaded BOOLEAN NOT NULL DEFAULT FALSE, + upload_failures INTEGER NOT NULL DEFAULT 0, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +ALTER TABLE recorded_backing_tracks ALTER COLUMN id SET DEFAULT nextval('tracks_next_tracker_seq'); diff --git a/db/up/recorded_backing_tracks_add_filename.sql b/db/up/recorded_backing_tracks_add_filename.sql new file mode 100644 index 000000000..e686700b7 --- /dev/null +++ b/db/up/recorded_backing_tracks_add_filename.sql @@ -0,0 +1,2 @@ +ALTER TABLE recorded_backing_tracks ADD COLUMN filename VARCHAR NOT NULL; +ALTER TABLE recorded_backing_tracks ADD COLUMN last_downloaded_at TIMESTAMP WITHOUT TIME ZONE; \ No newline at end of file diff --git a/db/up/remove_bpm_from_jamtracks.sql b/db/up/remove_bpm_from_jamtracks.sql new file mode 100644 index 000000000..681c3b8b8 --- /dev/null +++ b/db/up/remove_bpm_from_jamtracks.sql @@ -0,0 +1 @@ +ALTER TABLE jam_tracks DROP COLUMN bpm; \ No newline at end of file diff --git a/db/up/user_model_about_changes.sql b/db/up/user_model_about_changes.sql new file mode 100644 index 000000000..29cbbd0cc --- /dev/null +++ b/db/up/user_model_about_changes.sql @@ -0,0 +1,25 @@ +ALTER TABLE users ADD COLUMN website varchar(4000) NULL; +ALTER TABLE users ADD COLUMN skill_level smallint NULL; +ALTER TABLE users ADD COLUMN concert_count smallint NULL; +ALTER TABLE users ADD COLUMN studio_session_count smallint NULL; + +-- virtual band +ALTER TABLE users ADD COLUMN virtual_band boolean NOT NULL DEFAULT FALSE; +ALTER TABLE users ADD COLUMN virtual_band_commitment smallint NULL; + +-- traditional band +ALTER TABLE users ADD COLUMN traditional_band boolean NOT NULL DEFAULT FALSE; +ALTER TABLE users ADD COLUMN traditional_band_commitment smallint NULL; +ALTER TABLE users ADD COLUMN traditional_band_touring boolean NULL; + +-- paid sessions +ALTER TABLE users ADD COLUMN paid_sessions boolean NOT NULL DEFAULT FALSE; +ALTER TABLE users ADD COLUMN paid_sessions_hourly_rate int NULL; +ALTER TABLE users ADD COLUMN paid_sessions_daily_rate int NULL; + +-- free sessions +ALTER TABLE users ADD COLUMN free_sessions boolean NOT NULL DEFAULT FALSE; + +-- cowriting +ALTER TABLE users ADD COLUMN cowriting boolean NOT NULL DEFAULT FALSE; +ALTER TABLE users ADD COLUMN cowriting_purpose smallint NULL; \ No newline at end of file diff --git a/db/up/user_presences.sql b/db/up/user_presences.sql new file mode 100644 index 000000000..d18090634 --- /dev/null +++ b/db/up/user_presences.sql @@ -0,0 +1,8 @@ +CREATE TABLE user_presences ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id VARCHAR(64) NOT NULL REFERENCES users(id) ON DELETE CASCADE, + type VARCHAR(100) NOT NULL, + username VARCHAR(100) NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); \ No newline at end of file diff --git a/db/up/user_syncs_include_backing_tracks.sql b/db/up/user_syncs_include_backing_tracks.sql new file mode 100644 index 000000000..165617abf --- /dev/null +++ b/db/up/user_syncs_include_backing_tracks.sql @@ -0,0 +1,47 @@ + +DROP VIEW user_syncs; + +CREATE VIEW user_syncs AS + SELECT DISTINCT b.id AS recorded_track_id, + CAST(NULL as BIGINT) AS mix_id, + CAST(NULL as BIGINT) AS quick_mix_id, + CAST(NULL as BIGINT) AS recorded_backing_track_id, + b.id AS unified_id, + a.user_id AS user_id, + b.fully_uploaded, + recordings.created_at AS created_at, + recordings.id AS recording_id + FROM recorded_tracks a INNER JOIN recordings ON a.recording_id = recordings.id AND duration IS NOT NULL AND all_discarded = FALSE AND deleted = FALSE INNER JOIN recorded_tracks b ON a.recording_id = b.recording_id + UNION ALL + SELECT CAST(NULL AS BIGINT) AS recorded_track_id, + CAST(NULL as BIGINT) AS mix_id, + CAST(NULL as BIGINT) AS quick_mix_id, + a.id AS recorded_backing_track_id, + a.id AS unified_id, + a.user_id AS user_id, + a.fully_uploaded, + recordings.created_at AS created_at, + recordings.id AS recording_id + FROM recorded_backing_tracks a INNER JOIN recordings ON a.recording_id = recordings.id AND duration IS NOT NULL AND all_discarded = FALSE AND deleted = FALSE + UNION ALL + SELECT CAST(NULL as BIGINT) AS recorded_track_id, + mixes.id AS mix_id, + CAST(NULL as BIGINT) AS quick_mix_id, + CAST(NULL as BIGINT) AS recorded_backing_track_id, + mixes.id AS unified_id, + claimed_recordings.user_id AS user_id, + NULL as fully_uploaded, + recordings.created_at AS created_at, + recordings.id AS recording_id + FROM mixes INNER JOIN recordings ON mixes.recording_id = recordings.id INNER JOIN claimed_recordings ON recordings.id = claimed_recordings.recording_id WHERE claimed_recordings.discarded = FALSE AND deleted = FALSE + UNION ALL + SELECT CAST(NULL as BIGINT) AS recorded_track_id, + CAST(NULL as BIGINT) AS mix_id, + quick_mixes.id AS quick_mix_id, + CAST(NULL as BIGINT) AS recorded_backing_track_id, + quick_mixes.id AS unified_id, + quick_mixes.user_id, + quick_mixes.fully_uploaded, + recordings.created_at AS created_at, + recordings.id AS recording_id + FROM quick_mixes INNER JOIN recordings ON quick_mixes.recording_id = recordings.id AND duration IS NOT NULL AND all_discarded = FALSE AND deleted = FALSE; diff --git a/monitor/spec/production_spec.rb b/monitor/spec/production_spec.rb index 99e296464..01cf90fa1 100755 --- a/monitor/spec/production_spec.rb +++ b/monitor/spec/production_spec.rb @@ -50,6 +50,7 @@ describe "Deployed site at #{www}", :js => true, :type => :feature, :capybara_fe end it "is possible for #{user3} to sign in and not get disconnected within 30 seconds" do + pending "continual failures - need to debug - try using Selenium instead of PhantomJS" as_monitor(user3) do sign_in_poltergeist(user3) repeat_for(30.seconds) do diff --git a/monitor/spec/support/utilities.rb b/monitor/spec/support/utilities.rb index be0e32722..694d9e604 100755 --- a/monitor/spec/support/utilities.rb +++ b/monitor/spec/support/utilities.rb @@ -430,7 +430,7 @@ def assert_all_tracks_seen(users=[]) users.each do |user| in_client(user) do users.reject {|u| u==user}.each do |other| - find('div.track-label', text: other.name) + find('div.track-label > span', text: other.name) #puts user.name + " is able to see " + other.name + "\'s track" end end diff --git a/ruby/Gemfile b/ruby/Gemfile index f38b2de02..7a31165d1 100644 --- a/ruby/Gemfile +++ b/ruby/Gemfile @@ -28,7 +28,7 @@ gem 'amqp', '1.0.2' gem 'will_paginate' gem 'actionmailer', '3.2.13' gem 'sendgrid', '1.2.0' -gem 'aws-sdk' #, '1.29.1' +gem 'aws-sdk', '~> 1' gem 'carrierwave', '0.9.0' gem 'aasm', '3.0.16' gem 'devise', '3.3.0' # 3.4.0 causes: uninitialized constant ActionController::Metal (NameError) diff --git a/ruby/lib/jam_ruby.rb b/ruby/lib/jam_ruby.rb index 084ae7d9e..b94d7d9a2 100755 --- a/ruby/lib/jam_ruby.rb +++ b/ruby/lib/jam_ruby.rb @@ -42,6 +42,7 @@ require "jam_ruby/resque/resque_hooks" require "jam_ruby/resque/audiomixer" require "jam_ruby/resque/quick_mixer" require "jam_ruby/resque/icecast_config_writer" +require "jam_ruby/resque/stress_job" require "jam_ruby/resque/scheduled/audiomixer_retry" require "jam_ruby/resque/scheduled/icecast_config_retry" require "jam_ruby/resque/scheduled/icecast_source_check" @@ -86,6 +87,7 @@ require "jam_ruby/lib/stats.rb" require "jam_ruby/amqp/amqp_connection_manager" require "jam_ruby/database" require "jam_ruby/message_factory" +require "jam_ruby/models/backing_track" require "jam_ruby/models/feedback" require "jam_ruby/models/feedback_observer" #require "jam_ruby/models/max_mind_geo" @@ -131,8 +133,11 @@ require "jam_ruby/models/search" require "jam_ruby/models/recording" require "jam_ruby/models/recording_comment" require "jam_ruby/models/recording_liker" +require "jam_ruby/models/recorded_backing_track" +require "jam_ruby/models/recorded_backing_track_observer" require "jam_ruby/models/recorded_track" require "jam_ruby/models/recorded_track_observer" +require "jam_ruby/models/recorded_video" require "jam_ruby/models/quick_mix" require "jam_ruby/models/quick_mix_observer" require "jam_ruby/models/share_token" @@ -196,7 +201,6 @@ require "jam_ruby/models/score_history" require "jam_ruby/models/jam_company" require "jam_ruby/models/user_sync" require "jam_ruby/models/video_source" -require "jam_ruby/models/recorded_video" require "jam_ruby/models/text_message" require "jam_ruby/jam_tracks_manager" diff --git a/ruby/lib/jam_ruby/app/uploaders/jam_track_track_uploader.rb b/ruby/lib/jam_ruby/app/uploaders/jam_track_track_uploader.rb index 4a4d8cc4f..9defe1099 100644 --- a/ruby/lib/jam_ruby/app/uploaders/jam_track_track_uploader.rb +++ b/ruby/lib/jam_ruby/app/uploaders/jam_track_track_uploader.rb @@ -11,7 +11,7 @@ class JamTrackTrackUploader < CarrierWave::Uploader::Base # Add a white list of extensions which are allowed to be uploaded. def extension_white_list - %w(ogg) + %w(ogg wav) end def store_dir diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/welcome_message.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/welcome_message.html.erb index 439aefe3a..35e380618 100644 --- a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/welcome_message.html.erb +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/welcome_message.html.erb @@ -1,45 +1,64 @@ <% provide(:title, 'Welcome to JamKazam!') %> +

Hello <%= EmailBatchProgression::VAR_FIRST_NAME %> --

+

We're delighted that you have decided to try the JamKazam service, - and we hope that you will enjoy using JamKazam to play music with others. - Following are links to some resources that can help to get you up and running quickly. + and we hope that you will enjoy using JamKazam to play + music with others. + Following are some resources that can help you get oriented and get the most out of JamKazam. +

+ + +

+ +

Getting Started
+ There are basically three kinds of setups you can use to play on JamKazam.
+

+

+ +

JamKazam Features
+ JamKazam offers a very robust and exciting set of features for playing online and sharing your performances with others. Here are some videos you can watch + to easily get up to speed on some of the things you can do with JamKazam:
+

+

+ +

Getting Help
+ If you run into trouble and need help, please reach out to us. We will be glad to do everything we can to answer your questions and get you up and running. + You can visit our + Support Portal + to find knowledge base articles and post questions that have + not already been answered. You can email us at support@jamkazam.com. And if you just want to chat, share tips and war stories, and hang out with fellow JamKazamers, + you can visit our Community Forum + .

-Getting Started Video
-We recommend watching this video before you jump into the service just to get oriented. It will really help you hit the ground running: -https://www.youtube.com/watch?v=DBo--aj_P1w + Again, welcome to JamKazam, and we look forward to seeing – and hearing – you online soon!

-

-Other Great Tutorial Videos
-There are several other very great videos that will help you understand how to find and connect with other musicians on the service, create your own sessions or find and join other musicians’ sessions, play in sessions, record and share your performances, and even live broadcast your sessions to family, friends, and fans. Check these helpful videos out here: -https://jamkazam.desk.com/customer/portal/topics/673198-tutorials-on-major-features/articles -

- -

-Knowledge Base Articles
-You can find Getting Started knowledge base articles on things like frequently asked questions (FAQ), minimum system requirements for your Windows or Mac computer, how to troubleshoot audio problems in sessions, and more here: -https://jamkazam.desk.com/customer/portal/topics/564807-getting-started/articles -

- -

-JamKazam Support Portal
-If you run into trouble and need help, please reach out to us. We will be glad to do everything we can to get you up and running. You can find our support portal here: -https://jamkazam.desk.com/ -

- -

-JamKazam Community Forum
-And if you just want to chat, share tips and war stories, and hang out with fellow JamKazamers, you can visit our community forum here: -http://forums.jamkazam.com/ -

- -

-Please take a moment to like or follow us by clicking the icons below, and we look forward to seeing – and hearing – you online soon! -

- -  -- Team JamKazam +

Best Regards,
+ Team JamKazam

\ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/welcome_message.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/welcome_message.text.erb index 90efc88ef..a9f2ed06b 100644 --- a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/welcome_message.text.erb +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/welcome_message.text.erb @@ -1,27 +1,43 @@ Hello <%= EmailBatchProgression::VAR_FIRST_NAME %> -- -We're delighted that you have decided to try the JamKazam service, and we hope that you will enjoy using JamKazam to play music with others. Following are links to some resources that can help to get you up and running quickly. +We're delighted that you have decided to try the JamKazam service, and we hope that you will enjoy using JamKazam to play music with others. Following are some resources that can help you get oriented and get the most out of JamKazam. -Getting Started Video -We recommend watching this video before you jump into the service just to get oriented. It will really help you hit the ground running: -https://www.youtube.com/watch?v=DBo--aj_P1w -Other Great Tutorial Videos -There are several other very great videos that will help you understand how to find and connect with other musicians on the service, create your own sessions or find and join other musicians’ sessions, play in sessions, record and share your performances, and even live broadcast your sessions to family, friends, and fans. Check these helpful videos out here: -https://jamkazam.desk.com/customer/portal/topics/673198-tutorials-on-major-features/articles +Getting Started +--------------- -Knowledge Base Articles -You can find Getting Started knowledge base articles on things like frequently asked questions (FAQ), minimum system requirements for your Windows or Mac computer, how to troubleshoot audio problems in sessions, and more here: -https://jamkazam.desk.com/customer/portal/topics/564807-getting-started/articles +There are basically three kinds of setups you can use to play on JamKazam. -JamKazam Support Portal -If you run into trouble and need help, please reach out to us. We will be glad to do everything we can to get you up and running. You can find our support portal here: -https://jamkazam.desk.com +* Built-In Audio on Your Computer - You can use a Windows or Mac computer, and just use the built-in mic and headphone jack to handle your audio. This is cheap and easy, but your audio quality will suffer, and it will also process audio very slowly, creating problems with latency, or lag, in your sessions. Still, you can at least start experimenting with JamKazam in this way. -JamKazam Community Forum -And if you just want to chat, share tips and war stories, and hang out with fellow JamKazamers, you can visit our community forum here: -http://forums.jamkazam.com +* Computer with External Audio Interface - - You can use a Windows or Mac computer with an external audio interface that you already own and use for recording, if you happen to have one already. If you are going to do this, or use the built-in mic/headphones on your computer, please refer to our Minimum System Requirements at https://jamkazam.desk.com/customer/portal/articles/1288274-minimum-system-requirements to make sure your computer will work. These requirements were on the download page for the app, but you may have sped by them. Also, we'd recommend watching our Getting Started Video at https://www.youtube.com/watch?v=DBo--aj_P1w to learn more about your options here. -Please take a moment to like or follow us by clicking the icons below, and we look forward to seeing – and hearing – you online soon! +* The JamBlaster - JamKazam has designed a new product from the ground up to be the best way to play music online in real time. It's called the JamBlaster. It processes audio faster than any of the thousands of combinations of computers and interfaces in use on JamKazam today, which means you can play with musicians who are farther away from you, and closer sessions will feel/sound tighter. The JamBlaster is both a computer and an audio interface, so it also eliminates the system requirements worries, and it "just works" so you don't have to be an audio and computer genius to get it working. This is a great product - available only through a Kickstarter program running during a 30-day window during parts of February and March 2015. You can watch the JamBlaster Video at https://www.youtube.com/watch?v=gAJAIHMyois to learn more about this amazing new product. + + +JamKazam Features +----------------- + +JamKazam offers a very robust and exciting set of features for playing online and sharing your performances with others. Here are some videos you can watch to easily get up to speed on some of the things you can do with JamKazam: + +* Creating a Session - https://www.youtube.com/watch?v=EZZuGcDUoWk + +* Finding a Session - https://www.youtube.com/watch?v=xWponSJo-GU + +* Playing in a Session - https://www.youtube.com/watch?v=zJ68hA8-fLA + +* Connecting with Other Musicians - https://www.youtube.com/watch?v=4KWklSZZxRc + +* Working with Recordings - https://www.youtube.com/watch?v=Gn-dOqnNLoY + + +Getting Help +------------ + +If you run into trouble and need help, please reach out to us. We will be glad to do everything we can to answer your questions and get you up and running. You can visit our Support Portal at https://jamkazam.desk.com/ to find knowledge base articles and post questions that have not already been answered. You can email us at support@jamkazam.com. And if you just want to chat, share tips and war stories, and hang out with fellow JamKazamers, you can visit our Community Forum at http://forums.jamkazam.com/. + +Again, welcome to JamKazam, and we look forward to seeing – and hearing – you online soon! + +Best Regards, +Team JamKazam --- Team JamKazam diff --git a/ruby/lib/jam_ruby/connection_manager.rb b/ruby/lib/jam_ruby/connection_manager.rb index bcf06b4c5..3ab2f4a84 100644 --- a/ruby/lib/jam_ruby/connection_manager.rb +++ b/ruby/lib/jam_ruby/connection_manager.rb @@ -311,14 +311,19 @@ SQL end end else - # there are still people in the session + + conn.exec("UPDATE active_music_sessions set backing_track_initiator_id = NULL, backing_track_path = NULL where backing_track_initiator_id = $1 and id = $2", + [user_id, previous_music_session_id]) + + conn.exec("UPDATE active_music_sessions set metronome_initiator_id = NULL, metronome_active = FALSE where metronome_initiator_id = $1 and id = $2", + [user_id, previous_music_session_id]) #ensure that there is no active claimed recording if the owner of that recording left the session conn.exec("UPDATE active_music_sessions set claimed_recording_id = NULL, claimed_recording_initiator_id = NULL where claimed_recording_initiator_id = $1 and id = $2", - [user_id, previous_music_session_id]) + [user_id, previous_music_session_id]) conn.exec("UPDATE active_music_sessions set jam_track_id = NULL, jam_track_initiator_id = NULL where jam_track_initiator_id = $1 and id = $2", - [user_id, previous_music_session_id]) + [user_id, previous_music_session_id]) end end diff --git a/ruby/lib/jam_ruby/constants/validation_messages.rb b/ruby/lib/jam_ruby/constants/validation_messages.rb index c24ca07ce..662fce608 100644 --- a/ruby/lib/jam_ruby/constants/validation_messages.rb +++ b/ruby/lib/jam_ruby/constants/validation_messages.rb @@ -82,6 +82,8 @@ module ValidationMessages MUST_BE_KNOWN_TIMEZONE = "not valid" JAM_TRACK_ALREADY_OPEN = 'another jam track already open' RECORDING_ALREADY_IN_PROGRESS = "recording being made" + METRONOME_ALREADY_OPEN = 'another metronome already open' + BACKING_TRACK_ALREADY_OPEN = 'another audio file already open' # notification DIFFERENT_SOURCE_TARGET = 'can\'t be same as the sender' diff --git a/ruby/lib/jam_ruby/jam_tracks_manager.rb b/ruby/lib/jam_ruby/jam_tracks_manager.rb index ee7318118..a5c04cd61 100644 --- a/ruby/lib/jam_ruby/jam_tracks_manager.rb +++ b/ruby/lib/jam_ruby/jam_tracks_manager.rb @@ -25,6 +25,9 @@ module JamRuby Dir.mktmpdir do |tmp_dir| jam_file_opts="" jam_track.jam_track_tracks.each do |jam_track_track| + + next if jam_track_track.track_type != "Track" # master mixes do not go into the JKZ + # use the jam_track_track ID as the filename.ogg/.wav, because it's important metadata nm = jam_track_track.id + File.extname(jam_track_track.filename) track_filename = File.join(tmp_dir, nm) diff --git a/ruby/lib/jam_ruby/models/active_music_session.rb b/ruby/lib/jam_ruby/models/active_music_session.rb index c28ae8de2..5e6ed8036 100644 --- a/ruby/lib/jam_ruby/models/active_music_session.rb +++ b/ruby/lib/jam_ruby/models/active_music_session.rb @@ -7,7 +7,7 @@ module JamRuby self.table_name = 'active_music_sessions' - attr_accessor :legal_terms, :max_score, :opening_jam_track, :opening_recording + attr_accessor :legal_terms, :max_score, :opening_jam_track, :opening_recording, :opening_backing_track, :opening_metronome belongs_to :claimed_recording, :class_name => "JamRuby::ClaimedRecording", :foreign_key => "claimed_recording_id", :inverse_of => :playing_sessions belongs_to :claimed_recording_initiator, :class_name => "JamRuby::User", :inverse_of => :playing_claimed_recordings, :foreign_key => "claimed_recording_initiator_id" @@ -15,6 +15,9 @@ module JamRuby belongs_to :jam_track, :class_name => "JamRuby::JamTrack", :foreign_key => "jam_track_id", :inverse_of => :playing_sessions belongs_to :jam_track_initiator, :class_name => "JamRuby::User", :inverse_of => :playing_jam_tracks, :foreign_key => "jam_track_initiator_id" + belongs_to :backing_track_initiator, :class_name => "JamRuby::User", :inverse_of => :playing_jam_tracks, :foreign_key => "backing_track_initiator_id" + belongs_to :metronome_initiator, :class_name => "JamRuby::User", :inverse_of => :playing_jam_tracks, :foreign_key => "metronome_initiator_id" + has_one :music_session, :class_name => "JamRuby::MusicSession", :foreign_key => 'music_session_id' has_one :mount, :class_name => "JamRuby::IcecastMount", :inverse_of => :music_session, :foreign_key => 'music_session_id' belongs_to :creator, :class_name => 'JamRuby::User', :foreign_key => :user_id @@ -27,6 +30,8 @@ module JamRuby validate :creator_is_musician validate :validate_opening_recording, :if => :opening_recording validate :validate_opening_jam_track, :if => :opening_jam_track + validate :validate_opening_backing_track, :if => :opening_backing_track + validate :validate_opening_metronome, :if => :opening_metronome after_create :started_session @@ -73,22 +78,52 @@ module JamRuby if is_jam_track_open? errors.add(:claimed_recording, ValidationMessages::JAM_TRACK_ALREADY_OPEN) end + + if is_backing_track_open? + errors.add(:claimed_recording, ValidationMessages::BACKING_TRACK_ALREADY_OPEN) + end + + if is_metronome_open? + errors.add(:claimed_recording, ValidationMessages::METRONOME_ALREADY_OPEN) + end end def validate_opening_jam_track + validate_other_audio(:jam_track) + end + + def validate_opening_backing_track + validate_other_audio(:backing_track) + end + + def validate_opening_metronome + validate_other_audio(:metronome) + end + + def validate_other_audio(error_key) + # validate that there is no metronome already open in this session + if metronome_active_was + errors.add(error_key, ValidationMessages::METRONOME_ALREADY_OPEN) + end + + # validate that there is no backing track already open in this session + if backing_track_path_was.present? + errors.add(error_key, ValidationMessages::BACKING_TRACK_ALREADY_OPEN) + end + # validate that there is no jam track already open in this session - unless jam_track_id_was.nil? - errors.add(:jam_track, ValidationMessages::JAM_TRACK_ALREADY_OPEN) + if jam_track_id_was.present? + errors.add(error_key, ValidationMessages::JAM_TRACK_ALREADY_OPEN) end # validate that there is no recording being made if is_recording? - errors.add(:jam_track, ValidationMessages::RECORDING_ALREADY_IN_PROGRESS) + errors.add(error_key, ValidationMessages::RECORDING_ALREADY_IN_PROGRESS) end # validate that there is no recording being played back to the session if is_playing_recording? - errors.add(:jam_track, ValidationMessages::CLAIMED_RECORDING_ALREADY_IN_PROGRESS) + errors.add(error_key, ValidationMessages::CLAIMED_RECORDING_ALREADY_IN_PROGRESS) end end @@ -593,6 +628,14 @@ module JamRuby !self.jam_track.nil? end + def is_backing_track_open? + self.backing_track_path.present? + end + + def is_metronome_open? + self.metronome_active.present? + end + # is this music session currently recording? def is_recording? recordings.where(:duration => nil).count > 0 @@ -742,6 +785,35 @@ module JamRuby self.save end + # @param backing_track_path is a relative path: + def open_backing_track(user, backing_track_path) + self.backing_track_path = backing_track_path + self.backing_track_initiator = user + self.opening_backing_track = true + self.save + self.opening_backing_track = false + end + + def close_backing_track + self.backing_track_path = nil + self.backing_track_initiator = nil + self.save + end + + def open_metronome(user) + self.metronome_active = true + self.metronome_initiator = user + self.opening_metronome = true + self.save + self.opening_metronome = false + end + + def close_metronome + self.metronome_active = false + self.metronome_initiator = nil + self.save + end + def self.sync(session_history) music_session = MusicSession.find_by_id(session_history.id) diff --git a/ruby/lib/jam_ruby/models/backing_track.rb b/ruby/lib/jam_ruby/models/backing_track.rb new file mode 100644 index 000000000..fc5e6517b --- /dev/null +++ b/ruby/lib/jam_ruby/models/backing_track.rb @@ -0,0 +1,19 @@ +module JamRuby + class BackingTrack < ActiveRecord::Base + + self.table_name = "backing_tracks" + self.primary_key = 'id' + + default_scope order('created_at ASC') + + belongs_to :connection, :class_name => "JamRuby::Connection", :inverse_of => :tracks, :foreign_key => 'connection_id' + validates :connection, presence: true + validates :client_track_id, presence: true + validates :filename, presence: true + + def user + self.connection.user + end + + end +end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/connection.rb b/ruby/lib/jam_ruby/models/connection.rb index 8a813f9ce..56591a318 100644 --- a/ruby/lib/jam_ruby/models/connection.rb +++ b/ruby/lib/jam_ruby/models/connection.rb @@ -18,6 +18,7 @@ module JamRuby belongs_to :music_session, :class_name => "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 :backing_tracks, :class_name => "JamRuby::BackingTrack", :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, nil]} diff --git a/ruby/lib/jam_ruby/models/jam_track.rb b/ruby/lib/jam_ruby/models/jam_track.rb index 7f383f5dc..3d884ca3a 100644 --- a/ruby/lib/jam_ruby/models/jam_track.rb +++ b/ruby/lib/jam_ruby/models/jam_track.rb @@ -20,7 +20,6 @@ module JamRuby validates :name, presence: true, uniqueness: true, length: {maximum: 200} validates :description, length: {maximum: 1000} - validates_format_of :bpm, with: /^\d+\.*\d{0,1}$/ validates :time_signature, inclusion: {in: [nil] + TIME_SIGNATURES} validates :status, inclusion: {in: [nil] + STATUS} validates :recording_type, inclusion: {in: [nil] + RECORDING_TYPE} diff --git a/ruby/lib/jam_ruby/models/mix.rb b/ruby/lib/jam_ruby/models/mix.rb index 8e8ab2ace..83557f4f7 100644 --- a/ruby/lib/jam_ruby/models/mix.rb +++ b/ruby/lib/jam_ruby/models/mix.rb @@ -138,11 +138,17 @@ module JamRuby manifest = { "files" => [], "timeline" => [] } mix_params = [] + recording.recorded_tracks.each do |recorded_track| manifest["files"] << { "filename" => recorded_track.sign_url(one_day), "codec" => "vorbis", "offset" => 0 } mix_params << { "level" => 100, "balance" => 0 } end + recording.recorded_backing_tracks.each do |recorded_backing_track| + manifest["files"] << { "filename" => recorded_backing_track.sign_url(one_day), "codec" => "vorbis", "offset" => 0 } + mix_params << { "level" => 100, "balance" => 0 } + end + manifest["timeline"] << { "timestamp" => 0, "mix" => mix_params } manifest["output"] = { "codec" => "vorbis" } manifest["recording_id"] = self.recording.id diff --git a/ruby/lib/jam_ruby/models/recorded_backing_track.rb b/ruby/lib/jam_ruby/models/recorded_backing_track.rb new file mode 100644 index 000000000..0826d160c --- /dev/null +++ b/ruby/lib/jam_ruby/models/recorded_backing_track.rb @@ -0,0 +1,196 @@ +module JamRuby + # BackingTrack analog to JamRuby::RecordedTrack + class RecordedBackingTrack < ActiveRecord::Base + + include JamRuby::S3ManagerMixin + + attr_accessor :marking_complete + attr_writer :current_user + + belongs_to :user, :class_name => "JamRuby::User", :inverse_of => :recorded_backing_tracks + belongs_to :recording, :class_name => "JamRuby::Recording", :inverse_of => :recorded_backing_tracks + validates :filename, :presence => true + + validates :client_id, :presence => true # not a connection relation on purpose + validates :backing_track_id, :presence => true # not a track relation on purpose + validates :client_track_id, :presence => true + validates :md5, :presence => true, :if => :upload_starting? + validates :length, length: {minimum: 1, maximum: 1024 * 1024 * 256 }, if: :upload_starting? # 256 megs max. is this reasonable? surely... + validates :user, presence: true + validates :download_count, presence: true + + before_destroy :delete_s3_files + validate :validate_fully_uploaded + validate :validate_part_complete + validate :validate_too_many_upload_failures + validate :verify_download_count + + def self.create_from_backing_track(backing_track, recording) + recorded_backing_track = self.new + recorded_backing_track.recording = recording + recorded_backing_track.client_id = backing_track.connection.client_id + recorded_backing_track.backing_track_id = backing_track.id + recorded_backing_track.client_track_id = "R" + backing_track.client_track_id # Matches behavior in RecordingManager.cpp#getWavComment + recorded_backing_track.user = backing_track.connection.user + recorded_backing_track.filename = backing_track.filename + recorded_backing_track.next_part_to_upload = 0 + recorded_backing_track.file_offset = 0 + recorded_backing_track[:url] = construct_filename(recording.created_at, recording.id, backing_track.client_track_id) + recorded_backing_track.save + recorded_backing_track + end + + def sign_url(expiration_time = 120) + s3_manager.sign_url(self[:url], {:expires => expiration_time, :response_content_type => 'audio/ogg', :secure => false}) + end + + def can_download?(some_user) + claimed_recording = recording.claimed_recordings.find{|claimed_recording| claimed_recording.user == some_user } + + if claimed_recording + !claimed_recording.discarded + else + false + end + end + + def too_many_upload_failures? + upload_failures >= APP_CONFIG.max_track_upload_failures + end + + def too_many_downloads? + (self.download_count < 0 || self.download_count > APP_CONFIG.max_audio_downloads) && !@current_user.admin + end + + def upload_starting? + next_part_to_upload_was == 0 && next_part_to_upload == 1 + end + + def validate_too_many_upload_failures + if upload_failures >= APP_CONFIG.max_track_upload_failures + errors.add(:upload_failures, ValidationMessages::UPLOAD_FAILURES_EXCEEDED) + end + end + + def validate_fully_uploaded + if marking_complete && fully_uploaded && fully_uploaded_was + errors.add(:fully_uploaded, ValidationMessages::ALREADY_UPLOADED) + end + end + + def validate_part_complete + + # if we see a transition from is_part_uploading from true to false, we validate + if is_part_uploading_was && !is_part_uploading + if next_part_to_upload_was + 1 != next_part_to_upload + errors.add(:next_part_to_upload, ValidationMessages::INVALID_PART_NUMBER_SPECIFIED) + end + + if file_offset > length + errors.add(:file_offset, ValidationMessages::FILE_OFFSET_EXCEEDS_LENGTH) + end + elsif next_part_to_upload_was + 1 == next_part_to_upload + # this makes sure we are only catching 'upload_part_complete' transitions, and not upload_start + if next_part_to_upload_was != 0 + # we see that the part number was ticked--but was is_part_upload set to true before this transition? + if !is_part_uploading_was && !is_part_uploading + errors.add(:next_part_to_upload, ValidationMessages::PART_NOT_STARTED) + end + end + end + end + + def verify_download_count + if (self.download_count < 0 || self.download_count > APP_CONFIG.max_audio_downloads) && !@current_user.admin + errors.add(:download_count, "must be less than or equal to 100") + end + end + + + def upload_start(length, md5) + #self.upload_id set by the observer + self.next_part_to_upload = 1 + self.length = length + self.md5 = md5 + save + end + + # if for some reason the server thinks the client can't carry on with the upload, + # this resets everything to the initial state + def reset_upload + self.upload_failures = self.upload_failures + 1 + self.part_failures = 0 + self.file_offset = 0 + self.next_part_to_upload = 0 + self.upload_id = nil + self.md5 = nil + self.length = 0 + self.fully_uploaded = false + self.is_part_uploading = false + save :validate => false # skip validation because we need this to always work + end + + def upload_next_part(length, md5) + self.marking_complete = true + if next_part_to_upload == 0 + upload_start(length, md5) + end + self.is_part_uploading = true + save + end + + def upload_sign(content_md5) + s3_manager.upload_sign(self[:url], content_md5, next_part_to_upload, upload_id) + end + + def upload_part_complete(part, offset) + # validated by :validate_part_complete + self.marking_complete = true + self.is_part_uploading = false + self.next_part_to_upload = self.next_part_to_upload + 1 + self.file_offset = offset.to_i + self.part_failures = 0 + save + end + + def upload_complete + # validate from happening twice by :validate_fully_uploaded + self.fully_uploaded = true + self.marking_complete = true + save + end + + def increment_part_failures(part_failure_before_error) + self.part_failures = part_failure_before_error + 1 + RecordedBackingTrack.update_all("part_failures = #{self.part_failures}", "id = '#{self.id}'") + end + + def stored_filename + # construct a path from s3 + RecordedBacknigTrack.construct_filename(recording.created_at, self.recording.id, self.client_track_id) + end + + def update_download_count(count=1) + self.download_count = self.download_count + count + self.last_downloaded_at = Time.now + end + + def delete_s3_files + s3_manager.delete(self[:url]) if self[:url] && s3_manager.exists?(self[:url]) + end + + def mark_silent + destroy + + # check if we have all the files we need, now that the recorded_backing_track is out of the way + recording.preconditions_for_mix? + end + + private + + def self.construct_filename(created_at, recording_id, client_track_id) + raise "unknown ID" unless client_track_id + "recordings/#{created_at.strftime('%m-%d-%Y')}/#{recording_id}/backing-track-#{client_track_id}.ogg" + end + end +end diff --git a/ruby/lib/jam_ruby/models/recorded_backing_track_observer.rb b/ruby/lib/jam_ruby/models/recorded_backing_track_observer.rb new file mode 100644 index 000000000..1a2b8f291 --- /dev/null +++ b/ruby/lib/jam_ruby/models/recorded_backing_track_observer.rb @@ -0,0 +1,91 @@ +module JamRuby + class RecordedBackingTrackObserver < ActiveRecord::Observer + + # if you change the this class, tests really should accompany. having alot of logic in observers is really tricky, as we do here + observe JamRuby::RecordedBackingTrack + + def before_validation(recorded_backing_tracks) + + # if we see that a part was just uploaded entirely, validate that we can find the part that was just uploaded + if recorded_backing_tracks.is_part_uploading_was && !recorded_backing_tracks.is_part_uploading + begin + aws_part = recorded_backing_tracks.s3_manager.multiple_upload_find_part(recorded_backing_tracks[:url], recorded_backing_tracks.upload_id, recorded_backing_tracks.next_part_to_upload - 1) + # calling size on a part that does not exist will throw an exception... that's what we want + aws_part.size + rescue SocketError => e + raise # this should cause a 500 error, which is what we want. The client will retry later on 500. + rescue Exception => e + recorded_backing_tracks.errors.add(:next_part_to_upload, ValidationMessages::PART_NOT_FOUND_IN_AWS) + rescue RuntimeError => e + recorded_backing_tracks.errors.add(:next_part_to_upload, ValidationMessages::PART_NOT_FOUND_IN_AWS) + rescue + recorded_backing_tracks.errors.add(:next_part_to_upload, ValidationMessages::PART_NOT_FOUND_IN_AWS) + end + + end + + # if we detect that this just became fully uploaded -- if so, tell s3 to put the parts together + if recorded_backing_tracks.marking_complete && !recorded_backing_tracks.fully_uploaded_was && recorded_backing_tracks.fully_uploaded + + multipart_success = false + begin + recorded_backing_tracks.s3_manager.multipart_upload_complete(recorded_backing_tracks[:url], recorded_backing_tracks.upload_id) + multipart_success = true + rescue SocketError => e + raise # this should cause a 500 error, which is what we want. The client will retry later. + rescue Exception => e + #recorded_track.reload + recorded_backing_tracks.reset_upload + recorded_backing_tracks.errors.add(:upload_id, ValidationMessages::BAD_UPLOAD) + end + + # unlike RecordedTracks, only the person who uploaded can download it, so no need to notify + + # tell all users that a download is available, except for the user who just uploaded + # recorded_backing_tracks.recording.users.each do |user| + #Notification.send_download_available(recorded_backing_tracks.user_id) unless user == recorded_backing_tracks.user + # end + + end + end + + def after_commit(recorded_backing_track) + + end + + # here we tick upload failure counts, or revert the state of the model, as needed + def after_rollback(recorded_backing_track) + # if fully uploaded, don't increment failures + if recorded_backing_track.fully_uploaded + return + end + + # increment part failures if there is a part currently being uploaded + if recorded_backing_track.is_part_uploading_was + #recorded_track.reload # we don't want anything else that the user set to get applied + recorded_backing_track.increment_part_failures(recorded_backing_track.part_failures_was) + if recorded_backing_track.part_failures >= APP_CONFIG.max_track_part_upload_failures + # save upload id before we abort this bad boy + upload_id = recorded_backing_track.upload_id + begin + recorded_backing_track.s3_manager.multipart_upload_abort(recorded_backing_track[:url], upload_id) + rescue => e + puts e.inspect + end + recorded_backing_track.reset_upload + if recorded_backing_track.upload_failures >= APP_CONFIG.max_track_upload_failures + # do anything? + end + end + end + + end + + def before_save(recorded_backing_track) + # if we are on the 1st part, then we need to make sure we can save the upload_id + if recorded_backing_track.next_part_to_upload == 1 + recorded_backing_track.upload_id = recorded_backing_track.s3_manager.multipart_upload_start(recorded_backing_track[:url]) + end + end + end +end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/recording.rb b/ruby/lib/jam_ruby/models/recording.rb index 49f4219f1..69d690962 100644 --- a/ruby/lib/jam_ruby/models/recording.rb +++ b/ruby/lib/jam_ruby/models/recording.rb @@ -11,6 +11,7 @@ module JamRuby has_many :quick_mixes, :class_name => "JamRuby::QuickMix", :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 :recorded_backing_tracks, :class_name => "JamRuby::RecordedBackingTrack", :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 @@ -179,6 +180,14 @@ module JamRuby recorded_tracks.where(:user_id => user.id) end + def recorded_backing_tracks_for_user(user) + unless self.users.exists?(user) + raise PermissionError, "user was not in this session" + end + recorded_backing_tracks.where(:user_id => user.id) + end + + def has_access?(user) users.exists?(user) end @@ -209,6 +218,10 @@ module JamRuby connection.video_sources.each do |video| recording.recorded_videos << RecordedVideo.create_from_video_source(video, recording) end + + connection.backing_tracks.each do |backing_track| + recording.recorded_backing_tracks << RecordedBackingTrack.create_from_backing_track(backing_track, recording) + end end end end @@ -321,8 +334,7 @@ module JamRuby } ) end - - latest_recorded_track = downloads[-1][:next] if downloads.length > 0 + latest_recorded_track = (downloads.length > 0) ? downloads[-1][:next] : 0 Mix.joins(:recording).joins(:recording => :claimed_recordings) .order('mixes.id') @@ -345,16 +357,31 @@ module JamRuby } ) end + latest_mix = (downloads.length > 0) ? downloads[-1][:next] : 0 - latest_mix = downloads[-1][:next] if downloads.length > 0 - - if !latest_mix.nil? && !latest_recorded_track.nil? - next_date = [latest_mix, latest_recorded_track].max - elsif latest_mix.nil? - next_date = latest_recorded_track - else - next_date = latest_mix + RecordedBackingTrack.joins(:recording).joins(:recording => :claimed_recordings) + .order('recorded_backing_tracks.id') + .where('recorded_backing_tracks.fully_uploaded = TRUE') + .where('recorded_backing_tracks.id > ?', since) + .where('recorded_backing_tracks.user_id = ?', user.id) # only the person who opened the backing track can have it back + .where('all_discarded = false') + .where('deleted = false') + .where('claimed_recordings.user_id = ? AND claimed_recordings.discarded = FALSE', user).limit(limit).each do |recorded_backing_track| + downloads.push( + { + :type => "recorded_backing_track", + :id => recorded_backing_track.client_track_id, + :recording_id => recorded_backing_track.recording_id, + :length => recorded_backing_track.length, + :md5 => recorded_backing_track.md5, + :url => recorded_backing_track[:url], + :next => recorded_backing_track.id + } + ) end + latest_recorded_backing_track = (downloads.length > 0) ? downloads[-1][:next] : 0 + + next_date = [latest_mix, latest_recorded_track, latest_recorded_backing_track].max if next_date.nil? next_date = since # echo back to the client the same value they passed in, if there are no results @@ -417,6 +444,20 @@ module JamRuby Arel::Nodes::As.new('stream_mix', Arel.sql('item_type')) ]).reorder("") + # Select fields for quick mix. Note that it must include + # the same number of fields as the track or video in order for + # the union to work: + backing_track_arel = RecordedBackingTrack.select([ + :id, + :recording_id, + :user_id, + :url, + :fully_uploaded, + :upload_failures, + :client_track_id, + Arel::Nodes::As.new('backing_track', Arel.sql('item_type')) + ]).reorder("") + # Glue them together: union = track_arel.union(vid_arel) @@ -439,7 +480,25 @@ module JamRuby ]) # And repeat: - union_all = arel.union(quick_mix_arel) + union_quick = arel.union(quick_mix_arel) + utable_quick = Arel::Nodes::TableAlias.new(union_quick, :recorded_items_quick) + arel = arel.from(utable_quick) + + arel = arel.except(:select) + arel = arel.select([ + "recorded_items_quick.id", + :recording_id, + :user_id, + :url, + :fully_uploaded, + :upload_failures, + :client_track_id, + :item_type + ]) + + + # And repeat for backing track: + union_all = arel.union(backing_track_arel) utable_all = Arel::Nodes::TableAlias.new(union_all, :recorded_items_all) arel = arel.from(utable_all) @@ -455,7 +514,6 @@ module JamRuby :item_type ]) - # Further joining and criteria for the unioned object: arel = arel.joins("INNER JOIN recordings ON recordings.id=recorded_items_all.recording_id") \ .where('recorded_items_all.user_id' => user.id) \ @@ -492,6 +550,13 @@ module JamRuby :recording_id => recorded_item.recording_id, :next => recorded_item.id }) + elsif recorded_item.item_type == 'backing_track' + uploads << ({ + :type => "recorded_backing_track", + :recording_id => recorded_item.recording_id, + :client_track_id => recorded_item.client_track_id, + :next => recorded_item.id + }) else end @@ -513,6 +578,11 @@ module JamRuby recorded_tracks.each do |recorded_track| return false unless recorded_track.fully_uploaded end + + recorded_backing_tracks.each do |recorded_backing_track| + return false unless recorded_backing_track.fully_uploaded + end + true end diff --git a/ruby/lib/jam_ruby/models/track.rb b/ruby/lib/jam_ruby/models/track.rb index 10b7ecb40..c06ad2d7a 100644 --- a/ruby/lib/jam_ruby/models/track.rb +++ b/ruby/lib/jam_ruby/models/track.rb @@ -55,11 +55,64 @@ module JamRuby return query end + def self.diff_track(track_class, existing_tracks, new_tracks, &blk) + result = [] + if new_tracks.length == 0 + existing_tracks.delete_all + else + + # we will prune from this as we find matching tracks + to_delete = Set.new(existing_tracks) + to_add = Array.new(new_tracks) + + existing_tracks.each do |existing_track| + new_tracks.each do |new_track| + + if new_track[:id] == existing_track.id || new_track[:client_track_id] == existing_track.client_track_id + to_delete.delete(existing_track) + to_add.delete(new_track) + + blk.call(existing_track, new_track) + + result.push(existing_track) + + if existing_track.save + next + else + result = existing_track + raise ActiveRecord::Rollback + end + end + end + end + + + to_add.each do |new_track| + existing_track = track_class.new + + blk.call(existing_track, new_track) + + if existing_track.save + result.push(existing_track) + else + result = existing_track + raise ActiveRecord::Rollback + end + end + + to_delete.each do |delete_me| + delete_me.delete + end + end + result + end # this is a bit different from a normal track synchronization in that the client just sends up all tracks, # ... some may already exist - def self.sync(clientId, tracks) - result = [] + def self.sync(clientId, tracks, backing_tracks = []) + result = {} + + backing_tracks = [] unless backing_tracks Track.transaction do connection = Connection.find_by_client_id!(clientId) @@ -68,67 +121,28 @@ module JamRuby msh = MusicSessionUserHistory.find_by_client_id!(clientId) instruments = [] - if tracks.length == 0 - connection.tracks.delete_all - else - connection_tracks = connection.tracks + tracks.each do |track| + instruments << track[:instrument_id] + end - # we will prune from this as we find matching tracks - to_delete = Set.new(connection_tracks) - to_add = Array.new(tracks) + result[:tracks] = diff_track(Track, connection.tracks, tracks) do |track_record, track_info| + track_record.connection = connection + track_record.client_track_id = track_info[:client_track_id] + track_record.client_resource_id = track_info[:client_resource_id] + track_record.instrument_id = track_info[:instrument_id] + track_record.sound = track_info[:sound] + end - tracks.each do |track| - instruments << track[:instrument_id] - end + result[:backing_tracks] = diff_track(BackingTrack, connection.backing_tracks, backing_tracks) do |track_record, track_info| + track_record.connection = connection + track_record.client_track_id = track_info[:client_track_id] + track_record.client_resource_id = track_info[:client_resource_id] + track_record.filename = track_info[:filename] + end - connection_tracks.each do |connection_track| - tracks.each do |track| - - if track[:id] == connection_track.id || track[:client_track_id] == connection_track.client_track_id - to_delete.delete(connection_track) - to_add.delete(track) - # don't update connection_id or client_id; it's unknown what would happen if these changed mid-session - connection_track.instrument_id = track[:instrument_id] - connection_track.sound = track[:sound] - connection_track.client_track_id = track[:client_track_id] - connection_track.client_resource_id = track[:client_resource_id] - - result.push(connection_track) - - if connection_track.save - next - else - result = connection_track - raise ActiveRecord::Rollback - end - - end - end - end - - msh.instruments = instruments.join("|") - if !msh.save - raise ActiveRecord::Rollback - end - - to_add.each do |track| - connection_track = Track.new - connection_track.connection = connection - connection_track.instrument_id = track[:instrument_id] - connection_track.sound = track[:sound] - connection_track.client_track_id = track[:client_track_id] - connection_track.client_resource_id = track[:client_resource_id] - if connection_track.save - result.push(connection_track) - else - result = connection_track - raise ActiveRecord::Rollback - end - end - - to_delete.each do |delete_me| - delete_me.delete - end + msh.instruments = instruments.join("|") + if !msh.save + raise ActiveRecord::Rollback end end diff --git a/ruby/lib/jam_ruby/models/user.rb b/ruby/lib/jam_ruby/models/user.rb index c59021903..3bb4c270a 100644 --- a/ruby/lib/jam_ruby/models/user.rb +++ b/ruby/lib/jam_ruby/models/user.rb @@ -122,6 +122,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 + has_many :recorded_backing_tracks, :foreign_key => "user_id", :class_name => "JamRuby::RecordedBackingTrack", :inverse_of => :user has_many :quick_mixes, :foreign_key => "user_id", :class_name => "JamRuby::QuickMix", :inverse_of => :user # invited users @@ -342,6 +343,11 @@ module JamRuby self.recordings.size end + def age + now = Time.now.utc.to_date + self.birth_date.nil? ? "unspecified" : now.year - self.birth_date.year - (self.birth_date.to_date.change(:year => now.year) > now ? 1 : 0) + end + def session_count MusicSession.where("user_id = ? AND started_at IS NOT NULL", self.id).size end diff --git a/ruby/lib/jam_ruby/models/user_sync.rb b/ruby/lib/jam_ruby/models/user_sync.rb index 8cdeac13b..1e915b953 100644 --- a/ruby/lib/jam_ruby/models/user_sync.rb +++ b/ruby/lib/jam_ruby/models/user_sync.rb @@ -4,6 +4,7 @@ module JamRuby belongs_to :recorded_track belongs_to :mix belongs_to :quick_mix + belongs_to :recorded_backing_track def self.show(id, user_id) self.index({user_id: user_id, id: id, limit: 1, offset: 0})[:query].first @@ -22,7 +23,7 @@ module JamRuby raise 'no user id specified' if user_id.blank? query = UserSync - .includes(recorded_track: [{recording: [:owner, {claimed_recordings: [:share_token]}, {recorded_tracks: [:user]}, {comments:[:user]}, :likes, :plays, :mixes]}, user: [], instrument:[]], mix: [], quick_mix:[]) + .includes(recorded_track: [{recording: [:owner, {claimed_recordings: [:share_token]}, {recorded_tracks: [:user]}, {comments:[:user]}, :likes, :plays, :mixes]}, user: [], instrument:[]], mix: [], quick_mix:[], recorded_backing_track:[]) .joins("LEFT OUTER JOIN claimed_recordings ON claimed_recordings.user_id = user_syncs.user_id AND claimed_recordings.recording_id = user_syncs.recording_id") .where(user_id: user_id) .where(%Q{ diff --git a/ruby/lib/jam_ruby/resque/resque_hooks.rb b/ruby/lib/jam_ruby/resque/resque_hooks.rb index e58298569..be09d0925 100644 --- a/ruby/lib/jam_ruby/resque/resque_hooks.rb +++ b/ruby/lib/jam_ruby/resque/resque_hooks.rb @@ -35,7 +35,6 @@ Resque.before_first_fork do end JamRuby::Stats.init(config) - end # https://devcenter.heroku.com/articles/forked-pg-connections Resque.before_fork do diff --git a/ruby/lib/jam_ruby/resque/stress_job.rb b/ruby/lib/jam_ruby/resque/stress_job.rb new file mode 100644 index 000000000..b5004dd11 --- /dev/null +++ b/ruby/lib/jam_ruby/resque/stress_job.rb @@ -0,0 +1,29 @@ +require 'resque' + +module JamRuby + + # this job exists as a way to manually test a bunch of jobs firing at once. It's not a real job. + class StressJob + extend JamRuby::ResqueStats + + @queue = :stress_job + + @@log = Logging.logger[StressJob] + + + def self.perform + + @@log.debug("STARTING") + 100.times do + user = User.first.id + diagnostic = Diagnostic.first.user_id + count = Diagnostic.all.count + end + @@log.debug("ENDING") + + end + end + +end + + diff --git a/ruby/spec/factories.rb b/ruby/spec/factories.rb index bd52cc17f..42418a699 100644 --- a/ruby/spec/factories.rb +++ b/ruby/spec/factories.rb @@ -232,6 +232,11 @@ FactoryGirl.define do sequence(:client_resource_id) { |n| "resource_id#{n}"} end + factory :backing_track, :class => JamRuby::BackingTrack do + sequence(:client_track_id) { |n| "client_track_id#{n}"} + filename 'foo.mp3' + end + factory :video_source, :class => JamRuby::VideoSource do #client_video_source_id "test_source_id" sequence(:client_video_source_id) { |n| "client_video_source_id#{n}"} @@ -250,6 +255,20 @@ FactoryGirl.define do association :recording, factory: :recording end + factory :recorded_backing_track, :class => JamRuby::RecordedBackingTrack do + sequence(:client_id) { |n| "client_id-#{n}"} + sequence(:backing_track_id) { |n| "track_id-#{n}"} + sequence(:client_track_id) { |n| "client_track_id-#{n}"} + sequence(:filename) { |n| "filename-{#n}"} + sequence(:url) { |n| "/recordings/blah/#{n}"} + md5 'abc' + length 1 + fully_uploaded true + association :user, factory: :user + association :recording, factory: :recording + end + + factory :recorded_video, :class => JamRuby::RecordedVideo do sequence(:client_video_source_id) { |n| "client_video_source_id-#{n}"} fully_uploaded true @@ -700,7 +719,6 @@ FactoryGirl.define do factory :jam_track, :class => JamRuby::JamTrack do sequence(:name) { |n| "jam-track-#{n}" } sequence(:description) { |n| "description-#{n}" } - bpm 100.1 time_signature '4/4' status 'Production' recording_type 'Cover' 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 392d70473..07d6f49a5 100644 --- a/ruby/spec/jam_ruby/models/active_music_session_spec.rb +++ b/ruby/spec/jam_ruby/models/active_music_session_spec.rb @@ -745,6 +745,29 @@ describe ActiveMusicSession do @music_session.errors[:claimed_recording] == [ValidationMessages::JAM_TRACK_ALREADY_OPEN] end + + it "disallow a claimed recording to be started when backing track is open" do + # open the backing track + @backing_track = "foo.mp3" + @music_session.open_backing_track(@user1, @backing_track) + @music_session.errors.any?.should be_false + + # and try to open a recording for playback + @music_session.claimed_recording_start(@user1, @claimed_recording) + @music_session.errors.any?.should be_true + @music_session.errors[:claimed_recording] == [ValidationMessages::BACKING_TRACK_ALREADY_OPEN] + end + + it "disallow a claimed recording to be started when metronome is open" do + # open the metronome + @music_session.open_metronome(@user1) + @music_session.errors.any?.should be_false + + # and try to open a recording for playback + @music_session.claimed_recording_start(@user1, @claimed_recording) + @music_session.errors.any?.should be_true + @music_session.errors[:claimed_recording] == [ValidationMessages::METRONOME_ALREADY_OPEN] + end end end @@ -830,5 +853,143 @@ describe ActiveMusicSession do music_sessions[0].connections[0].tracks.should have(1).items end end + + describe "open_backing_track" do + before(:each) do + @user1 = FactoryGirl.create(:user) + @connection = FactoryGirl.create(:connection, :user => @user1) + @instrument = FactoryGirl.create(:instrument, :description => 'a great instrument') + @track = FactoryGirl.create(:track, :connection => @connection, :instrument => @instrument) + @music_session = FactoryGirl.create(:active_music_session, :creator => @user1, :musician_access => true) + # @music_session.connections << @connection + @music_session.save! + @connection.join_the_session(@music_session, true, nil, @user1, 10) + @backing_track = "foo/bar.mp3" + end + + it "allow a backing track to be associated" do + # simple success case; just open the backing track and observe the state of the session is correct + @music_session.open_backing_track(@user1, @backing_track) + @music_session.errors.any?.should be_false + @music_session.reload + @music_session.backing_track_path.should == @backing_track + @music_session.backing_track_initiator.should == @user1 + end + + it "allow a backing track to be closed" do + # simple success case; close an opened backing track and observe the state of the session is correct + @music_session.open_backing_track(@user1, @backing_track) + @music_session.errors.any?.should be_false + @music_session.close_backing_track + @music_session.errors.any?.should be_false + @music_session.reload + @music_session.backing_track_path.should be_nil + @music_session.backing_track_initiator.should be_nil + end + + it "disallow a backing track to be opened when another is already opened" do + # if a backing track is open, don't allow another to be opened + @music_session.open_backing_track(@user1, @backing_track) + @music_session.errors.any?.should be_false + @music_session.open_backing_track(@user1, @backing_track) + @music_session.errors.any?.should be_true + @music_session.errors[:backing_track] == [ValidationMessages::BACKING_TRACK_ALREADY_OPEN] + end + + it "disallow a backing track to be opened when recording is ongoing" do + @recording = Recording.start(@music_session, @user1) + @music_session.errors.any?.should be_false + @music_session.open_backing_track(@user1, @backing_track) + @music_session.errors.any?.should be_true + @music_session.errors[:backing_track] == [ValidationMessages::RECORDING_ALREADY_IN_PROGRESS] + end + + it "disallow a backing track to be opened when recording is playing back" do + # create a recording, and open it for play back + @recording = Recording.start(@music_session, @user1) + @recording.errors.any?.should be_false + @recording.stop + @recording.reload + @claimed_recording = @recording.claim(@user1, "name", "description", Genre.first, true) + @claimed_recording.errors.any?.should be_false + @music_session.claimed_recording_start(@user1, @claimed_recording) + @music_session.errors.any?.should be_false + + # while it's open, try to open a jam track + @music_session.open_backing_track(@user1, @backing_track) + @music_session.errors.any?.should be_true + @music_session.errors[:backing_track] == [ValidationMessages::CLAIMED_RECORDING_ALREADY_IN_PROGRESS] + end + + end + + describe "open_metronome" do + before(:each) do + @user1 = FactoryGirl.create(:user) + @connection = FactoryGirl.create(:connection, :user => @user1) + @instrument = FactoryGirl.create(:instrument, :description => 'a great instrument') + @track = FactoryGirl.create(:track, :connection => @connection, :instrument => @instrument) + @music_session = FactoryGirl.create(:active_music_session, :creator => @user1, :musician_access => true) + # @music_session.connections << @connection + @music_session.save! + @connection.join_the_session(@music_session, true, nil, @user1, 10) + end + + it "allow a metronome to be activated" do + # simple success case; just open the metronome and observe the state of the session is correct + @music_session.open_metronome(@user1) + @music_session.errors.any?.should be_false + @music_session.reload + @music_session.metronome_active.should == true + @music_session.metronome_initiator.should == @user1 + end + + it "allow a metronome to be closed" do + # simple success case; close an opened metronome and observe the state of the session is correct + @music_session.open_metronome(@user1) + @music_session.errors.any?.should be_false + @music_session.close_metronome + @music_session.errors.any?.should be_false + @music_session.reload + @music_session.metronome_active.should be_false + @music_session.metronome_initiator.should be_nil + end + + it "disallow a metronome to be opened when another is already opened" do + # if a metronome is open, don't allow another to be opened + @music_session.open_metronome(@user1) + @music_session.errors.any?.should be_false + @music_session.open_metronome(@user1) + @music_session.errors.any?.should be_true + @music_session.errors[:metronome] == [ValidationMessages::METRONOME_ALREADY_OPEN] + end + + it "disallow a metronome to be opened when recording is ongoing" do + @recording = Recording.start(@music_session, @user1) + @music_session.errors.any?.should be_false + @music_session.open_metronome(@user1) + @music_session.errors.any?.should be_true + @music_session.errors[:metronome] == [ValidationMessages::RECORDING_ALREADY_IN_PROGRESS] + end + + it "disallow a metronome to be opened when recording is playing back" do + # create a recording, and open it for play back + @recording = Recording.start(@music_session, @user1) + @recording.errors.any?.should be_false + @recording.stop + @recording.reload + @claimed_recording = @recording.claim(@user1, "name", "description", Genre.first, true) + @claimed_recording.errors.any?.should be_false + @music_session.claimed_recording_start(@user1, @claimed_recording) + @music_session.errors.any?.should be_false + + # while it's open, try to open a jam track + @music_session.open_metronome(@user1) + @music_session.errors.any?.should be_true + @music_session.errors[:metronome] == [ValidationMessages::CLAIMED_RECORDING_ALREADY_IN_PROGRESS] + end + + end + end diff --git a/ruby/spec/jam_ruby/models/jam_track_spec.rb b/ruby/spec/jam_ruby/models/jam_track_spec.rb index 0c3ab5936..af4980f7b 100644 --- a/ruby/spec/jam_ruby/models/jam_track_spec.rb +++ b/ruby/spec/jam_ruby/models/jam_track_spec.rb @@ -55,25 +55,6 @@ describe JamTrack do end describe "validations" do - describe "bpm" do - it "1" do - FactoryGirl.build(:jam_track, bpm: 1).valid?.should be_true - end - - it "100" do - FactoryGirl.build(:jam_track, bpm: 100).valid?.should be_true - end - - it "100.1" do - FactoryGirl.build(:jam_track, bpm: 100.1).valid?.should be_true - end - - it "100.12" do - jam_track = FactoryGirl.build(:jam_track, bpm: 100.12) - jam_track.valid?.should be_false - jam_track.errors[:bpm].should == ['is invalid'] - end - end describe "price" do diff --git a/ruby/spec/jam_ruby/models/recorded_backing_track_spec.rb b/ruby/spec/jam_ruby/models/recorded_backing_track_spec.rb new file mode 100644 index 000000000..de65e22e1 --- /dev/null +++ b/ruby/spec/jam_ruby/models/recorded_backing_track_spec.rb @@ -0,0 +1,228 @@ +require 'spec_helper' +require 'rest-client' + +describe RecordedBackingTrack do + + include UsesTempFiles + + before do + @user = FactoryGirl.create(:user) + @connection = FactoryGirl.create(:connection, :user => @user) + @instrument = FactoryGirl.create(:instrument, :description => 'a great instrument') + @music_session = FactoryGirl.create(:active_music_session, :creator => @user, :musician_access => true) + @track = FactoryGirl.create(:track, :connection => @connection, :instrument => @instrument) + @backing_track = FactoryGirl.create(:backing_track, :connection => @connection) + @recording = FactoryGirl.create(:recording, :music_session => @music_session, :owner => @user) + end + + it "should copy from a regular track properly" do + @recorded_backing_track = RecordedBackingTrack.create_from_backing_track(@backing_track, @recording) + + @recorded_backing_track.user.id.should == @backing_track.connection.user.id + @recorded_backing_track.filename.should == @backing_track.filename + @recorded_backing_track.next_part_to_upload.should == 0 + @recorded_backing_track.fully_uploaded.should == false + @recorded_backing_track.client_id = @connection.client_id + @recorded_backing_track.backing_track_id = @backing_track.id + end + + it "should update the next part to upload properly" do + @recorded_backing_track = RecordedBackingTrack.create_from_backing_track(@backing_track, @recording) + @recorded_backing_track.upload_part_complete(1, 1000) + @recorded_backing_track.errors.any?.should be_true + @recorded_backing_track.errors[:length][0].should == "is too short (minimum is 1 characters)" + @recorded_backing_track.errors[:md5][0].should == "can't be blank" + end + + it "properly finds a recorded track given its upload filename" do + @recorded_backing_track = RecordedBackingTrack.create_from_backing_track(@backing_track, @recording) + @recorded_backing_track.save.should be_true + RecordedBackingTrack.find_by_recording_id_and_backing_track_id(@recorded_backing_track.recording_id, @recorded_backing_track.backing_track_id).should == @recorded_backing_track + end + + it "gets a url for the track" do + @recorded_backing_track = RecordedBackingTrack.create_from_backing_track(@backing_track, @recording) + @recorded_backing_track.errors.any?.should be_false + @recorded_backing_track[:url].should == "recordings/#{@recorded_backing_track.created_at.strftime('%m-%d-%Y')}/#{@recording.id}/backing-track-#{@backing_track.client_track_id}.ogg" + end + + it "signs url" do + stub_const("APP_CONFIG", app_config) + @recorded_backing_track = RecordedBackingTrack.create_from_backing_track(@backing_track, @recording) + @recorded_backing_track.sign_url.should_not be_nil + end + + it "can not be downloaded if no claimed recording" do + user2 = FactoryGirl.create(:user) + @recorded_backing_track = RecordedBackingTrack.create_from_backing_track(@backing_track, @recording) + @recorded_backing_track.can_download?(user2).should be_false + @recorded_backing_track.can_download?(@user).should be_false + end + + it "can be downloaded if there is a claimed recording" do + @recorded_track = RecordedTrack.create_from_track(@track, @recording) + @recorded_backing_track = RecordedBackingTrack.create_from_backing_track(@backing_track, @recording) + @recording.claim(@user, "my recording", "my description", Genre.first, true).errors.any?.should be_false + @recorded_backing_track.can_download?(@user).should be_true + end + + + describe "aws-based operations", :aws => true do + + def put_file_to_aws(signed_data, contents) + + begin + RestClient.put( signed_data[:url], + contents, + { + :'Content-Type' => 'audio/ogg', + :Date => signed_data[:datetime], + :'Content-MD5' => signed_data[:md5], + :Authorization => signed_data[:authorization] + }) + rescue => e + puts e.response + raise e + end + + end + # create a test file + upload_file='some_file.ogg' + in_directory_with_file(upload_file) + + upload_file_contents="ogg binary stuff in here" + md5 = Base64.encode64(Digest::MD5.digest(upload_file_contents)).chomp + test_config = app_config + s3_manager = S3Manager.new(test_config.aws_bucket, test_config.aws_access_key_id, test_config.aws_secret_access_key) + + + before do + stub_const("APP_CONFIG", app_config) + # this block of code will fully upload a sample file to s3 + content_for_file(upload_file_contents) + s3_manager.delete_folder('recordings') # keep the bucket clean to save cost, and make it easier if post-mortuem debugging + + + end + + it "cant mark a part complete without having started it" do + @recorded_backing_track = RecordedBackingTrack.create_from_backing_track(@backing_track, @recording) + @recorded_backing_track.upload_start(1000, "abc") + @recorded_backing_track.upload_part_complete(1, 1000) + @recorded_backing_track.errors.any?.should be_true + @recorded_backing_track.errors[:next_part_to_upload][0].should == ValidationMessages::PART_NOT_STARTED + end + + it "no parts" do + @recorded_backing_track = RecordedBackingTrack.create_from_backing_track(@backing_track, @recording) + @recorded_backing_track.upload_start(1000, "abc") + @recorded_backing_track.upload_next_part(1000, "abc") + @recorded_backing_track.errors.any?.should be_false + @recorded_backing_track.upload_part_complete(1, 1000) + @recorded_backing_track.errors.any?.should be_true + @recorded_backing_track.errors[:next_part_to_upload][0].should == ValidationMessages::PART_NOT_FOUND_IN_AWS + end + + it "enough part failures reset the upload" do + @recorded_backing_track = RecordedBackingTrack.create_from_backing_track(@backing_track, @recording) + @recorded_backing_track.upload_start(File.size(upload_file), md5) + @recorded_backing_track.upload_next_part(File.size(upload_file), md5) + @recorded_backing_track.errors.any?.should be_false + APP_CONFIG.max_track_part_upload_failures.times do |i| + @recorded_backing_track.upload_part_complete(@recorded_backing_track.next_part_to_upload, File.size(upload_file)) + @recorded_backing_track.errors[:next_part_to_upload] == [ValidationMessages::PART_NOT_FOUND_IN_AWS] + part_failure_rollover = i == APP_CONFIG.max_track_part_upload_failures - 1 + expected_is_part_uploading = !part_failure_rollover + expected_part_failures = part_failure_rollover ? 0 : i + 1 + @recorded_backing_track.reload + @recorded_backing_track.is_part_uploading.should == expected_is_part_uploading + @recorded_backing_track.part_failures.should == expected_part_failures + end + + @recorded_backing_track.reload + @recorded_backing_track.upload_failures.should == 1 + @recorded_backing_track.file_offset.should == 0 + @recorded_backing_track.next_part_to_upload.should == 0 + @recorded_backing_track.upload_id.should be_nil + @recorded_backing_track.md5.should be_nil + @recorded_backing_track.length.should == 0 + end + + it "enough upload failures fails the upload forever" do + APP_CONFIG.stub(:max_track_upload_failures).and_return(1) + APP_CONFIG.stub(:max_track_part_upload_failures).and_return(2) + @recorded_backing_track = RecordedBackingTrack.create_from_backing_track(@backing_track, @recording) + APP_CONFIG.max_track_upload_failures.times do |j| + @recorded_backing_track.upload_start(File.size(upload_file), md5) + @recorded_backing_track.upload_next_part(File.size(upload_file), md5) + @recorded_backing_track.errors.any?.should be_false + APP_CONFIG.max_track_part_upload_failures.times do |i| + @recorded_backing_track.upload_part_complete(@recorded_backing_track.next_part_to_upload, File.size(upload_file)) + @recorded_backing_track.errors[:next_part_to_upload] == [ValidationMessages::PART_NOT_FOUND_IN_AWS] + part_failure_rollover = i == APP_CONFIG.max_track_part_upload_failures - 1 + expected_is_part_uploading = part_failure_rollover ? false : true + expected_part_failures = part_failure_rollover ? 0 : i + 1 + @recorded_backing_track.reload + @recorded_backing_track.is_part_uploading.should == expected_is_part_uploading + @recorded_backing_track.part_failures.should == expected_part_failures + end + @recorded_backing_track.upload_failures.should == j + 1 + end + + @recorded_backing_track.reload + @recorded_backing_track.upload_failures.should == APP_CONFIG.max_track_upload_failures + @recorded_backing_track.file_offset.should == 0 + @recorded_backing_track.next_part_to_upload.should == 0 + @recorded_backing_track.upload_id.should be_nil + @recorded_backing_track.md5.should be_nil + @recorded_backing_track.length.should == 0 + + # try to poke it and get the right kind of error back + @recorded_backing_track.upload_next_part(File.size(upload_file), md5) + @recorded_backing_track.errors[:upload_failures] = [ValidationMessages::UPLOAD_FAILURES_EXCEEDED] + end + + describe "correctly uploaded a file" do + + before do + @recorded_backing_track = RecordedBackingTrack.create_from_backing_track(@backing_track, @recording) + @recorded_backing_track.upload_start(File.size(upload_file), md5) + @recorded_backing_track.upload_next_part(File.size(upload_file), md5) + signed_data = @recorded_backing_track.upload_sign(md5) + @response = put_file_to_aws(signed_data, upload_file_contents) + @recorded_backing_track.upload_part_complete(@recorded_backing_track.next_part_to_upload, File.size(upload_file)) + @recorded_backing_track.errors.any?.should be_false + @recorded_backing_track.upload_complete + @recorded_backing_track.errors.any?.should be_false + @recorded_backing_track.marking_complete = false + end + + it "can download an updated file" do + @response = RestClient.get @recorded_backing_track.sign_url + @response.body.should == upload_file_contents + end + + it "can't mark completely uploaded twice" do + @recorded_backing_track.upload_complete + @recorded_backing_track.errors.any?.should be_true + @recorded_backing_track.errors[:fully_uploaded][0].should == "already set" + @recorded_backing_track.part_failures.should == 0 + end + + it "can't ask for a next part if fully uploaded" do + @recorded_backing_track.upload_next_part(File.size(upload_file), md5) + @recorded_backing_track.errors.any?.should be_true + @recorded_backing_track.errors[:fully_uploaded][0].should == "already set" + @recorded_backing_track.part_failures.should == 0 + end + + it "can't ask for mark part complete if fully uploaded" do + @recorded_backing_track.upload_part_complete(1, 1000) + @recorded_backing_track.errors.any?.should be_true + @recorded_backing_track.errors[:fully_uploaded][0].should == "already set" + @recorded_backing_track.part_failures.should == 0 + end + end + end +end + diff --git a/ruby/spec/jam_ruby/models/recording_spec.rb b/ruby/spec/jam_ruby/models/recording_spec.rb index 303080174..d2fd7c3b0 100644 --- a/ruby/spec/jam_ruby/models/recording_spec.rb +++ b/ruby/spec/jam_ruby/models/recording_spec.rb @@ -211,6 +211,20 @@ describe Recording do user1_recorded_tracks[0].discard = true user1_recorded_tracks[0].save! end + + it "should allow finding of backing tracks" do + user2 = FactoryGirl.create(:user) + connection2 = FactoryGirl.create(:connection, :user => user2, :music_session => @music_session) + track2 = FactoryGirl.create(:track, :connection => connection2, :instrument => @instrument) + backing_track = FactoryGirl.create(:backing_track, :connection => connection2) + + + @recording = Recording.start(@music_session, @user) + @recording.recorded_backing_tracks_for_user(@user).length.should eq(0) + user2_recorded_tracks = @recording.recorded_backing_tracks_for_user(user2) + user2_recorded_tracks.length.should == 1 + user2_recorded_tracks[0].should == user2.recorded_backing_tracks[0] + end it "should set up the recording properly when recording is started with 1 user in the session" do @music_session.is_recording?.should be_false @@ -547,6 +561,8 @@ describe Recording do @genre = FactoryGirl.create(:genre) @recording.claim(@user, "Recording", "Recording Description", @genre, true) + @backing_track = FactoryGirl.create(:backing_track, :connection => @connection) + # We should have 2 items; a track and a video: uploads = Recording.list_uploads(@user) uploads["uploads"].should have(3).items diff --git a/ruby/spec/jam_ruby/models/score_spec.rb b/ruby/spec/jam_ruby/models/score_spec.rb index 0e87f433d..b7daca9d7 100644 --- a/ruby/spec/jam_ruby/models/score_spec.rb +++ b/ruby/spec/jam_ruby/models/score_spec.rb @@ -522,29 +522,30 @@ describe Score do it "works" do Score.createx(LOCA, NODEA, ADDRA, LOCB, NODEB, ADDRB, 20, nil) Score.count.should == 2 - Score.connection.execute("SELECT discard_scores()").check + Score.connection.execute("SELECT discard_scores(5)").check Score.count.should == 2 end it "discards over 5 items" do - Score.createx(LOCA, NODEA, ADDRA, LOCB, NODEB, ADDRB, 20, nil) - Score.createx(LOCA, NODEA, ADDRA, LOCB, NODEB, ADDRB, 20, nil) - Score.createx(LOCA, NODEA, ADDRA, LOCB, NODEB, ADDRB, 20, nil) - Score.createx(LOCA, NODEA, ADDRA, LOCB, NODEB, ADDRB, 20, nil) - Score.createx(LOCA, NODEA, ADDRA, LOCB, NODEB, ADDRB, 20, nil) - Score.createx(LOCA, NODEA, ADDRA, LOCB, NODEB, ADDRB, 20, nil) + Score.createx(LOCA, NODEA, ADDRA, LOCB, NODEB, ADDRB, 20, 6.days.ago) + Score.createx(LOCA, NODEA, ADDRA, LOCB, NODEB, ADDRB, 20, 5.days.ago) + Score.createx(LOCA, NODEA, ADDRA, LOCB, NODEB, ADDRB, 20, 4.days.ago) + Score.createx(LOCA, NODEA, ADDRA, LOCB, NODEB, ADDRB, 20, 3.days.ago) + Score.createx(LOCA, NODEA, ADDRA, LOCB, NODEB, ADDRB, 20, 2.days.ago) - Score.count.should == 12 - Score.connection.execute("SELECT discard_scores()").check - Score.count.should == 12 + Score.count.should == 10 - Score.createx(LOCA, NODEA, ADDRA, LOCB, NODEB, ADDRB, 26, nil) - Score.connection.execute("UPDATE scores set created_at = TIMESTAMP '#{2.days.ago}' WHERE score = 26").cmdtuples.should == 2 - Score.connection.execute("SELECT discard_scores()").check - Score.count.should == 12 - Score.connection.execute("SELECT * FROM scores WHERE score = 20").ntuples.should == 12 - Score.connection.execute("SELECT * FROM scores WHERE scorer = 0").ntuples.should == 6 - Score.connection.execute("SELECT * FROM scores WHERE scorer = 1").ntuples.should == 6 + Score.createx(LOCA, NODEA, ADDRA, LOCB, NODEB, ADDRB, 20, 1.days.ago) + + Score.count.should == 10 + + # make a score older than all the rest; it should get whacked + Score.createx(LOCA, NODEA, ADDRA, LOCB, NODEB, ADDRB, 26, 7.days.ago) + + Score.count.should == 10 + Score.connection.execute("SELECT * FROM scores WHERE score = 20").ntuples.should == 10 + Score.connection.execute("SELECT * FROM scores WHERE scorer = 0").ntuples.should == 5 + Score.connection.execute("SELECT * FROM scores WHERE scorer = 1").ntuples.should == 5 Score.createx(LOCB, NODEB, ADDRB, LOCA, NODEA, ADDRA, 22, nil) @@ -554,18 +555,35 @@ describe Score do Score.createx(LOCB, NODEB, ADDRB, LOCA, NODEA, ADDRA, 22, nil) Score.createx(LOCB, NODEB, ADDRB, LOCA, NODEA, ADDRA, 22, nil) - Score.count.should == 24 - Score.connection.execute("SELECT discard_scores()").check - Score.count.should == 24 + Score.count.should == 20 - Score.createx(LOCB, NODEB, ADDRB, LOCA, NODEA, ADDRA, 36, nil) - Score.connection.execute("UPDATE scores set created_at = TIMESTAMP '#{2.days.ago}' WHERE score = 36").cmdtuples.should == 2 - Score.connection.execute("SELECT discard_scores()").check - Score.count.should == 24 - Score.connection.execute("SELECT * FROM scores WHERE score = 22").ntuples.should == 12 - Score.connection.execute("SELECT * FROM scores WHERE score = 22 AND scorer = 0").ntuples.should == 6 - Score.connection.execute("SELECT * FROM scores WHERE score = 22 AND scorer = 1").ntuples.should == 6 + Score.connection.execute("SELECT * FROM scores WHERE score = 22").ntuples.should == 10 + Score.connection.execute("SELECT * FROM scores WHERE score = 20").ntuples.should == 10 + Score.connection.execute("SELECT * FROM scores WHERE scorer = 0").ntuples.should == 10 + Score.connection.execute("SELECT * FROM scores WHERE scorer = 1").ntuples.should == 10 + Score.createx(LOCB, NODEB, ADDRB, LOCA, NODEA, ADDRA, 36, 7.days.ago) + Score.count.should == 20 + Score.connection.execute("SELECT * FROM scores WHERE score = 22").ntuples.should == 10 + Score.connection.execute("SELECT * FROM scores WHERE score = 20").ntuples.should == 10 + Score.connection.execute("SELECT * FROM scores WHERE scorer = 0").ntuples.should == 10 + Score.connection.execute("SELECT * FROM scores WHERE scorer = 1").ntuples.should == 10 + + # let's create scores between a new location, and make sure they don't distrurb the data we have now + Score.createx(LOCC, NODEC, ADDRC, LOCA, NODEA, ADDRA, 10, nil) + + Score.count.should == 22 + + Score.createx(LOCC, NODEC, ADDRC, LOCA, NODEA, ADDRA, 10, nil) + Score.createx(LOCC, NODEC, ADDRC, LOCA, NODEA, ADDRA, 10, nil) + Score.createx(LOCC, NODEC, ADDRC, LOCA, NODEA, ADDRA, 10, nil) + Score.createx(LOCC, NODEC, ADDRC, LOCA, NODEA, ADDRA, 10, nil) + + Score.count.should == 30 + + Score.connection.execute("SELECT * FROM scores WHERE score = 20").ntuples.should == 10 + Score.connection.execute("SELECT * FROM scores WHERE score = 22").ntuples.should == 10 + Score.connection.execute("SELECT * FROM scores WHERE score = 10").ntuples.should == 10 end end end diff --git a/ruby/spec/jam_ruby/models/track_spec.rb b/ruby/spec/jam_ruby/models/track_spec.rb index 1e03d123b..4b27ad062 100644 --- a/ruby/spec/jam_ruby/models/track_spec.rb +++ b/ruby/spec/jam_ruby/models/track_spec.rb @@ -7,8 +7,10 @@ describe Track do let (:connection) { FactoryGirl.create(:connection, :user => user, :music_session => music_session) } let (:track) { FactoryGirl.create(:track, :connection => connection)} let (:track2) { FactoryGirl.create(:track, :connection => connection)} + let (:backing_track) { FactoryGirl.create(:backing_track, :connection => connection)} let (:msuh) {FactoryGirl.create(:music_session_user_history, :history => music_session.music_session, :user => user, :client_id => connection.client_id) } let (:track_hash) { {:client_track_id => 'client_guid', :sound => 'stereo', :instrument_id => 'drums'} } + let (:backing_track_hash) { {:client_track_id => 'client_guid', :filename => "blah.wav"} } before(:each) do msuh.touch @@ -16,7 +18,8 @@ describe Track do describe "sync" do it "create one track" do - tracks = Track.sync(connection.client_id, [track_hash]) + result = Track.sync(connection.client_id, [track_hash]) + tracks = result[:tracks] tracks.length.should == 1 track = tracks[0] track.client_track_id.should == track_hash[:client_track_id] @@ -25,7 +28,8 @@ describe Track do end it "create two tracks" do - tracks = Track.sync(connection.client_id, [track_hash, track_hash]) + result = Track.sync(connection.client_id, [track_hash, track_hash]) + tracks = result[:tracks] tracks.length.should == 2 track = tracks[0] track.client_track_id.should == track_hash[:client_track_id] @@ -40,7 +44,8 @@ describe Track do it "delete only track" do track.id.should_not be_nil connection.tracks.length.should == 1 - tracks = Track.sync(connection.client_id, []) + result = Track.sync(connection.client_id, []) + tracks = result[:tracks] tracks.length.should == 0 end @@ -49,7 +54,8 @@ describe Track do track.id.should_not be_nil track2.id.should_not be_nil connection.tracks.length.should == 2 - tracks = Track.sync(connection.client_id, [{:id => track.id, :client_track_id => 'client_guid_new', :sound => 'mono', :instrument_id => 'drums'}]) + result = Track.sync(connection.client_id, [{:id => track.id, :client_track_id => 'client_guid_new', :sound => 'mono', :instrument_id => 'drums'}]) + tracks = result[:tracks] tracks.length.should == 1 found = tracks[0] found.id.should == track.id @@ -62,7 +68,8 @@ describe Track do track.id.should_not be_nil track2.id.should_not be_nil connection.tracks.length.should == 2 - tracks = Track.sync(connection.client_id, [{:client_track_id => track.client_track_id, :sound => 'mono', :instrument_id => 'drums'}]) + result = Track.sync(connection.client_id, [{:client_track_id => track.client_track_id, :sound => 'mono', :instrument_id => 'drums'}]) + tracks = result[:tracks] tracks.length.should == 1 found = tracks[0] found.id.should == track.id @@ -75,7 +82,8 @@ describe Track do track.id.should_not be_nil connection.tracks.length.should == 1 set_updated_at(track, 1.days.ago) - tracks = Track.sync(connection.client_id, [{:id => track.id, :client_track_id => 'client_guid_new', :sound => 'mono', :instrument_id => 'drums'}]) + result = Track.sync(connection.client_id, [{:id => track.id, :client_track_id => 'client_guid_new', :sound => 'mono', :instrument_id => 'drums'}]) + tracks = result[:tracks] tracks.length.should == 1 found = tracks[0] found.id.should == track.id @@ -87,7 +95,8 @@ describe Track do it "updates a single track using .client_track_id to correlate" do track.id.should_not be_nil connection.tracks.length.should == 1 - tracks = Track.sync(connection.client_id, [{:client_track_id => track.client_track_id, :sound => 'mono', :instrument_id => 'drums'}]) + result = Track.sync(connection.client_id, [{:client_track_id => track.client_track_id, :sound => 'mono', :instrument_id => 'drums'}]) + tracks = result[:tracks] tracks.length.should == 1 found = tracks[0] found.id.should == track.id @@ -99,11 +108,69 @@ describe Track do track.id.should_not be_nil connection.tracks.length.should == 1 set_updated_at(track, 1.days.ago) - tracks = Track.sync(connection.client_id, [{:id => track.id, :client_track_id => track.client_track_id, :sound => track.sound, :instrument_id => track.instrument_id, client_resource_id: track.client_resource_id}]) + result = Track.sync(connection.client_id, [{:id => track.id, :client_track_id => track.client_track_id, :sound => track.sound, :instrument_id => track.instrument_id, client_resource_id: track.client_resource_id}]) + tracks = result[:tracks] tracks.length.should == 1 found = tracks[0] expect(found.id).to eq track.id expect(found.updated_at.to_i).to eq track.updated_at.to_i end + + describe "backing tracks" do + it "create one track and one backing track" do + result = Track.sync(connection.client_id, [track_hash], [backing_track_hash]) + tracks = result[:tracks] + tracks.length.should == 1 + track = tracks[0] + track.client_track_id.should == track_hash[:client_track_id] + track.sound = track_hash[:sound] + track.instrument.should == Instrument.find('drums') + + backing_tracks = result[:backing_tracks] + backing_tracks.length.should == 1 + track = backing_tracks[0] + track.client_track_id.should == backing_track_hash[:client_track_id] + end + + it "delete only backing_track" do + track.id.should_not be_nil + backing_track.id.should_not be_nil + connection.tracks.length.should == 1 + connection.backing_tracks.length.should == 1 + result = Track.sync(connection.client_id, + [{:id => track.id, :client_track_id => track.client_track_id, :sound => track.sound, :instrument_id => track.instrument_id, client_resource_id: track.client_resource_id}], + []) + tracks = result[:tracks] + tracks.length.should == 1 + found = tracks[0] + expect(found.id).to eq track.id + expect(found.updated_at.to_i).to eq track.updated_at.to_i + + backing_tracks = result[:backing_tracks] + backing_tracks.length.should == 0 + end + + it "does not touch updated_at when nothing changes" do + track.id.should_not be_nil + backing_track.id.should_not be_nil + connection.tracks.length.should == 1 + set_updated_at(track, 1.days.ago) + set_updated_at(backing_track, 1.days.ago) + result = Track.sync(connection.client_id, + [{:id => track.id, :client_track_id => track.client_track_id, :sound => track.sound, :instrument_id => track.instrument_id, client_resource_id: track.client_resource_id}], + [{:id => backing_track.id, :client_track_id => backing_track.client_track_id, :filename => backing_track.filename, client_resource_id: backing_track.client_resource_id}]) + tracks = result[:tracks] + tracks.length.should == 1 + found = tracks[0] + expect(found.id).to eq track.id + expect(found.updated_at.to_i).to eq track.updated_at.to_i + + backing_tracks = result[:backing_tracks] + backing_tracks.length.should == 1 + found = backing_tracks[0] + expect(found.id).to eq backing_track.id + expect(found.updated_at.to_i).to eq backing_track.updated_at.to_i + end + end end end \ No newline at end of file diff --git a/ruby/spec/jam_ruby/models/user_spec.rb b/ruby/spec/jam_ruby/models/user_spec.rb index ec8b8440d..6d51c9b6e 100644 --- a/ruby/spec/jam_ruby/models/user_spec.rb +++ b/ruby/spec/jam_ruby/models/user_spec.rb @@ -25,6 +25,22 @@ describe User do should respond_to(:can_invite) should respond_to(:mods) should respond_to(:last_jam_audio_latency) + should respond_to(:website) + should respond_to(:age) + should respond_to(:skill_level) + should respond_to(:concert_count) + should respond_to(:studio_session_count) + should respond_to(:virtual_band) + should respond_to(:virtual_band_commitment) + should respond_to(:traditional_band) + should respond_to(:traditional_band_commitment) + should respond_to(:traditional_band_touring) + should respond_to(:paid_sessions) + should respond_to(:paid_sessions_hourly_rate) + should respond_to(:paid_sessions_daily_rate) + should respond_to(:free_sessions) + should respond_to(:cowriting) + should respond_to(:cowriting_purpose) should be_valid should_not be_admin } @@ -649,6 +665,18 @@ describe User do end end + describe "age" do + let(:user) {FactoryGirl.create(:user)} + + it "should calculate age based on birth_date" do + user.birth_date = Time.now - 10.years + user.age.should == 10 + + user.birth_date = Time.now - 10.years + 3.months + user.age.should == 9 + end + end + describe "mods_merge" do let(:user) {FactoryGirl.create(:user)} diff --git a/ruby/spec/jam_ruby/models/user_sync_spec.rb b/ruby/spec/jam_ruby/models/user_sync_spec.rb index 2bea2d766..b015e77f5 100644 --- a/ruby/spec/jam_ruby/models/user_sync_spec.rb +++ b/ruby/spec/jam_ruby/models/user_sync_spec.rb @@ -20,6 +20,49 @@ describe UserSync do data[:next].should be_nil end + describe "backing_tracks" do + + let!(:recording1) { + recording = FactoryGirl.create(:recording, owner: user1, band: nil, duration:1) + recording.recorded_tracks << FactoryGirl.create(:recorded_track, recording: recording, user: recording.owner, fully_uploaded:false) + recording.recorded_tracks << FactoryGirl.create(:recorded_track, recording: recording, user: user2, fully_uploaded:false) + recording.recorded_backing_tracks << FactoryGirl.create(:recorded_backing_track, recording: recording, user: recording.owner, fully_uploaded:false) + recording.save! + recording.reload + recording + } + + let(:sorted_tracks) { + Array.new(recording1.recorded_tracks).sort! {|a, b| + if a.created_at == b.created_at + a.id <=> b.id + else + a.created_at <=> b.created_at + end + } + } + + # backing tracks should only list download, or upload, for the person who opened it, for legal reasons + it "lists backing track for opener" do + data = UserSync.index({user_id: user1.id}) + data[:next].should be_nil + user_syncs = data[:query] + user_syncs.count.should eq(3) + user_syncs[0].recorded_track.should == sorted_tracks[0] + user_syncs[1].recorded_track.should == sorted_tracks[1] + user_syncs[2].recorded_backing_track.should == recording1.recorded_backing_tracks[0] + end + + it "does not list backing track for non-opener" do + data = UserSync.index({user_id: user2.id}) + data[:next].should be_nil + user_syncs = data[:query] + user_syncs.count.should eq(2) + user_syncs[0].recorded_track.should == sorted_tracks[0] + user_syncs[1].recorded_track.should == sorted_tracks[1] + end + end + it "one mix and quick mix" do mix = FactoryGirl.create(:mix) mix.recording.duration = 1 diff --git a/ruby/spec/spec_helper.rb b/ruby/spec/spec_helper.rb index 890a195c0..16ac84514 100644 --- a/ruby/spec/spec_helper.rb +++ b/ruby/spec/spec_helper.rb @@ -46,6 +46,7 @@ ActiveRecord::Base.add_observer InvitedUserObserver.instance ActiveRecord::Base.add_observer UserObserver.instance ActiveRecord::Base.add_observer FeedbackObserver.instance ActiveRecord::Base.add_observer RecordedTrackObserver.instance +ActiveRecord::Base.add_observer RecordedBackingTrackObserver.instance ActiveRecord::Base.add_observer QuickMixObserver.instance #RecordedTrack.observers.disable :all # only a few tests want this observer active diff --git a/web/Gemfile b/web/Gemfile index e5c3f1583..0405c352f 100644 --- a/web/Gemfile +++ b/web/Gemfile @@ -51,7 +51,7 @@ gem 'twitter' gem 'fb_graph', '2.5.9' gem 'sendgrid', '1.2.0' gem 'filepicker-rails', '0.1.0' -gem 'aws-sdk' #, '1.29.1' +gem 'aws-sdk', '~> 1' gem 'aasm', '3.0.16' gem 'carrierwave', '0.9.0' gem 'carrierwave_direct' @@ -75,7 +75,7 @@ gem 'netaddr' gem 'quiet_assets', :group => :development gem 'bugsnag' gem 'multi_json', '1.9.0' -gem 'rest_client' +gem 'rest-client' gem 'iso-639' gem 'language_list' gem 'rubyzip' diff --git a/web/README.md b/web/README.md index efb2ab112..b32f3ab99 100644 --- a/web/README.md +++ b/web/README.md @@ -1,6 +1,3 @@ -TODO: -==== - Jasmine Javascript Unit Tests ============================= diff --git a/web/app/assets/images/content/icon_metronome.png b/web/app/assets/images/content/icon_metronome.png new file mode 100644 index 000000000..2e8d963b2 Binary files /dev/null and b/web/app/assets/images/content/icon_metronome.png differ diff --git a/web/app/assets/images/content/icon_metronome_small.png b/web/app/assets/images/content/icon_metronome_small.png new file mode 100644 index 000000000..b24f11fdb Binary files /dev/null and b/web/app/assets/images/content/icon_metronome_small.png differ diff --git a/web/app/assets/images/web/back-us-kickstarter.png b/web/app/assets/images/web/back-us-kickstarter.png new file mode 100644 index 000000000..d2883c8a3 Binary files /dev/null and b/web/app/assets/images/web/back-us-kickstarter.png differ diff --git a/web/app/assets/images/web/carousel_community.png b/web/app/assets/images/web/carousel_community.png new file mode 100644 index 000000000..ace54546b Binary files /dev/null and b/web/app/assets/images/web/carousel_community.png differ diff --git a/web/app/assets/images/web/carousel_jamblaster.png b/web/app/assets/images/web/carousel_jamblaster.png new file mode 100644 index 000000000..636ede273 Binary files /dev/null and b/web/app/assets/images/web/carousel_jamblaster.png differ diff --git a/web/app/assets/images/web/carousel_overview.png b/web/app/assets/images/web/carousel_overview.png new file mode 100644 index 000000000..f8493c1f4 Binary files /dev/null and b/web/app/assets/images/web/carousel_overview.png differ diff --git a/web/app/assets/javascripts/configureTracksHelper.js b/web/app/assets/javascripts/configureTracksHelper.js index 32f220366..ba16f1d28 100644 --- a/web/app/assets/javascripts/configureTracksHelper.js +++ b/web/app/assets/javascripts/configureTracksHelper.js @@ -355,7 +355,7 @@ context._.each(tracks.tracks, function(track) { if(!track.instrument_id) { logger.debug("ConfigureTracks validation error: all tracks with ports assigned must specify an instrument."); - context.JK.Banner.showAlert('All tracks with ports assigned must specify an instrument.'); + context.JK.Banner.showAlert('Please use the instrument icons to choose what you plan to play on each track.'); return false; } }); diff --git a/web/app/assets/javascripts/dialog/localRecordingsDialog.js b/web/app/assets/javascripts/dialog/localRecordingsDialog.js index 27e1bc3e8..d08911d7c 100644 --- a/web/app/assets/javascripts/dialog/localRecordingsDialog.js +++ b/web/app/assets/javascripts/dialog/localRecordingsDialog.js @@ -112,6 +112,10 @@ // tell the server we are about to start a recording rest.startPlayClaimedRecording({id: context.JK.CurrentSessionModel.id(), claimed_recording_id: claimedRecording.id}) .done(function(response) { + + // update session info + context.JK.CurrentSessionModel.updateSession(response); + var recordingId = $(this).attr('data-recording-id'); var openRecordingResult = context.jamClient.OpenRecording(claimedRecording.recording); diff --git a/web/app/assets/javascripts/dialog/openBackingTrackDialog.js b/web/app/assets/javascripts/dialog/openBackingTrackDialog.js new file mode 100644 index 000000000..87150456f --- /dev/null +++ b/web/app/assets/javascripts/dialog/openBackingTrackDialog.js @@ -0,0 +1,147 @@ +(function(context,$) { + + "use strict"; + context.JK = context.JK || {}; + context.JK.OpenBackingTrackDialog = function(app) { + var logger = context.JK.logger; + var rest = context.JK.Rest(); + var showing = false; + var perPage = 10; + var $dialog = null; + var $tbody = null; + var $paginatorHolder = null; + var $templateOpenBackingTrackRow = null; + var $downloadedTrackHelp = null; + var $whatAreBackingTracks = null; + var $displayAudioFileFolder = null; + + + function emptyList() { + $tbody.empty(); + } + + function resetPagination() { + $dialog.find('.paginator').remove(); + } + + function beforeShow() { + emptyList(); + resetPagination(); + showing = true; + getBackingTracks(); + $dialog.data('result', null); + // .done(function(data, textStatus, jqXHR) { + // // initialize pagination + // var $paginator = context.JK.Paginator.create(parseInt(jqXHR.getResponseHeader('total-entries')), perPage, 0, onPageSelected) + // $paginatorHolder.append($paginator); + // }); + } + + function afterHide() { + showing = false; + } + + + function onPageSelected(targetPage) { + return getBackingTracks(targetPage); + } + + function getBackingTracks(page) { + + var result = context.jamClient.getBackingTrackList(); + console.log("result", result) + var backingTracks = result.backing_tracks; + + if (!backingTracks || backingTracks.length == 0) { + $tbody.append("No Tracks found"); + } else { + $.each(backingTracks, function(index, backingTrack) { + var extension = backingTrack.name + var options = { + backingTrackState: null, + name: backingTrack.name, + type: getExtension(backingTrack.name), + length: displaySize(backingTrack.size) + } + var $tr = $(context._.template($templateOpenBackingTrackRow.html(), options, { variable: 'data' })); + $tr.data('server-model', backingTrack); + $tbody.append($tr); + }); + }//end + } + + // from http://stackoverflow.com/questions/190852/how-can-i-get-file-extensions-with-javascript + function getExtension(filename) { + return filename.substr((~-filename.lastIndexOf(".") >>> 0) + 2) + } + + // from seth: + function displaySize(length) { + var size = (length==null || typeof(length)=='undefined') ? 0 : Number(length) + return (Math.round(size * 10 / (1024 * 1024) ) / 10).toString() + "M" + } + + function registerStaticEvents() { + $tbody.on('click', 'tr', function(e) { + var backingTrack = $(this).data('server-model'); + + // tell the server we are about to open a backing track: + rest.openBackingTrack({id: context.JK.CurrentSessionModel.id(), backing_track_path: backingTrack.name}) + .done(function(response) { + var result = context.jamClient.SessionOpenBackingTrackFile(backingTrack.name, false); + console.log("BackingTrackPlay response: %o", result); + + // TODO: Possibly actually check the result. Investigate + // what real client returns: + // // if(result) { + // let callers see which backing track was chosen + $dialog.data('result', backingTrack); + app.layout.closeDialog('open-backing-track-dialog'); + // } + // else { + // logger.error("unable to open backing track") + // } + context.JK.CurrentSessionModel.refreshCurrentSession(true); + + }) + .fail(function(jqXHR) { + app.notifyServerError(jqXHR, "Unable to Open BackingTrack For Playback"); + }) + + return false; + }) + + context.JK.helpBubble($whatAreBackingTracks, 'no help yet for this topic', {}, {positions:['bottom'], offsetParent: $dialog}) + $whatAreBackingTracks.on('click', false) // no help yet + + $displayAudioFileFolder.on('click', function(e) { + e.stopPropagation(); + context.jamClient.OpenBackingTracksDirectory(); + }) + } + + function initialize(){ + var dialogBindings = { + 'beforeShow' : beforeShow, + 'afterHide': afterHide + }; + + app.bindDialog('open-backing-track-dialog', dialogBindings); + + $dialog = $('#open-backing-track-dialog'); + $tbody = $dialog.find('table.open-backing-tracks tbody'); + $paginatorHolder = $dialog.find('.paginator-holder'); + $templateOpenBackingTrackRow = $('#template-backing-track-row') + $whatAreBackingTracks = $dialog.find('.what-are-backingtracks') + $displayAudioFileFolder = $dialog.find('.display-backingtracks-folder') + + registerStaticEvents(); + }; + + + this.initialize = initialize; + this.isShowing = function isShowing() { return showing; } + } + + return this; +})(window,jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/dialog/recordingFinishedDialog.js b/web/app/assets/javascripts/dialog/recordingFinishedDialog.js index f0d24a27d..f78022877 100644 --- a/web/app/assets/javascripts/dialog/recordingFinishedDialog.js +++ b/web/app/assets/javascripts/dialog/recordingFinishedDialog.js @@ -67,7 +67,7 @@ else { // load recording - var openRecordingResult = context.jamClient.OpenRecording(recording); + var openRecordingResult = context.jamClient.PreviewRecording(recording); logger.debug("OpenRecording response: %o", openRecordingResult); @@ -78,6 +78,23 @@ "icon_url": "/assets/content/icon_alert_big.png" }); } + else { + // hunt for missing backing tracks; if so, mark them as silent + context._.each(openRecordingResult.backing_tracks, function(backingTrack) { + if(backingTrack.local_state == "MISSING") { + // mark this as deleted + logger.debug("marking recorded track as deleted") + rest.markRecordedBackingTrackSilent({recording_id: openRecordingResult.recording_id, backing_track_id: backingTrack.client_track_id}) + .fail(function() { + app.notify({ + "title": "Unable to Mark Backing Track", + "text": "A backing track was never played, but we could not tell the server to remove it from the recording.", + "icon_url": "/assets/content/icon_alert_big.png" + }); + }) + } + }) + } playbackControls.startMonitor(); } @@ -88,7 +105,7 @@ function afterHide() { recording = null; playbackControls.stopMonitor(); - context.jamClient.CloseRecording(); + context.jamClient.ClosePreviewRecording(); } function discardRecording(e) { diff --git a/web/app/assets/javascripts/faderHelpers.js b/web/app/assets/javascripts/faderHelpers.js index 7beca59db..b4cb3823d 100644 --- a/web/app/assets/javascripts/faderHelpers.js +++ b/web/app/assets/javascripts/faderHelpers.js @@ -28,6 +28,15 @@ return false; } + if($fader.data('showHelpAboutMediaMixers')) { + if(window.JK.CurrentSessionModel) { + if(!window.JK.CurrentSessionModel.hasShownAudioMediaMixerHelp()) { + window.JK.prodBubble($fader, 'volume-media-mixers', {}, {positions:['top'], offsetParent: $fader.closest('.screen')}) + window.JK.CurrentSessionModel.markShownAudioMediaMixerHelp() + } + } + } + draggingOrientation = $fader.attr('orientation'); var offset = $fader.offset(); var position = { top: e.pageY - offset.top, left: e.pageX - offset.left} @@ -137,6 +146,16 @@ } function onFaderDragStop(e, ui) { + + if($draggingFader.data('showHelpAboutMediaMixers')) { + if(window.JK.CurrentSessionModel) { + if(!window.JK.CurrentSessionModel.hasShownAudioMediaMixerHelp()) { + window.JK.prodBubble($draggingFader, 'volume-media-mixers', {}, {positions:['bottom'], offsetParent: $draggingFader.closest('.screen')}) + window.JK.CurrentSessionModel.markShownAudioMediaMixerHelp() + } + } + } + var faderPct = faderValue($draggingFader, e, ui.position); // protect against attempts to drag outside of the slider, which jquery.draggable sometimes allows @@ -179,7 +198,10 @@ selector.html(g._.template(templateSource, options)); - selector.find('div[control="fader"]').data('media-controls-disabled', selector.data('media-controls-disabled')).data('media-track-opener', selector.data('media-track-opener')) + selector.find('div[control="fader"]') + .data('media-controls-disabled', selector.data('media-controls-disabled')) + .data('media-track-opener', selector.data('media-track-opener')) + .data('showHelpAboutMediaMixers', selector.data('showHelpAboutMediaMixers')) selector.find('div[control="fader-handle"]').draggable({ drag: onFaderDrag, @@ -187,7 +209,9 @@ stop: onFaderDragStop, containment: "parent", axis: options.faderType === 'horizontal' ? 'x' : 'y' - }).data('media-controls-disabled', selector.data('media-controls-disabled')).data('media-track-opener', selector.data('media-track-opener')) + }).data('media-controls-disabled', selector.data('media-controls-disabled')) + .data('media-track-opener', selector.data('media-track-opener')) + .data('showHelpAboutMediaMixers', selector.data('showHelpAboutMediaMixers')) // Embed any custom styles, applied to the .fader below selector if ("style" in options) { diff --git a/web/app/assets/javascripts/fakeJamClient.js b/web/app/assets/javascripts/fakeJamClient.js index e8bdfcaaf..7a15ffbb1 100644 --- a/web/app/assets/javascripts/fakeJamClient.js +++ b/web/app/assets/javascripts/fakeJamClient.js @@ -21,6 +21,12 @@ var frameSize = 2.5; var fakeJamClientRecordings = null; var p2pCallbacks = null; + var metronomeActive=false; + var metronomeBPM=false; + var metronomeSound=false; + var metronomeMeter=0; + var backingTrackPath=""; + var backingTrackLoop=false; function dbg(msg) { logger.debug('FakeJamClient: ' + msg); } @@ -398,21 +404,42 @@ } function SessionGetControlState(mixerIds, isMasterOrPersonal) { dbg("SessionGetControlState"); - var groups = [0, 1, 2, 3, 7, 9]; + var groups = [0, 1, 2, 3, 3, 7, 8, 10, 11, 12]; var names = [ "FW AP Multi", "FW AP Multi", "FW AP Multi", "FW AP Multi", "", - "" + "", + "", + "", + "", + "" ]; + + var media_types = [ + "Master", + "Monitor", + "AudioInputMusic", + "AudioInputChat", + "StreamOutMusic", + "UserMusicInput", + "PeerAudioInputMusic", + "PeerMediaTrack", + "JamTrack", + "MetronomeTrack" + ] var clientIds = [ "", "", "", "", "3933ebec-913b-43ab-a4d3-f21dc5f8955b", + "", + "", + "", + "", "" ]; var response = []; @@ -422,6 +449,7 @@ group_id: groups[i], id: mixerIds[i] + (isMasterOrPersonal ? 'm' : 'p'), master: isMasterOrPersonal, + media_type: media_types[i], monitor: !isMasterOrPersonal, mute: false, name: names[i], @@ -696,6 +724,55 @@ function GetScoreWorkTimingInterval() { return {interval: 1000, backoff:60000} } function SetScoreWorkTimingInterval(knobs) {return true;} + function SessionOpenBackingTrackFile(path, loop) { + backingTrackPath = path + backingTrackLoop = loop + } + + function SessionSetBackingTrackFileLoop(path, loop) { + backingTrackPath = path + backingTrackLoop = loop + } + + function SessionCloseBackingTrackFile(path) { + backingTrackPath="" + } + + function SessionOpenMetronome(bpm, click, meter, mode){ + console.log("Setting metronome BPM: ", bpm) + metronomeActive =true + metronomeBPM = bpm + metronomeSound = click + metronomeMeter = meter + } + + //change setting - click. Mode 0: = mono, 1, = left ear, 2= right ear + function SessionSetMetronome(bpm,click,meter, mode){ + SessionOpenMetronome(bpm, click, meter, mode) + } + + //close everywhere + function SessionCloseMetronome(){ + metronomeActive=false + } + + function setMetronomeOpenCallback(callback) { + + } + + function getMyNetworkState() { + return { + ntp_stable: Math.random() > 0.5 + } + } + + function getPeerState(clientId) { + return { + ntp_stable: Math.random() > 0.5 + } + } + + // stun function NetworkTestResult() { return {remote_udp_blocked: false} } @@ -727,6 +804,14 @@ fire(); } + + function getBackingTrackList() { + return {backing_tracks: [ + {name:"This is a really long name for a song dude.mp3", size:4283}, + {name:"foo.mp3",size:325783838} + ]}; + } + function ClientUpdateStartUpdate(path, successCallback, failureCallback) {} // ------------------------------- @@ -806,7 +891,11 @@ function OpenRecording(claimedRecording) { return {success: true} } + function PreviewRecording(claimedRecording) { + return OpenRecording(claimedRecording); + } function CloseRecording() {} + function ClosePreviewRecording() {CloseRecording();} function OnDownloadAvailable() {} function SaveToClipboard(text) {} function IsNativeClient() { /* must always return false in all scenarios due to not ruin scoring !*/ return false; } @@ -994,6 +1083,20 @@ this.GetScoreWorkTimingInterval = GetScoreWorkTimingInterval; this.SetScoreWorkTimingInterval = SetScoreWorkTimingInterval; + // Backing tracks: + this.getBackingTrackList = getBackingTrackList; + this.SessionCloseBackingTrackFile = SessionCloseBackingTrackFile; + this.SessionOpenBackingTrackFile = SessionOpenBackingTrackFile; + this.SessionSetBackingTrackFileLoop = SessionSetBackingTrackFileLoop; + + // Metronome: + this.SessionCloseMetronome = SessionCloseMetronome; + this.SessionOpenMetronome = SessionOpenMetronome; + this.SessionSetMetronome = SessionSetMetronome; + this.setMetronomeOpenCallback = setMetronomeOpenCallback; + this.getMyNetworkState = getMyNetworkState; + this.getPeerState = getPeerState; + // Client Update this.IsAppInWritableVolume = IsAppInWritableVolume; this.ClientUpdateVersion = ClientUpdateVersion; @@ -1017,6 +1120,8 @@ this.GetLocalRecordingState = GetLocalRecordingState; this.OpenRecording = OpenRecording; this.CloseRecording = CloseRecording; + this.PreviewRecording = PreviewRecording; + this.ClosePreviewRecording = ClosePreviewRecording; this.OnDownloadAvailable = OnDownloadAvailable; // Clipboard diff --git a/web/app/assets/javascripts/jam_rest.js b/web/app/assets/javascripts/jam_rest.js index c02cee31c..df18a8070 100644 --- a/web/app/assets/javascripts/jam_rest.js +++ b/web/app/assets/javascripts/jam_rest.js @@ -1043,6 +1043,18 @@ }) } + function markRecordedBackingTrackSilent(options) { + var recordingId = options["recording_id"]; + var trackId = options["backing_track_id"]; + + return $.ajax({ + type: "POST", + dataType: "json", + contentType: 'application/json', + data: {}, + url: "/api/recordings/" + recordingId + "/backing_tracks/" + trackId + '/silent' + }); + } function getRecordedTrack(options) { var recordingId = options["recording_id"]; var trackId = options["track_id"]; @@ -1055,6 +1067,18 @@ }); } + function getRecordedBackingTrack(options) { + var recordingId = options["recording_id"]; + var trackId = options["track_id"]; + + return $.ajax({ + type: "GET", + dataType: "json", + contentType: 'application/json', + url: "/api/recordings/" + recordingId + "/backing_tracks/" + trackId + }); + } + function getRecording(options) { var recordingId = options["id"]; @@ -1157,6 +1181,32 @@ }) } + function openBackingTrack(options) { + var musicSessionId = options["id"]; + delete options["id"]; + + return $.ajax({ + type: "POST", + dataType: "json", + contentType: 'application/json', + url: "/api/sessions/" + musicSessionId + "/backing_tracks/open", + data: JSON.stringify(options) + }) + } + + function closeBackingTrack(options) { + var musicSessionId = options["id"]; + delete options["id"]; + + return $.ajax({ + type: "POST", + dataType: "json", + contentType: 'application/json', + url: "/api/sessions/" + musicSessionId + "/backing_tracks/close", + data: JSON.stringify(options) + }) + } + function openJamTrack(options) { var musicSessionId = options["id"]; var jamTrackId = options["jam_track_id"]; @@ -1185,6 +1235,32 @@ }) } + function openMetronome(options) { + var musicSessionId = options["id"]; + delete options["id"]; + + return $.ajax({ + type: "POST", + dataType: "json", + contentType: 'application/json', + url: "/api/sessions/" + musicSessionId + "/metronome/open", + data: JSON.stringify(options) + }) + } + + function closeMetronome(options) { + var musicSessionId = options["id"]; + delete options["id"]; + + return $.ajax({ + type: "POST", + dataType: "json", + contentType: 'application/json', + url: "/api/sessions/" + musicSessionId + "/metronome/close", + data: JSON.stringify(options) + }) + } + function discardRecording(options) { var recordingId = options["id"]; @@ -1389,6 +1465,15 @@ }); } + function getBackingTracks(options) { + return $.ajax({ + type: "GET", + url: '/api/backing_tracks?' + $.param(options), + dataType: "json", + contentType: 'application/json' + }); + } + function addJamtrackToShoppingCart(options) { return $.ajax({ type: "POST", @@ -1500,6 +1585,14 @@ }); } + function validateUrlSite(url, sitetype) { + return $.ajax({ + type: "GET", + url: '/api/data_validation?sitetype='+sitetype+'&data=' + encodeURIComponent(url), + contentType: 'application/json' + }); + } + function initialize() { return self; } @@ -1577,6 +1670,7 @@ this.stopRecording = stopRecording; this.getRecording = getRecording; this.getRecordedTrack = getRecordedTrack; + this.getRecordedBackingTrack = getRecordedBackingTrack; this.getClaimedRecordings = getClaimedRecordings; this.getClaimedRecording = getClaimedRecording; this.updateClaimedRecording = updateClaimedRecording; @@ -1585,8 +1679,13 @@ this.claimRecording = claimRecording; this.startPlayClaimedRecording = startPlayClaimedRecording; this.stopPlayClaimedRecording = stopPlayClaimedRecording; - this.openJamTrack = openJamTrack; + this.openJamTrack = openJamTrack + this.openBackingTrack = openBackingTrack + this.closeBackingTrack = closeBackingTrack + this.closeMetronome = closeMetronome; this.closeJamTrack = closeJamTrack; + this.openMetronome = openMetronome; + this.closeMetronome = closeMetronome; this.discardRecording = discardRecording; this.putTrackSyncChange = putTrackSyncChange; this.createBand = createBand; @@ -1619,6 +1718,7 @@ this.getPurchasedJamTracks = getPurchasedJamTracks; this.getJamTrackRight = getJamTrackRight; this.enqueueJamTrack = enqueueJamTrack; + this.getBackingTracks = getBackingTracks; this.addJamtrackToShoppingCart = addJamtrackToShoppingCart; this.getShoppingCarts = getShoppingCarts; this.removeShoppingCart = removeShoppingCart; @@ -1632,6 +1732,8 @@ this.resendBandInvitation = resendBandInvitation; this.getMount = getMount; this.createSourceChange = createSourceChange; + this.validateUrlSite = validateUrlSite; + this.markRecordedBackingTrackSilent = markRecordedBackingTrackSilent; return this; }; diff --git a/web/app/assets/javascripts/landing/init.js b/web/app/assets/javascripts/landing/init.js index bc2a5c696..ade6a14ec 100644 --- a/web/app/assets/javascripts/landing/init.js +++ b/web/app/assets/javascripts/landing/init.js @@ -3,7 +3,8 @@ "use strict"; $(function() { - context.JK.popExternalLinks(); + // commented out because JamKazam.js does this, and it's included everywhere that this file is + //scontext.JK.popExternalLinks(); }) })(window, jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/layout.js b/web/app/assets/javascripts/layout.js index 723e75650..6c40f596d 100644 --- a/web/app/assets/javascripts/layout.js +++ b/web/app/assets/javascripts/layout.js @@ -215,6 +215,14 @@ top: childLayout.top, left: childLayout.left }, opts.animationDuration); + + if($(this).is('.feed')) { + $('#jamblaster-notice').animate({ + width: childLayout.width, + bottom: '102%', + left: childLayout.left + }, opts.animationDuration) + } }); } diff --git a/web/app/assets/javascripts/notificationPanel.js b/web/app/assets/javascripts/notificationPanel.js index fdf0ac3ee..7c3e608de 100644 --- a/web/app/assets/javascripts/notificationPanel.js +++ b/web/app/assets/javascripts/notificationPanel.js @@ -221,9 +221,9 @@ $.each(response, function(index, val) { // this means the session no longer exists - if (response.fan_access == null && response.musician_access == null) { - return; - } + //if (response.fan_access == null && response.musician_access == null) { + // return; + //} if(val.description == context.JK.MessageType.TEXT_MESSAGE) { val.formatted_msg = textMessageDialog.formatTextMessage(val.message.substring(0, 200), val.source_user_id, val.source_user.name, val.message.length > 200).html(); diff --git a/web/app/assets/javascripts/session.js b/web/app/assets/javascripts/session.js index 051143f73..3a759149c 100644 --- a/web/app/assets/javascripts/session.js +++ b/web/app/assets/javascripts/session.js @@ -4,6 +4,7 @@ context.JK = context.JK || {}; context.JK.SessionScreen = function(app) { + var TEMPOS = context.JK.TEMPOS; var EVENTS = context.JK.EVENTS; var MIX_MODES = context.JK.MIX_MODES; var NAMED_MESSAGES = context.JK.NAMED_MESSAGES; @@ -12,6 +13,48 @@ var modUtils = context.JK.ModUtils; var logger = context.JK.logger; var self = this; + + var defaultParticipant = { + tracks: [{ + instrument_id: "unknown" + }], + user: { + first_name: 'Unknown', + last_name: 'User', + photo_url: null + } + }; + + // Be sure to copy/extend these instead of modifying in place + var trackVuOpts = { + vuType: "vertical", + lightCount: 13, + lightWidth: 3, + lightHeight: 17 + }; + // Must add faderId key to this + var trackFaderOpts = { + faderType: "vertical", + height: 83 + }; + + // Recreate ChannelGroupIDs ENUM from C++ + var ChannelGroupIds = { + "MasterGroup": 0, + "MonitorGroup": 1, + "AudioInputMusicGroup": 2, + "AudioInputChatGroup": 3, + "MediaTrackGroup": 4, + "StreamOutMusicGroup": 5, + "StreamOutChatGroup": 6, + "UserMusicInputGroup": 7, + "UserChatInputGroup": 8, + "PeerAudioInputMusicGroup": 9, + "PeerMediaTrackGroup": 10, + "JamTrackGroup": 11, + "MetronomeGroup": 12 + }; + var sessionModel = null; var sessionId; var tracks = {}; @@ -38,11 +81,14 @@ var startTimeDate = null; var startingRecording = false; // double-click guard var claimedRecording = null; + var backing_track_path = null; var playbackControls = null; var promptLeave = false; var rateSessionDialog = null; var friendInput = null; var sessionPageDone = null; + var metroTempo = 120; + var metroSound = "Beep"; var $recordingManagerViewer = null; var $screen = null; var $mixModeDropdown = null; @@ -51,51 +97,12 @@ var $myTracksContainer = null; var $liveTracksContainer = null; var downloadJamTrack = null; + var $closePlaybackRecording = null; + var mediaTrackGroups = [ChannelGroupIds.MediaTrackGroup, ChannelGroupIds.JamTrackGroup, ChannelGroupIds.MetronomeGroup]; var rest = context.JK.Rest(); var RENDER_SESSION_DELAY = 750; // When I need to render a session, I have to wait a bit for the mixers to be there. - var defaultParticipant = { - tracks: [{ - instrument_id: "unknown" - }], - user: { - first_name: 'Unknown', - last_name: 'User', - photo_url: null - } - }; - - // Be sure to copy/extend these instead of modifying in place - var trackVuOpts = { - vuType: "vertical", - lightCount: 13, - lightWidth: 3, - lightHeight: 17 - }; - // Must add faderId key to this - var trackFaderOpts = { - faderType: "vertical", - height: 83 - }; - - // Recreate ChannelGroupIDs ENUM from C++ - var ChannelGroupIds = { - "MasterGroup": 0, - "MonitorGroup": 1, - "AudioInputMusicGroup": 2, - "AudioInputChatGroup": 3, - "MediaTrackGroup": 4, - "StreamOutMusicGroup": 5, - "StreamOutChatGroup": 6, - "UserMusicInputGroup": 7, - "UserChatInputGroup": 8, - "PeerAudioInputMusicGroup": 9, - "PeerMediaTrackGroup": 10, - "JamTrackGroup": 11, - "MetronomeGroup": 12 - }; - function beforeShow(data) { sessionId = data.id; if(!sessionId) { @@ -436,6 +443,7 @@ var currentSession = sessionModel.getCurrentSession(); + if(claimedRecording == null && (currentSession && currentSession.claimed_recording != null)) { // this is a 'started with a claimed_recording' transition. // we need to start a timer to watch for the state of the play session @@ -444,10 +452,18 @@ else if(claimedRecording && (currentSession == null || currentSession.claimed_recording == null)) { playbackControls.stopMonitor(); } - claimedRecording = currentSession == null ? null : currentSession.claimed_recording; + + if(backing_track_path == null && (currentSession && currentSession.backing_track_path != null)) { + playbackControls.startMonitor(); + } + else if(backing_track_path && (currentSession == null || currentSession.backing_track_path == null)) { + playbackControls.stopMonitor(); + } + backing_track_path = currentSession == null ? null : currentSession.backing_track_path; } + function sessionChanged() { handleTransitionsInRecordingPlayback(); @@ -511,6 +527,16 @@ $('.session-recordings .session-recording-name').text('(No audio loaded)') } } + function didSelfOpenMedia() { + var localMediaMixers = _mixersForGroupIds([ChannelGroupIds.MediaTrackGroup, ChannelGroupIds.JamTrackGroup, ChannelGroupIds.MetronomeGroup], MIX_MODES.MASTER); + + // if we find any local media mixers, then we are the opener of media + return localMediaMixers.length > 0; + } + + function checkShowCloseControl() { + didSelfOpenMedia() ? $closePlaybackRecording.show() : $closePlaybackRecording.hide(); + } function renderSession() { $myTracksContainer.empty(); @@ -524,11 +550,31 @@ _wireTopMix(); _addVoiceChat(); _initDialogs(); + if ($('.session-livetracks .track').length === 0) { $('.session-livetracks .when-empty').show(); } resetOtherAudioContent(); - } + + /** + if ($('.session-recordings .track').length === 0) { + $('.session-recordings .when-empty').show(); + $('.session-recording-name-wrapper').hide(); + $('.session-recordings .recording-controls').hide(); + // should we show the close button? Only if the user opened the media + checkShowCloseControl(); + } else { + $('.session-recordings .when-empty').hide(); + $('.session-recording-name-wrapper').show(); + $('.session-recordings .recording-controls').show(); + checkShowCloseControl(); + } + */ + + // Handle long labels: + $(".track-label").dotdotdot() + $(".session-recording-name").dotdotdot() + } // renderSession function _initDialogs() { configureTrackDialog.initialize(); @@ -544,6 +590,7 @@ function _updateMixers() { masterMixers = context.jamClient.SessionGetAllControlState(true); personalMixers = context.jamClient.SessionGetAllControlState(false); + context.jamClient //logger.debug("masterMixers", masterMixers) //logger.debug("personalMixers", personalMixers) @@ -683,7 +730,6 @@ //logger.debug("clientId", clientId, "groupIds", groupIds, "mixers", mixers) var foundMixers = {}; var mixers = mixMode == MIX_MODES.MASTER ? masterMixers : personalMixers; - // console.log("_groupedMixersForClientId", mixers) $.each(mixers, function(index, mixer) { if (mixer.client_id === clientId) { for (var i=0; i 0) { renderRecordingTracks(recordingTrackMixers) @@ -880,16 +959,127 @@ renderJamTracks(jamTrackMixers); } if(metronomeTrackMixers.length > 0) { - renderMetronomeTracks(jamTrackMixers); + renderMetronomeTracks(metronomeTrackMixers); } if(adhocTrackMixers.length > 0) { logger.warn("some tracks are open that we don't know how to show") } + } - + // this method is pretty complicated because it forks on a key bit of state: + // sessionModel.isPlayingRecording() + // a backing track opened as part of a recording has a different behavior and presence on the server (recording.recorded_backing_tracks) + // than a backing track opend ad-hoc (connection.backing_tracks) function renderBackingTracks(backingTrackMixers) { - logger.error("do not know how to draw backing tracks yet") + + var backingTracks = [] + if(sessionModel.isPlayingRecording()) { + // only return managed mixers for recorded backing tracks + backingTrackMixers = context._.filter(backingTrackMixers, function(mixer){return mixer.managed || mixer.managed === undefined}) + backingTracks = sessionModel.recordedBackingTracks(); + } + else { + // only return un-managed (ad-hoc) mixers for normal backing tracks + backingTracks = sessionModel.backingTracks(); + backingTrackMixers = context._.filter(backingTrackMixers, function(mixer){return !mixer.managed}) + if(backingTrackMixers.length > 1) { + logger.error("multiple, managed backing track mixers encountered", backingTrackMixers) + app.notify({ + title: "Multiple Backing Tracks Encountered", + text: "Only one backing track can be open a time.", + icon_url: "/assets/content/icon_alert_big.png" + }); + return false; + } + } + + var noCorrespondingTracks = false; + $.each(backingTrackMixers, function(index, mixer) { + + // find the track or tracks that correspond to the mixer + var correspondingTracks = [] + + var noCorrespondingTracks = false; + if(sessionModel.isPlayingRecording()) { + $.each(backingTracks, function (i, backingTrack) { + if(mixer.persisted_track_id == backingTrack.client_track_id || // occurs if this client is the one that opened the track + mixer.id == 'L' + backingTrack.client_track_id) { // occurs if this client is a remote participant + correspondingTracks.push(backingTrack) + } + }); + } + else + { + // if this is just an open backing track, then we can assume that the 1st backingTrackMixer is ours + correspondingTracks.push(backingTracks[0]) + } + + if (correspondingTracks.length == 0) { + noCorrespondingTracks = true; + logger.debug("renderBackingTracks: could not map backing tracks") + app.notify({ + title: "Unable to Open Backing Track", + text: "Could not correlate server and client tracks", + icon_url: "/assets/content/icon_alert_big.png" + }); + return false; + } + + // now we have backing track and mixer in hand; we can render + var backingTrack = correspondingTracks[0] + + // pluck the 1st mixer, and assume that all other mixers in this group are of the same type (between JamTrack vs Peer) + // if it's a locally opened track (MediaTrackGroup), then we can say this person is the opener + var isOpener = mixer.group_id == ChannelGroupIds.MediaTrackGroup; + + var shortFilename = context.JK.getNameOfFile(backingTrack.filename); + + if(!sessionModel.isPlayingRecording()) { + // if a recording is being played back, do not set this header, because renderRecordedTracks already did + // ugly. + $('.session-recording-name').text(shortFilename); + } + + var instrumentIcon = context.JK.getInstrumentIcon45(backingTrack.instrument_id); + var photoUrl = "/assets/content/icon_recording.png"; + + + // Default trackData to participant + no Mixer state. + var trackData = { + trackId: backingTrack.id, + clientId: backingTrack.client_id, + name: 'Backing', + filename: backingTrack.filename, + instrumentIcon: instrumentIcon, + avatar: photoUrl, + latency: "good", + gainPercent: 0, + muteClass: 'muted', + showLoop: isOpener && !sessionModel.isPlayingRecording(), + mixerId: "", + avatarClass: 'avatar-recording', + preMasteredClass: "" + }; + + var gainPercent = percentFromMixerValue( + mixer.range_low, mixer.range_high, mixer.volume_left); + var muteClass = "enabled"; + if (mixer.mute) { + muteClass = "muted"; + } + + trackData.gainPercent = gainPercent; + trackData.muteClass = muteClass; + trackData.mixerId = mixer.id; // the master mixer controls the volume control for recordings (no personal controls in either master or personal mode) + trackData.vuMixerId = mixer.id; // the master mixer controls the VUs for recordings (no personal controls in either master or personal mode) + trackData.muteMixerId = mixer.id; // the master mixer controls the mute for recordings (no personal controls in either master or personal mode) + trackData.mediaTrackOpener = isOpener; + trackData.mediaControlsDisabled = !isOpener; + trackData.showHelpAboutMediaMixers = sessionModel.isPersonalMixMode() && isOpener; + + _addRecordingTrack(trackData, mixer); + }); } function renderJamTracks(jamTrackMixers) { @@ -909,7 +1099,6 @@ var preMasteredClass = ""; // find the track or tracks that correspond to the mixer var correspondingTracks = [] - console.log("mixer", mixer) $.each(jamTracks, function(i, jamTrack) { if(mixer.id.indexOf("L") == 0) { if(mixer.id.substring(1) == jamTrack.id) { @@ -917,7 +1106,7 @@ } else { // this should not be possible - alert("Invalid state: the recorded track had neither persisted_track_id or persisted_client_id"); + alert("Invalid state: the backing track had neither persisted_track_id or persisted_client_id"); } } }); @@ -971,11 +1160,10 @@ trackData.mixerId = mixer.id; // the master mixer controls the volume control for recordings (no personal controls in either master or personal mode) trackData.vuMixerId = mixer.id; // the master mixer controls the VUs for recordings (no personal controls in either master or personal mode) trackData.muteMixerId = mixer.id; // the master mixer controls the mute for recordings (no personal controls in either master or personal mode) + trackData.mediaTrackOpener = isOpener; + trackData.mediaControlsDisabled = !isOpener; + trackData.showHelpAboutMediaMixers = sessionModel.isPersonalMixMode() && isOpener; - if(sessionModel.isPersonalMixMode() || !isOpener) { - trackData.mediaControlsDisabled = true; - trackData.mediaTrackOpener = isOpener; - } _addRecordingTrack(trackData); }); @@ -989,13 +1177,106 @@ } function renderMetronomeTracks(metronomeTrackMixers) { - logger.error("do not know how to draw metronome tracks yet") + var metronomeActive = sessionModel.metronomeActive(); + logger.debug("rendering metronome track",metronomeActive) + + // pluck the 1st mixer, and assume that all other mixers in this group are of the same type (between JamTrack vs Peer) + // if it's a locally opened track (MediaTrackGroup), then we can say this person is the opener + var isOpener = metronomeTrackMixers[0].group_id == ChannelGroupIds.MediaTrackGroup; + var name = "Metronome" + + // using the server's info in conjuction with the client's, draw the recording tracks + if(metronomeActive && metronomeTrackMixers.length > 0) { + var metronome = {active: metronomeActive} + $('.session-recording-name').text(name);//sessionModel.getCurrentSession().backing_track_path); + + var noCorrespondingTracks = false; + var mixer = metronomeTrackMixers[0] + var preMasteredClass = ""; + // find the track or tracks that correspond to the mixer + var correspondingTracks = [] + correspondingTracks.push(metronome); + + if(correspondingTracks.length == 0) { + noCorrespondingTracks = true; + app.notify({ + title: "Unable to Open Metronome", + text: "Could not correlate server and client tracks", + icon_url: "/assets/content/icon_metronome_small.png"}); + return false; + } + + // prune found recorded tracks + // Metronomes = $.grep(Metronomes, function(value) { + // return $.inArray(value, correspondingTracks) < 0; + // }); + + var oneOfTheTracks = correspondingTracks[0]; + var instrumentIcon = context.JK.getInstrumentIcon45(oneOfTheTracks.instrument_id); + var photoUrl = "/assets/content/icon_metronome_small.png"; + + // var trackData = { + // trackId: oneOfTheTracks.id, + // clientId: oneOfTheTracks.client_id, + // name: "Tempo", + // instrumentIcon: photoUrl, + // avatar: instrumentIcon, + // latency: "good", + // gainPercent: 0, + // muteClass: 'hidden', + // mixerId: "", + // avatarClass : 'avatar-recording', + // preMasteredClass: "", + // hideVU: true, + // faderChanged : tempoFaderChanged, + // showMetronomeControls: true + // }; + + // _addRecordingTrack(trackData); + + + // Default trackData to participant + no Mixer state. + var trackData = { + trackId: "MS" + oneOfTheTracks.id, + clientId: oneOfTheTracks.client_id, + name: "Metronome", + instrumentIcon: photoUrl, + avatar: instrumentIcon, + latency: "good", + gainPercent: 0, + muteClass: 'muted', + mixerId: "", + avatarClass : 'avatar-recording', + preMasteredClass: "", + showMetronomeControls: true + }; + + var gainPercent = percentFromMixerValue( + mixer.range_low, mixer.range_high, mixer.volume_left); + var muteClass = "enabled"; + if (mixer.mute) { + muteClass = "muted"; + } + trackData.gainPercent = gainPercent; + trackData.muteClass = muteClass; + trackData.mixerId = mixer.id; // the master mixer controls the volume control for recordings (no personal controls in either master or personal mode) + trackData.vuMixerId = mixer.id; // the master mixer controls the VUs for recordings (no personal controls in either master or personal mode) + trackData.muteMixerId = mixer.id; // the master mixer controls the mute for recordings (no personal controls in either master or personal mode) + trackData.mediaTrackOpener = isOpener; + trackData.mediaControlsDisabled = !isOpener; + trackData.showHelpAboutMediaMixers = sessionModel.isPersonalMixMode() && isOpener; + + + _addRecordingTrack(trackData, mixer); + }// if + setFormFromMetronome() } function renderRecordingTracks(recordingMixers) { // get the server's info for the recording var recordedTracks = sessionModel.recordedTracks(); + var recordedBackingTracks = sessionModel.recordedBackingTracks(); // pluck the 1st mixer, and assume that all other mixers in this group are of the same type (between Local vs Peer) // if it's a locally opened track (MediaTrackGroup), then we can say this person is the opener @@ -1031,6 +1312,7 @@ if(correspondingTracks.length == 0) { noCorrespondingTracks = true; + logger.debug("unable to correlate all recorded tracks", recordingMixers, recordedTracks) app.notify({ title: "Unable to Open Recording", text: "Could not correlate server and client tracks", @@ -1078,12 +1360,11 @@ trackData.mixerId = mixer.id; // the master mixer controls the volume control for recordings (no personal controls in either master or personal mode) trackData.vuMixerId = mixer.id; // the master mixer controls the VUs for recordings (no personal controls in either master or personal mode) trackData.muteMixerId = mixer.id; // the master mixer controls the mute for recordings (no personal controls in either master or personal mode) + trackData.mediaControlsDisabled = !isOpener; + trackData.mediaTrackOpener = isOpener; + trackData.showHelpAboutMediaMixers = sessionModel.isPersonalMixMode() && isOpener; - if(sessionModel.isPersonalMixMode() || !isOpener) { - trackData.mediaControlsDisabled = true; - trackData.mediaTrackOpener = isOpener; - } - _addRecordingTrack(trackData); + _addRecordingTrack(trackData, mixer); }); if(!noCorrespondingTracks && recordedTracks.length > 0) { @@ -1140,6 +1421,7 @@ var mixMode = sessionModel.getMixMode(); if(myTrack) { + // when it's your track, look it up by the backend resource ID mixer = getMixerByTrackId(track.client_track_id, mixMode) vuMixer = mixer; @@ -1318,18 +1600,40 @@ if(track.mediaControlsDisabled) { $fader.data('media-controls-disabled', true).data('media-track-opener', track.mediaTrackOpener) // this we be applied later to the fader handle $element } + $fader.data('showHelpAboutMediaMixers', track.showHelpAboutMediaMixers) + + var $track = $(trackSelector); // Set mixer-id attributes and render VU/Fader - context.JK.VuHelpers.renderVU(vuLeftSelector, vuOpts); - $track.find('.track-vu-left').attr('mixer-id', track.vuMixerId + '_vul').data('groupId', groupId) - context.JK.VuHelpers.renderVU(vuRightSelector, vuOpts); - $track.find('.track-vu-right').attr('mixer-id', track.vuMixerId + '_vur').data('groupId', groupId) + + if (!track.hideVU) { + context.JK.VuHelpers.renderVU(vuLeftSelector, vuOpts); + $track.find('.track-vu-left').attr('mixer-id', track.vuMixerId + '_vul').data('groupId', groupId) + context.JK.VuHelpers.renderVU(vuRightSelector, vuOpts); + $track.find('.track-vu-right').attr('mixer-id', track.vuMixerId + '_vur').data('groupId', groupId) + } + + if (track.showMetronomeControls) { + $track.find('.metronome-selects').removeClass("hidden") + } else { + $track.find('.metronome-selects').addClass("hidden") + } + + // if (track.showMetroSound) { + // $track.find('.metro-sound-select').removeClass("hidden") + // } + context.JK.FaderHelpers.renderFader($fader, faderOpts); // Set gain position context.JK.FaderHelpers.setFaderValue(mixerId, gainPercent); - $fader.on('fader_change', faderChanged); - return $track; + if(track.faderChanged) { + $fader.on('fader_change', track.faderChanged); + } else { + $fader.on('fader_change', faderChanged); + } + + return $track; } // Function called on an interval when participants change. Mixers seem to @@ -1480,12 +1784,15 @@ $('.session-recording-name-wrapper').show(); } - function _addRecordingTrack(trackData) { + function _addRecordingTrack(trackData, mixer) { otherAudioFilled(); $('.session-recordings .recording-controls').show(); + var parentSelector = '#session-recordedtracks-container'; + var $destination = $(parentSelector); + var template = $('#template-session-track').html(); var newTrack = $(context.JK.fillTemplate(template, trackData)); $otherAudioContainer.append(newTrack); @@ -1501,6 +1808,22 @@ if(trackData.mediaControlsDisabled) { $trackIconMute.data('media-controls-disabled', true).data('media-track-opener', trackData.mediaTrackOpener) } + $trackIconMute.data('mixer', mixer).data('opposite-mixer', null) + $trackIconMute.data('showHelpAboutMediaMixers', trackData.showHelpAboutMediaMixers) + + + if(trackData.showLoop) { + var $trackIconLoop = $track.find('.track-icon-loop') + var $trackIconLoopCheckbox = $trackIconLoop.find('input') + + context.JK.checkbox($trackIconLoopCheckbox) + $trackIconLoopCheckbox.on('ifChanged', function() { + var loop = $trackIconLoopCheckbox.is(':checked') + _toggleAudioLoop(mixer.id, loop, getMixer(mixer.id).mode) + }); + $trackIconLoop.show() + } + // is this used? tracks[trackData.trackId] = new context.JK.SessionTrack(trackData.clientId); } @@ -1516,8 +1839,8 @@ var mixerIds = faderId.split(','); $.each(mixerIds, function(i,v) { var broadcast = !(data.dragging); // If fader is still dragging, don't broadcast - fillTrackVolumeObject(v, broadcast); - setMixerVolume(v, data.percentage); + var mixer = fillTrackVolumeObject(v, broadcast); + setMixerVolume(mixer, data.percentage); if(groupId == ChannelGroupIds.UserMusicInputGroup) { // there may be other mixers with this same ID in the case of a Peer Music Stream, so update them as well @@ -1526,6 +1849,28 @@ }); } + // function tempoFaderChanged(e, data) { + // var $target = $(this); + // var faderId = $target.attr('mixer-id'); + // var groupId = $target.data('groupId'); + // var mixerIds = faderId.split(','); + // $.each(mixerIds, function(i,v) { + // // TODO Interpolate tempo values if we decide to go this way: + // if(groupId == ChannelGroupIds.UserMusicInputGroup) { + // // there may be other mixers with this same ID in the case of a Peer Music Stream, so update them as well + // } + // }); + // } + + function handleMetronomeCallback(args) { + logger.debug("MetronomeCallback: ", args) + metroTempo = args.bpm + + // This isn't actually there, so we rely on the metroSound as set from select on form: + // metroSound = args.sound + context.JK.CurrentSessionModel.refreshCurrentSession(true); + } + function handleVolumeChangeCallback(mixerId, isLeft, value, isMuted) { // Visually update mixer // There is no need to actually set the back-end mixer value as the @@ -1601,7 +1946,26 @@ } } + function handleBackingTrackSelectedCallback(result) { + if(result.success) { + logger.debug("backing track selected: " + result.file); + rest.openBackingTrack({id: context.JK.CurrentSessionModel.id(), backing_track_path: result.file}) + .done(function(response) { + var openResult = context.jamClient.SessionOpenBackingTrackFile(result.file, false); + //context.JK.CurrentSessionModel.refreshCurrentSession(true); + sessionModel.setBackingTrack(result.file); + }) + .fail(function(jqXHR) { + app.notifyServerError(jqXHR, "Unable to Open BackingTrack For Playback"); + }) + + + } + else { + logger.debug("no backing track selected") + } + } function deleteSession(evt) { var sessionId = $(evt.currentTarget).attr("action-id"); if (sessionId) { @@ -1643,6 +2007,16 @@ context.jamClient.SessionSetControlState(mixerId, mode); } + function _toggleAudioLoop(mixerId, loop, mode) { + fillTrackVolumeObject(mixerId); + context.trackVolumeObject.loop = loop; + + if(mode === undefined) { + mode = sessionModel.getMixMode(); + } + context.jamClient.SessionSetControlState(mixerId, mode); + } + function showMuteDropdowns($control) { $control.btOn(); } @@ -1662,6 +2036,13 @@ return false; } + if($control.data('showHelpAboutMediaMixers')) { + if(!sessionModel.hasShownAudioMediaMixerHelp()) { + context.JK.prodBubble($control, 'volume-media-mixers', {}, {positions:['bottom'], offsetParent: $control.closest('.screen')}) + sessionModel.markShownAudioMediaMixerHelp() + } + } + $.each(mixerIds, function(i,v) { var mixerId = v; // behavior: if this is the user's track in personal mode, then we mute the track globally @@ -1680,7 +2061,7 @@ } // look for all controls matching this mixer id (important when it's personal mode + UserMusicInputGroup) - var $controls = $screen.find('.track-icon-mute[mixer-id=' + mixerId +']'); + var $controls = $screen.find('.track-icon-mute[mixer-id="' + mixerId +'"]'); _toggleVisualMuteControl($controls, muting); }); } @@ -1722,9 +2103,11 @@ context.trackVolumeObject.record = mixer.record; context.trackVolumeObject.volL = mixer.volume_left; context.trackVolumeObject.volR = mixer.volume_right; + context.trackVolumeObject.loop = mixer.loop; // trackVolumeObject doesn't have a place for range min/max currentMixerRangeMin = mixer.range_low; currentMixerRangeMax = mixer.range_high; + return mixer; } // Given a mixer's min/max and current value, return it as @@ -1759,7 +2142,7 @@ // Given a volume percent (0-100), set the underlying // audio volume level of the passed mixerId to the correct // value. - function setMixerVolume(mixerId, volumePercent) { + function setMixerVolume(mixer, volumePercent) { // The context.trackVolumeObject has been filled with the mixer values // that go with mixerId, and the range of that mixer // has been set in currentMixerRangeMin-Max. @@ -1771,13 +2154,17 @@ context.trackVolumeObject.volL = context.JK.FaderHelpers.convertPercentToAudioTaper(volumePercent); context.trackVolumeObject.volR = context.JK.FaderHelpers.convertPercentToAudioTaper(volumePercent); // Special case for L2M mix: - if (mixerId === '__L2M__') { + if (mixer.id === '__L2M__') { logger.debug("L2M volumePercent=" + volumePercent); var dbValue = context.JK.FaderHelpers.convertLinearToDb(volumePercent); context.jamClient.SessionSetMasterLocalMix(dbValue); // context.jamClient.SessionSetMasterLocalMix(sliderValue); } else { - context.jamClient.SessionSetControlState(mixerId, sessionModel.getMixMode()); + var isMediaMixer = mediaTrackGroups.indexOf(mixer.group_id) > -1; + + // if this is a media file (Metronome, JamTrack, BackingTrack, RecordedTrack), then we only modify master + var mixMode = isMediaMixer ? MIX_MODES.MASTER : sessionModel.getMixMode(); + context.jamClient.SessionSetControlState(mixer.id, mixMode); } } @@ -1920,6 +2307,27 @@ .fail(app.ajaxError); } + function openBackingTrack(e) { + // just ignore the click if they are currently recording for now + if(sessionModel.recordingModel.isRecording()) { + app.notify({ + "title": "Currently Recording", + "text": "You can't open a backing track while creating a recording.", + "icon_url": "/assets/content/icon_alert_big.png" + }); + return false; + } + + context.jamClient.ShowSelectBackingTrackDialog("window.JK.HandleBackingTrackSelectedCallback"); + + //app.layout.showDialog('open-backing-track-dialog').one(EVENTS.DIALOG_CLOSED, function(e, data) { + // if(!data.cancel && data.result){ + // sessionModel.setBackingTrack(data.result); + // } + //}) + return false; + } + function openJamTrack(e) { // just ignore the click if they are currently recording for now if(sessionModel.recordingModel.isRecording()) { @@ -1989,6 +2397,95 @@ return false; } + function openBackingTrackFile(e) { + // just ignore the click if they are currently recording for now + if(sessionModel.recordingModel.isRecording()) { + app.notify({ + "title": "Currently Recording", + "text": "You can't open a backing track while creating a recording.", + "icon_url": "/assets/content/icon_alert_big.png" + }); + return false; + } else { + context.jamClient.openBackingTrackFile(sessionModel.backing_track) + //context.JK.CurrentSessionModel.refreshCurrentSession(true); + } + return false; + } + + function unstableNTPClocks() { + var unstable = [] + + // This should be handled in the below loop, actually: + // var map = context.jamClient.getMyNetworkState() + // if (!map.ntp_stable) { + // unstable.push("self"); + // } + + var map; + $.each(sessionModel.participants(), function(index, participant) { + map = context.jamClient.getPeerState(participant.client_id) + + if (!map.ntp_stable) { + var name = participant.user.name; + if (!(name)) { + name = participant.user.first_name + ' ' + participant.user.last_name; + } + + if (app.clientId == participant.client_id) { + name += " (This computer)" + } + + unstable.push(name) + } + }); + + return unstable + } + + function openMetronome(e) { + // just ignore the click if they are currently recording for now + + if(sessionModel.recordingModel.isRecording()) { + app.notify({ + "title": "Currently Recording", + "text": "You can't open a metronome while creating a recording.", + "icon_url": "/assets/content/icon_alert_big.png" + }); + return false; + } else { + var unstable = unstableNTPClocks() + if (unstable.length > 0) { + var names = unstable.join(", ") + logger.debug("Unstable clocks: ", names, unstable) + app.notify({ + "title": "Couldn't open metronome", + "text": "The metronome feature requires that every user's computer in the session must agree on the current time. The computers of " + names + " have not successfully synchronized to the current time. The JamKazam service is trying to automatically correct this error condition. Please close this message, wait about 10 seconds, and then try opening the metronome again. If this problem persists after a couple of attempts, we recommend that the unsynchronized users restart the JamKazam application. If this error persists after a restart, please have the users with the issue contact support@jamkazam.com.", + "icon_url": "/assets/content/icon_alert_big.png" + }); + } else { + rest.openMetronome({id: sessionModel.id()}) + .done(function() { + context.jamClient.SessionOpenMetronome(120, "Click", 1, 0) + context.JK.CurrentSessionModel.refreshCurrentSession(true) + context.JK.CurrentSessionModel.refreshCurrentSession(true) + }) + .fail(function(jqXHR) { + logger.debug(jqXHR, jqXHR) + app.notify({ + "title": "Couldn't open metronome", + "text": "Couldn't inform the server to open metronome. msg=" + jqXHR.responseText, + "icon_url": "/assets/content/icon_alert_big.png" + }); + }); + } + + + return false; + } + } + + function openRecording(e) { // just ignore the click if they are currently recording for now if(sessionModel.recordingModel.isRecording()) { @@ -2014,12 +2511,37 @@ else if(sessionModel.jamTracks() || downloadJamTrack) { closeJamTrack(); } + else if(sessionModel.backingTrack() && sessionModel.backingTrack().path) { + closeBackingTrack(); + } + else if(sessionModel.metronomeActive()) { + closeMetronomeTrack(); + } else { - logger.error("don't know how to close open media (backing track maybe?)"); + logger.error("don't know how to close open media"); } return false; } + function closeBackingTrack() { + rest.closeBackingTrack({id: sessionModel.id()}) + .done(function() { + //sessionModel.refreshCurrentSession(true); + }) + .fail(function(jqXHR) { + app.notify({ + "title": "Couldn't Close BackingTrack", + "text": "Couldn't inform the server to close BackingTrack. msg=" + jqXHR.responseText, + "icon_url": "/assets/content/icon_alert_big.png" + }); + }); + + // '' closes all open backing tracks + context.jamClient.SessionCloseBackingTrackFile(''); + + return false; + } + function closeJamTrack() { logger.debug("closing recording"); @@ -2037,7 +2559,7 @@ rest.closeJamTrack({id: sessionModel.id()}) .done(function() { - sessionModel.refreshCurrentSession(); + sessionModel.refreshCurrentSession(true); }) .fail(function(jqXHR) { app.notify({ @@ -2052,12 +2574,30 @@ return false; } + function closeMetronomeTrack() { + rest.closeMetronome({id: sessionModel.id()}) + .done(function() { + context.jamClient.SessionCloseMetronome(); + sessionModel.refreshCurrentSession(true); + }) + .fail(function(jqXHR) { + app.notify({ + "title": "Couldn't Close MetronomeTrack", + "text": "Couldn't inform the server to close MetronomeTrack. msg=" + jqXHR.responseText, + "icon_url": "/assets/content/icon_alert_big.png" + }); + }); + return false; + } + function closeRecording() { logger.debug("closing recording"); rest.stopPlayClaimedRecording({id: sessionModel.id(), claimed_recording_id: sessionModel.getCurrentSession().claimed_recording.id}) - .done(function() { - sessionModel.refreshCurrentSession(); + .done(function(response) { + //sessionModel.refreshCurrentSession(true); + // update session info + context.JK.CurrentSessionModel.updateSession(response); }) .fail(function(jqXHR) { app.notify({ @@ -2101,12 +2641,41 @@ sessionId); inviteMusiciansUtil.loadFriends(); $(friendInput).show(); - } + } - function onMixerModeChanged(e, data) - { + function setFormFromMetronome() { + $("select.metro-tempo").val(metroTempo) + $("select.metro-sound").val(metroSound) + } + + function setMetronomeFromForm() { + var tempo = $("select.metro-tempo:visible option:selected").val() + var sound = $("select.metro-sound:visible option:selected").val() + + var t = parseInt(tempo) + var s + if (tempo==NaN || tempo==0 || tempo==null) { + t = 120 + } + + if (sound==null || typeof(sound)=='undefined' || sound=="") { + s = "click" + } else { + s = sound + } + + logger.debug("Setting tempo and sound:", t, s) + metroTempo = t + metroSound = s + context.jamClient.SessionSetMetronome(t, s, 1, 0) + } + + function onMetronomeChanged(e, data) { + setMetronomeFromForm() + } + + function onMixerModeChanged(e, data) { $mixModeDropdown.easyDropDown('select', data.mode, true); - setTimeout(renderSession, 1); } @@ -2125,7 +2694,7 @@ return true; } - function events() { + function events() { $('#session-leave').on('click', sessionLeave); $('#session-resync').on('click', sessionResync); $('#session-contents').on("click", '[action="delete"]', deleteSession); @@ -2133,6 +2702,8 @@ $('#recording-start-stop').on('click', startStopRecording); $('#open-a-recording').on('click', openRecording); $('#open-a-jamtrack').on('click', openJamTrack); + $('#open-a-backingtrack').on('click', openBackingTrack); + $('#open-a-metronome').on('click', openMetronome); $('#session-invite-musicians').on('click', inviteMusicians); $('#session-invite-musicians2').on('click', inviteMusicians); $('#track-settings').click(function() { @@ -2141,7 +2712,7 @@ configureTrackDialog.showMusicAudioPanel(true); }); - $('#close-playback-recording').on('click', closeOpenMedia); + $closePlaybackRecording.on('click', closeOpenMedia); $(playbackControls) .on('pause', onPause) .on('play', onPlay) @@ -2149,6 +2720,7 @@ $(friendInput).focus(function() { $(this).val(''); }) $(document).on(EVENTS.MIXER_MODE_CHANGED, onMixerModeChanged) $mixModeDropdown.change(onUserChangeMixMode) + $(document).on("change", ".metronome-select", onMetronomeChanged) } this.initialize = function(localRecordingsDialogInstance, recordingFinishedDialogInstance, friendSelectorDialog) { @@ -2159,6 +2731,7 @@ context.jamClient.SetVURefreshRate(150); context.jamClient.RegisterVolChangeCallBack("JK.HandleVolumeChangeCallback"); playbackControls = new context.JK.PlaybackControls($('.session-recordings .recording-controls')); + context.jamClient.setMetronomeOpenCallback("JK.HandleMetronomeCallback") var screenBindings = { 'beforeShow': beforeShow, @@ -2176,6 +2749,7 @@ $otherAudioContainer = $('#session-recordedtracks-container'); $myTracksContainer = $('#session-mytracks-container') $liveTracksContainer = $('#session-livetracks-container'); + $closePlaybackRecording = $('#close-playback-recording') events(); @@ -2203,7 +2777,9 @@ } context.JK.HandleVolumeChangeCallback = handleVolumeChangeCallback; + context.JK.HandleMetronomeCallback = handleMetronomeCallback; context.JK.HandleBridgeCallback = handleBridgeCallback; + context.JK.HandleBackingTrackSelectedCallback = handleBackingTrackSelectedCallback; }; })(window,jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/sessionList.js b/web/app/assets/javascripts/sessionList.js index 5a02eb429..9662b83ff 100644 --- a/web/app/assets/javascripts/sessionList.js +++ b/web/app/assets/javascripts/sessionList.js @@ -224,29 +224,11 @@ } if (showJoinLink) { - // wire up the Join Link to the T&Cs dialog + // wire up the Join Link to the T&Cs dialog $('.join-link', $parentRow).click(function(evt) { - if(!context.JK.guardAgainstBrowser(app)) { - return false; - } - - if (!context.JK.JamServer.connected) { - app.notifyAlert("Not Connected", 'To create or join a session, you must be connected to the server.'); - return false; - } - - gearUtils.guardAgainstInvalidConfiguration(app) - .fail(function() { - app.notify( - { title: "Unable to Join Session", - text: "You can only join a session once you have working audio gear and a tested internet connection." - }) - }) - .done(function(){ - sessionUtils.joinSession(session.id); - }) - - return false; + sessionUtils.ensureValidClient(app, gearUtils, function() { + sessionUtils.joinSession(session.id); + }); }); } } @@ -368,7 +350,7 @@ $('a.more.rsvps', $parentRow).click(toggleRsvps); var showRsvpLink = true; - var noLinkText = ''; + var sessionLinkText = ''; $('.rsvp-link-text', $parentRow).hide(); function showStartSessionButton(scheduledStart) { @@ -380,8 +362,8 @@ if (session.creator.id === context.JK.currentUserId) { showRsvpLink = false; - noLinkText = $('Start session now?'); - noLinkText.find('a').click(function() { + sessionLinkText = $('Start session now?'); + sessionLinkText.find('a').click(function() { ui.launchSessionStartDialog(session); return false; }); @@ -390,18 +372,18 @@ showRsvpLink = false; if (session.scheduled_start && showStartSessionButton(session.scheduled_start)) { - noLinkText = $('Start session now? | Cancel RSVP'); - noLinkText.find('a.start').click(function() { + sessionLinkText = $('Start session now? | Cancel RSVP'); + sessionLinkText.find('a.start').click(function() { ui.launchSessionStartDialog(session); return false; }); } else { - noLinkText = $('Cancel RSVP'); + sessionLinkText = $('Cancel RSVP'); } // wire cancel link - noLinkText.find('a.cancel').click(function() { + sessionLinkText.find('a.cancel').click(function() { ui.launchRsvpCancelDialog(session.id, approvedRsvpId) .one(EVENTS.RSVP_CANCELED, function() { rest.getSessionHistory(session.id) @@ -419,8 +401,8 @@ showRsvpLink = false; if (session.scheduled_start && showStartSessionButton(session.scheduled_start)) { - noLinkText = $('Start session now?'); - noLinkText.find('a').click(function() { + sessionLinkText = $('Start session now?'); + sessionLinkText.find('a').click(function() { ui.launchSessionStartDialog(session); return false; }); @@ -428,8 +410,8 @@ } else if (pendingRsvpId) { showRsvpLink = false; - noLinkText = $('Cancel RSVP'); - noLinkText.find('a').click(function() { + sessionLinkText = $('Cancel RSVP'); + sessionLinkText.find('a').click(function() { ui.launchRsvpCancelDialog(session.id, pendingRsvpId) .one(EVENTS.RSVP_CANCELED, function() { rest.getSessionHistory(session.id) @@ -445,11 +427,11 @@ } else if (!openSlots) { showRsvpLink = false; - noLinkText = 'No more openings in this session.'; + sessionLinkText = 'No more openings in this session.'; } else if (!openRsvps && !hasInvitation) { showRsvpLink = false; - noLinkText = 'You need an invitation to RSVP to this session.'; + sessionLinkText = 'You need an invitation to RSVP to this session.'; } if (showRsvpLink) { @@ -472,7 +454,7 @@ }); } else { - $('.rsvp-msg', $parentRow).html(noLinkText).show(); + $('.rsvp-msg', $parentRow).html(sessionLinkText).show(); $('.rsvp-link', $parentRow).hide(); } } diff --git a/web/app/assets/javascripts/sessionModel.js b/web/app/assets/javascripts/sessionModel.js index 92d77d2bb..7487dec2d 100644 --- a/web/app/assets/javascripts/sessionModel.js +++ b/web/app/assets/javascripts/sessionModel.js @@ -34,6 +34,9 @@ var sessionPageEnterTimeout = null; var startTime = null; var joinDeferred = null; + var previousBackingTracks = []; + var openBackingTrack = null; + var shownAudioMediaMixerHelp = false; var mixerMode = MIX_MODES.PERSONAL; @@ -67,7 +70,7 @@ function isPlayingRecording() { // this is the server's state; there is no guarantee that the local tracks // requested from the backend will have corresponding track information - return currentSession && currentSession.claimed_recording; + return !!(currentSession && currentSession.claimed_recording); } function recordedTracks() { @@ -79,6 +82,28 @@ } } + function recordedBackingTracks() { + if(currentSession && currentSession.claimed_recording) { + return currentSession.claimed_recording.recording.recorded_backing_tracks + } + else { + return null; + } + } + + function backingTracks() { + var backingTracks = [] + // this may be wrong if we loosen the idea that only one person can have a backing track open. + // but for now, the 1st person we find with a backing track open is all there is to find... + context._.each(participants(), function(participant) { + if(participant.backing_tracks.length > 0) { + backingTracks = participant.backing_tracks; + return false; // break + } + }) + return backingTracks; + } + function jamTracks() { if(currentSession && currentSession.jam_track) { return currentSession.jam_track.tracks @@ -88,6 +113,27 @@ } } + function backingTrack() { + if(currentSession) { + // TODO: objectize this for VRFS-2665, VRFS-2666, VRFS-2667, VRFS-2668 + return { + path: currentSession.backing_track_path + } + } + else { + return null; + } + } + + function metronomeActive() { + if(currentSession) { + return currentSession.metronome_active + } + else { + return null; + } + } + function creatorId() { if(!currentSession) { throw "creator is not known" @@ -303,6 +349,9 @@ } currentSessionId = null; currentParticipants = {} + previousBackingTracks = [] + openBackingTrack = null + shownAudioMediaMixerHelp = false } // you should only update currentSession with this function @@ -321,6 +370,25 @@ } } + function updateSession(response) { + updateSessionInfo(response, null, true); + } + + function updateSessionInfo(response, callback, force) { + if(force === true || currentTrackChanges < response.track_changes_counter) { + logger.debug("updating current track changes from %o to %o", currentTrackChanges, response.track_changes_counter) + currentTrackChanges = response.track_changes_counter; + sendClientParticipantChanges(currentSession, response); + updateCurrentSession(response); + if(callback != null) { + callback(); + } + } + else { + logger.info("ignoring refresh because we already have current: " + currentTrackChanges + ", seen: " + response.track_changes_counter); + } + } + /** * Reload the session data from the REST server, calling * the provided callback when complete. @@ -344,18 +412,7 @@ type: "GET", url: url, success: function(response) { - if(force === true || currentTrackChanges < response.track_changes_counter) { - logger.debug("updating current track changes from %o to %o", currentTrackChanges, response.track_changes_counter) - currentTrackChanges = response.track_changes_counter; - sendClientParticipantChanges(currentSession, response); - updateCurrentSession(response); - if(callback != null) { - callback(); - } - } - else { - logger.info("ignoring refresh because we already have current: " + currentTrackChanges + ", seen: " + response.track_changes_counter); - } + updateSessionInfo(response, callback, force); }, error: function(jqXHR) { if(jqXHR.status != 404) { @@ -532,9 +589,49 @@ return mixerMode == MIX_MODES.PERSONAL; } - function getMixMode() { - return mixerMode; - } + function getMixMode() { + return mixerMode; + } + + function syncTracks(backingTracks) { + // double check that we are in session, since a bunch could have happened since then + if(!inSession()) { + logger.debug("dropping queued up sync tracks because no longer in session"); + return null; + } + + // this is a local change to our tracks. we need to tell the server about our updated track information + var inputTracks = context.JK.TrackHelpers.getUserTracks(context.jamClient); + + // backingTracks can be passed in as an optimization, so that we don't hit the backend excessively + if(backingTracks === undefined ) { + backingTracks = context.JK.TrackHelpers.getBackingTracks(context.jamClient); + } + + // create a trackSync request based on backend data + var syncTrackRequest = {}; + syncTrackRequest.client_id = app.clientId; + syncTrackRequest.tracks = inputTracks; + syncTrackRequest.backing_tracks = backingTracks; + syncTrackRequest.id = id(); + + return rest.putTrackSyncChange(syncTrackRequest) + .done(function() { + }) + .fail(function(jqXHR) { + if(jqXHR.status != 404) { + app.notify({ + "title": "Can't Sync Local Tracks", + "text": "The client is unable to sync local track information with the server. You should rejoin the session to ensure a good experience.", + "icon_url": "/assets/content/icon_alert_big.png" + }); + } + else { + logger.debug("Unable to sync local tracks because session is gone.") + } + + }) + } function onWebsocketDisconnected(in_error) { // kill the streaming of the session immediately @@ -678,45 +775,25 @@ // wait until we are fully in session before trying to sync tracks to server if(joinDeferred) { joinDeferred.done(function() { - - // double check that we are in session, since a bunch could have happened since then - if(!inSession()) { - logger.debug("dropping queued up sync tracks because no longer in session"); - return; - } - - // this is a local change to our tracks. we need to tell the server about our updated track information - var inputTracks = context.JK.TrackHelpers.getUserTracks(context.jamClient); - - // create a trackSync request based on backend data - var syncTrackRequest = {}; - syncTrackRequest.client_id = app.clientId; - syncTrackRequest.tracks = inputTracks; - syncTrackRequest.id = id(); - - rest.putTrackSyncChange(syncTrackRequest) - .done(function() { - }) - .fail(function(jqXHR) { - if(jqXHR.status != 404) { - app.notify({ - "title": "Can't Sync Local Tracks", - "text": "The client is unable to sync local track information with the server. You should rejoin the session to ensure a good experience.", - "icon_url": "/assets/content/icon_alert_big.png" - }); - } - else { - logger.debug("Unable to sync local tracks because session is gone.") - } - - }) + syncTracks(); }) } }, 100); } else if(inSession() && (text == 'RebuildMediaControl' || text == 'RebuildRemoteUserControl')) { - refreshCurrentSession(true); + + var backingTracks = context.JK.TrackHelpers.getBackingTracks(context.jamClient); + + // the way we know if backing tracks changes, or recordings are opened, is via this event. + // but we want to report to the user when backing tracks change; so we need to detect change on our own + if(previousBackingTracks != backingTracks) { + logger.debug("backing tracks changed") + syncTracks(backingTracks); + } + else { + refreshCurrentSession(true); + } } else if(inSession() && (text == 'Global Peer Input Mixer Mode')) { setMixerMode(MIX_MODES.MASTER); @@ -729,6 +806,10 @@ // Public interface this.id = id; this.start = start; + this.backingTrack = backingTrack; + this.backingTracks = backingTracks; + this.recordedBackingTracks = recordedBackingTracks; + this.metronomeActive = metronomeActive; this.setUserTracks = setUserTracks; this.recordedTracks = recordedTracks; this.jamTracks = jamTracks; @@ -736,6 +817,7 @@ this.joinSession = joinSession; this.leaveCurrentSession = leaveCurrentSession; this.refreshCurrentSession = refreshCurrentSession; + this.updateSession = updateSession; this.subscribe = subscribe; this.participantForClientId = participantForClientId; this.isPlayingRecording = isPlayingRecording; @@ -767,6 +849,19 @@ this.getParticipant = function(clientId) { return participantsEverSeen[clientId] }; + this.setBackingTrack = function(backingTrack) { + openBackingTrack = backingTrack; + }; + this.getBackingTrack = function() { + return openBackingTrack; + }; + this.hasShownAudioMediaMixerHelp = function() { + return shownAudioMediaMixerHelp; + } + this.markShownAudioMediaMixerHelp = function() { + shownAudioMediaMixerHelp = true; + } + // call to report if the current user was able to establish audio with the specified clientID this.setAudioEstablished = function(clientId, audioEstablished) { diff --git a/web/app/assets/javascripts/session_utils.js b/web/app/assets/javascripts/session_utils.js index 071f05567..d1e2358f7 100644 --- a/web/app/assets/javascripts/session_utils.js +++ b/web/app/assets/javascripts/session_utils.js @@ -125,7 +125,33 @@ } } + sessionUtils.ensureValidClient = function(app, gearUtils, successCallback) { + + if(!context.JK.guardAgainstBrowser(app)) { + return false; + } + + if (!context.JK.JamServer.connected) { + app.notifyAlert("Not Connected", 'To create or join a session, you must be connected to the server.'); + return false; + } + + gearUtils.guardAgainstInvalidConfiguration(app) + .fail(function() { + app.notify( + { title: "Unable to Join Session", + text: "You can only join a session once you have working audio gear and a tested internet connection." + }); + }) + .done(function() { + if (successCallback) { + successCallback(); + } + }); + } + sessionUtils.joinSession = function(sessionId) { + var hasInvitation = false; var session = null; // we need to do a real-time check of the session in case the settings have diff --git a/web/app/assets/javascripts/site_validator.js.coffee b/web/app/assets/javascripts/site_validator.js.coffee new file mode 100644 index 000000000..268d87f0f --- /dev/null +++ b/web/app/assets/javascripts/site_validator.js.coffee @@ -0,0 +1,167 @@ +$ = jQuery +context = window +context.JK ||= {}; + +context.JK.SiteValidator = class SiteValidator + + constructor: (site_type) -> + @EVENTS = context.JK.EVENTS + @rest = context.JK.Rest() + @site_type = site_type + @input_div = $(".site_validator#"+site_type+"_validator") + @data_input = @input_div.find('input') + @logger = context.JK.logger + @spinner = @input_div.find('span.spinner-small') + @checkmark = @input_div.find('.validate-checkmark') + this.setSiteStatus(null) + this.showFormatStatus() + @is_rec_src = false + @deferred_status_check = null + @is_validating = false + + init: () => + this.renderErrors({}) + @spinner.hide() + validator = this + @data_input.on 'blur', -> + validator.didBlur() + @data_input.on 'focus', -> + validator.showFormatStatus() + @data_input.on 'change', -> + @site_status = null + + dataToValidate: () => + url = @data_input.val() + if 0 < url.length + url.substring(0,2000) + else + null + + showFormatStatus: () => + data = this.dataToValidate() + yn = true + if data && ('url' == @site_type || @is_rec_src) + regexp = /(http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?/ + yn = regexp.test(this.dataToValidate()) + unless yn + @checkmark.hide() + yn + + didBlur: () => + if this.showFormatStatus() + this.validateSite() + + validateSite: () => + unless data = this.dataToValidate() + return null + this.setSiteStatus(null) + @spinner.show() + @rest.validateUrlSite(data, @site_type) + .done(this.processSiteCheckSucceed) + .fail(this.processSiteCheckFail) + + processSiteCheckSucceed: (response) => + @spinner.hide() + + if 'Valid Site' == response.message + this.setSiteStatus(true) + this.renderErrors({}) + if @deferred_status_check + @deferred_status_check.resolve() + else + this.setSiteStatus(false) + this.renderErrors(response) + if @deferred_status_check + @deferred_status_check.reject() + @deferred_status_check = null + @logger.debug("site_status = "+@site_status) + + processSiteCheckFail: (response) => + @logger.error("site check error") + this.setSiteStatus(false) + if @deferred_status_check + @deferred_status_check.reject() + @deferred_status_check = null + + setSiteStatus: (status) => + @site_status = status + @spinner.hide() + if true == status + @checkmark.show() + else + @checkmark.hide() + + siteIsValid: () => + this.setSiteStatus(true) + + siteIsInvalid: () => + this.setSiteStatus(false) + + renderErrors: (errors) => + errdiv = @input_div.find('.error') + if errmsg = context.JK.format_errors("site", errors) + errdiv.show() + errdiv.html(errmsg) + else + errdiv.hide() + errdiv.html('') + + state: () => + dfr = $.Deferred() + if null == @site_status + @deferred_status_check = dfr + this.validateSite() + else + if true == @site_status + dfr.resolve() + else + dfr.reject() + return dfr.promise() + + +context.JK.RecordingSourceValidator = class RecordingSourceValidator extends SiteValidator + constructor: (site_type) -> + super(site_type) + @recording_sources = [] + @is_rec_src = true + @add_btn = @input_div.find('a.add-recording-source') + + init: (sources) => + super() + if sources + @recording_sources = sources + @add_btn.on 'click', => + this.attemptAdd() + + processSiteCheckSucceed: (response) => + super(response) + @add_btn.removeClass('disabled') + @recording_sources.push({ url: response.data, recording_id: response.recording_id }) + + processSiteCheckFail: (response) => + super(response) + @add_btn.removeClass('disabled') + + didBlur: () => + # do nothing, validate on add only + + validateSite: () => + @add_btn.addClass('disabled') + super() + + attemptAdd: () => + if data = this.dataToValidate() + unless this.containsRecordingUrl(data) + this.validateSite() + + removeRecordingId: (recording_id) => + start_len = @recording_sources.length + @recording_sources = $.grep @recording_sources, (src_data) -> + src_data['recording_id'] != recording_id + start_len != @recording_sources.length + + containsRecordingUrl: (url) => + vals = $.grep @recording_sources, (src_data) -> + src_data['url'] == url + 0 < vals.length + diff --git a/web/app/assets/javascripts/sync_viewer.js.coffee b/web/app/assets/javascripts/sync_viewer.js.coffee index 3215359e2..0bf819d2e 100644 --- a/web/app/assets/javascripts/sync_viewer.js.coffee +++ b/web/app/assets/javascripts/sync_viewer.js.coffee @@ -27,16 +27,19 @@ context.JK.SyncViewer = class SyncViewer @list = @root.find('.list') @logList = @root.find('.log-list') @templateRecordedTrack = $('#template-sync-viewer-recorded-track') + @templateRecordedBackingTrack = $('#template-sync-viewer-recorded-backing-track') @templateStreamMix = $('#template-sync-viewer-stream-mix') @templateMix = $('#template-sync-viewer-mix') @templateNoSyncs = $('#template-sync-viewer-no-syncs') @templateRecordingWrapperDetails = $('#template-sync-viewer-recording-wrapper-details') @templateHoverRecordedTrack = $('#template-sync-viewer-hover-recorded-track') + @templateHoverRecordedBackingTrack = $('#template-sync-viewer-hover-recorded-backing-track') @templateHoverMix = $('#template-sync-viewer-hover-mix') @templateDownloadReset = $('#template-sync-viewer-download-progress-reset') @templateUploadReset = $('#template-sync-viewer-upload-progress-reset') @templateGenericCommand = $('#template-sync-viewer-generic-command') @templateRecordedTrackCommand = $('#template-sync-viewer-recorded-track-command') + @templateRecordedBackingTrackCommand = $('#template-sync-viewer-recorded-backing-track-command') @templateLogItem = $('#template-sync-viewer-log-item') @tabSelectors = @root.find('.dialog-tabs .tab') @tabs = @root.find('.tab-content') @@ -50,7 +53,8 @@ context.JK.SyncViewer = class SyncViewer them_upload_soon: 'them-upload-soon' missing: 'missing', me_uploaded: 'me-uploaded', - them_uploaded: 'them-uploaded' + them_uploaded: 'them-uploaded', + not_mine: 'not-mine' } @clientStates = { unknown: 'unknown', @@ -58,7 +62,8 @@ context.JK.SyncViewer = class SyncViewer hq: 'hq', sq: 'sq', missing: 'missing', - discarded: 'discarded' + discarded: 'discarded', + not_mine: 'not-mine' } throw "no sync-viewer" if not @root.exists() @@ -329,12 +334,138 @@ context.JK.SyncViewer = class SyncViewer $clientRetry.hide() $uploadRetry.hide() + updateBackingTrackState: ($track) => + clientInfo = $track.data('client-info') + serverInfo = $track.data('server-info') + myTrack = serverInfo.user.id == context.JK.currentUserId + + # determine client state + clientStateMsg = 'UNKNOWN' + clientStateClass = 'unknown' + clientState = @clientStates.unknown + + if serverInfo.mine + if serverInfo.download.should_download + if serverInfo.download.too_many_downloads + clientStateMsg = 'EXCESS DOWNLOADS' + clientStateClass = 'error' + clientState = @clientStates.too_many_uploads + else + if clientInfo? + if clientInfo.local_state == 'HQ' + clientStateMsg = 'HIGHEST QUALITY' + clientStateClass = 'hq' + clientState = @clientStates.hq + else if clientInfo.local_state == 'MISSING' + clientStateMsg = 'MISSING' + clientStateClass = 'missing' + clientState = @clientStates.missing + else + clientStateMsg = 'MISSING' + clientStateClass = 'missing' + clientState = @clientStates.missing + else + clientStateMsg = 'DISCARDED' + clientStateClass = 'discarded' + clientState = @clientStates.discarded + else + clientStateMsg = 'NOT MINE' + clientStateClass = 'not_mine' + clientState = @clientStates.not_mine + + # determine upload state + uploadStateMsg = 'UNKNOWN' + uploadStateClass = 'unknown' + uploadState = @uploadStates.unknown + + if serverInfo.mine + if !serverInfo.fully_uploaded + if serverInfo.upload.too_many_upload_failures + uploadStateMsg = 'UPLOAD FAILURE' + uploadStateClass = 'error' + uploadState = @uploadStates.too_many_upload_failures + else + if myTrack + if clientInfo? + if clientInfo.local_state == 'HQ' + uploadStateMsg = 'PENDING UPLOAD' + uploadStateClass = 'upload-soon' + uploadState = @uploadStates.me_upload_soon + else + uploadStateMsg = 'MISSING' + uploadStateClass = 'missing' + uploadState = @uploadStates.missing + else + uploadStateMsg = 'MISSING' + uploadStateClass = 'missing' + uploadState = @uploadStates.missing + else + uploadStateMsg = 'PENDING UPLOAD' + uploadStateClass = 'upload-soon' + uploadState = @uploadStates.them_upload_soon + else + uploadStateMsg = 'UPLOADED' + uploadStateClass = 'uploaded' + if myTrack + uploadState = @uploadStates.me_uploaded + else + uploadState = @uploadStates.them_uploaded + else + uploadStateMsg = 'NOT MINE' + uploadStateClass = 'not_mine' + uploadState = @uploadStates.not_mine + + + $clientState = $track.find('.client-state') + $clientStateMsg = $clientState.find('.msg') + $clientStateProgress = $clientState.find('.progress') + $uploadState = $track.find('.upload-state') + $uploadStateMsg = $uploadState.find('.msg') + $uploadStateProgress = $uploadState.find('.progress') + + $clientState.removeClass('discarded missing hq unknown error not-mine').addClass(clientStateClass).attr('data-state', clientState).data('custom-class', clientStateClass) + $clientStateMsg.text(clientStateMsg) + $clientStateProgress.css('width', '0') + $uploadState.removeClass('upload-soon error unknown missing uploaded not-mine').addClass(uploadStateClass).attr('data-state', uploadState).data('custom-class', uploadStateClass) + $uploadStateMsg.text(uploadStateMsg) + $uploadStateProgress.css('width', '0') + + # this allows us to make styling decisions based on the combination of both client and upload state. + $track.addClass("clientState-#{clientStateClass}").addClass("uploadState-#{uploadStateClass}") + + $clientRetry = $clientState.find('.retry') + $uploadRetry = $uploadState.find('.retry') + + if gon.isNativeClient + # handle client state + + # only show RETRY button if you have a SQ or if it's missing, and it's been uploaded already + if (clientState == @clientStates.missing) and (uploadState == @uploadStates.me_uploaded or uploadState == @uploadStates.them_uploaded) + $clientRetry.show() + else + $clientRetry.hide() + + # only show RETRY button if you have the HQ track, it's your track, and the server doesn't yet have it + if myTrack and @clientStates.hq and (uploadState == @uploadStates.error or uploadState == @uploadStates.me_upload_soon) + $uploadRetry.show() + else + $uploadRetry.hide() + else + $clientRetry.hide() + $uploadRetry.hide() + + associateClientInfo: (recording) => for clientInfo in recording.local_tracks $track = @list.find(".recorded-track[data-recording-id='#{recording.recording_id}'][data-client-track-id='#{clientInfo.client_track_id}']") $track.data('client-info', clientInfo) $track.data('total-size', recording.size) + for clientInfo in recording.backing_tracks + $track = @list.find(".recorded-backing-track[data-recording-id='#{recording.recording_id}'][data-client-track-id='#{clientInfo.client_track_id}']") + $track.data('client-info', clientInfo) + $track.data('total-size', recording.size) + $track = @list.find(".mix[data-recording-id='#{recording.recording_id}']") $track.data('client-info', recording.mix) $track.data('total-size', recording.size) @@ -457,11 +588,77 @@ context.JK.SyncViewer = class SyncViewer uploadStateClass: uploadStateClass} {variable: 'data'}) - onHoverOfStateIndicator: () -> + displayBackingTrackHover: ($recordedTrack) => + $clientState = $recordedTrack.find('.client-state') + $clientStateMsg = $clientState.find('.msg') + clientStateClass = $clientState.data('custom-class') + clientState = $clientState.attr('data-state') + clientInfo = $recordedTrack.data('client-info') + + $uploadState = $recordedTrack.find('.upload-state') + $uploadStateMsg = $uploadState.find('.msg') + uploadStateClass = $uploadState.data('custom-class') + uploadState = $uploadState.attr('data-state') + serverInfo = $recordedTrack.data('server-info') + + # decide on special case strings first + + summary = '' + if clientState == @clientStates.not_mine && @uploadStates.them_uploaded + # this is not our backing track + summary = "#{serverInfo.user.name} opened this backing track. Due to legal concerns, we can not distribute it to you." + else if clientState == @clientStates.not_mine && @uploadStates.them_upload_soon + # this is not our backing track + summary = "#{serverInfo.user.name} has not yet uploaded their backing track." + else if clientState == @clientStates.missing && uploadState == @uploadStates.me_uploaded + # we have no version of the track at all, and the other user has uploaded the HQ version... it's coming soon! + summary = "You have previously uploaded the high-quality version of this track. JamKazam will soon restore it and then this backing track will no longer be missing." + else if clientState == @clientStates.discarded && (uploadState == @uploadStates.me_uploaded or uploadState == @uploadStates.them_uploaded) + # we decided not to keep the recording... so it's important to clarify why they are seeing it at all + summary = "When this recording was made, you elected to not keep it. JamKazam already uploaded your high-quality backing track for the recording, because at least one other person decided to keep the recording and needs your backing track to make a high-quality mix." + else if clientState == @clientStates.discarded + # we decided not to keep the recording... so it's important to clarify why they are seeing it at all + summary = "When this recording was made, you elected to not keep it. JamKazam will still try to upload your high-quality backing track for the recording, because at least one other person decided to keep the recording and needs your backing track to make a high-quality mix." + else if clientState == @clientStates.hq and ( uploadState == @uploadStates.me_uploaded ) + summary = "Both you and the JamKazam server have the high-quality version of this track. Once all the other tracks for this recording are also synchronized, then the final mix can be made." + + clientStateDefinition = switch clientState + when @clientStates.too_many_downloads then "This backing track has been downloaded an unusually large number of times. No more downloads are allowed." + when @clientStates.hq then "HIGHEST QUALITY means you have the original version of this backing track." + when @clientStates.missing then "MISSING means you do not have this backing track anymore." + when @clientStates.discarded then "DISCARDED means you chose to not keep this recording when the recording was over." + when @clientStates.not_mine then "NOT MINE means someone else opened and played this backing track." + else 'There is no help for this state' + + uploadStateDefinition = switch uploadState + when @uploadStates.too_many_upload_failures then "Failed attempts at uploading this backing track has happened an unusually large times. No more uploads will be attempted." + when @uploadStates.me_upload_soon then "PENDING UPLOAD means your JamKazam application will upload this backing track soon." + when @uploadStates.them_up_soon then "PENDING UPLOAD means #{serverInfo.user.name} will upload this backing track soon." + when @uploadStates.me_uploaded then "UPLOADED means you have already uploaded this backing track." + when @uploadStates.them_uploaded then "UPLOADED means #{serverInfo.user.name} has already uploaded this backing track." + when @uploadStates.missing then "MISSING means your JamKazam application does not have this backing track, and the server does not either." + when @uploadStates.not_mine then "NOT MINE means someone else opened and played this backing track." + + context._.template(@templateHoverRecordedBackingTrack.html(), + {summary: summary, + clientStateDefinition: clientStateDefinition, + uploadStateDefinition: uploadStateDefinition, + clientStateMsg: $clientStateMsg.text(), + uploadStateMsg: $uploadStateMsg.text(), + clientStateClass: clientStateClass, + uploadStateClass: uploadStateClass} + {variable: 'data'}) + + onTrackHoverOfStateIndicator: () -> $recordedTrack = $(this).closest('.recorded-track.sync') self = $recordedTrack.data('sync-viewer') self.displayTrackHover($recordedTrack) + onBackingTrackHoverOfStateIndicator: () -> + $recordedTrack = $(this).closest('.recorded-backing-track.sync') + self = $recordedTrack.data('sync-viewer') + self.displayBackingTrackHover($recordedTrack) + onStreamMixHover: () -> $streamMix = $(this).closest('.stream-mix.sync') self = $streamMix.data('sync-viewer') @@ -512,6 +709,39 @@ context.JK.SyncViewer = class SyncViewer return false + retryDownloadRecordedBackingTrack: (e) => + $retry = $(e.target) + $track = $retry.closest('.recorded-backing-track') + serverInfo = $track.data('server-info') + + console.log("track serverInfo", $track, serverInfo) + this.sendCommand($retry, { + type: 'recorded_backing_track', + action: 'download' + queue: 'download', + recording_id: serverInfo.recording_id + track_id: serverInfo.client_track_id + }) + + return false + + retryUploadRecordedBackingTrack: (e) => + $retry = $(e.target) + $track = $retry.closest('.recorded-backing-track') + serverInfo = $track.data('server-info') + + console.log("track serverInfo", $track, serverInfo) + + this.sendCommand($retry, { + type: 'recorded_backing_track', + action: 'upload' + queue: 'upload', + recording_id: serverInfo.recording_id + track_id: serverInfo.client_track_id + }) + + return false + createMix: (userSync) => recordingInfo = null if userSync == 'fake' @@ -548,8 +778,26 @@ context.JK.SyncViewer = class SyncViewer $uploadStateRetry.click(this.retryUploadRecordedTrack) context.JK.bindHoverEvents($track) context.JK.bindInstrumentHover($track, {positions:['top'], shrinkToFit: true}); - context.JK.hoverBubble($clientState, this.onHoverOfStateIndicator, {width:'450px', closeWhenOthersOpen: true, positions:['left']}) - context.JK.hoverBubble($uploadState, this.onHoverOfStateIndicator, {width:'450px', closeWhenOthersOpen: true, positions:['right']}) + context.JK.hoverBubble($clientState, this.onTrackHoverOfStateIndicator, {width:'450px', closeWhenOthersOpen: true, positions:['left']}) + context.JK.hoverBubble($uploadState, this.onTrackHoverOfStateIndicator, {width:'450px', closeWhenOthersOpen: true, positions:['right']}) + $clientState.addClass('is-native-client') if gon.isNativeClient + $uploadState.addClass('is-native-client') if gon.isNativeClient + $track + + createBackingTrack: (userSync) => + $track = $(context._.template(@templateRecordedBackingTrack.html(), userSync, {variable: 'data'})) + $track.data('server-info', userSync) + $track.data('sync-viewer', this) + $clientState = $track.find('.client-state') + $uploadState = $track.find('.upload-state') + $clientStateRetry = $clientState.find('.retry') + $clientStateRetry.click(this.retryDownloadRecordedBackingTrack) + $uploadStateRetry = $uploadState.find('.retry') + $uploadStateRetry.click(this.retryUploadRecordedBackingTrack) + context.JK.bindHoverEvents($track) + context.JK.bindInstrumentHover($track, {positions:['top'], shrinkToFit: true}); + context.JK.hoverBubble($clientState, this.onBackingTrackHoverOfStateIndicator, {width:'450px', closeWhenOthersOpen: true, positions:['left']}) + context.JK.hoverBubble($uploadState, this.onBackingTrackHoverOfStateIndicator, {width:'450px', closeWhenOthersOpen: true, positions:['right']}) $clientState.addClass('is-native-client') if gon.isNativeClient $uploadState.addClass('is-native-client') if gon.isNativeClient $track @@ -687,6 +935,8 @@ context.JK.SyncViewer = class SyncViewer for userSync in response.entries if userSync.type == 'recorded_track' @list.append(this.createTrack(userSync)) + if userSync.type == 'recorded_backing_track' + @list.append(this.createBackingTrack(userSync)) else if userSync.type == 'mix' @list.append(this.createMix(userSync)) else if userSync.type == 'stream_mix' @@ -707,6 +957,8 @@ context.JK.SyncViewer = class SyncViewer for track in @list.find('.recorded-track.sync') this.updateTrackState($(track)) + for track in @list.find('.recorded-backing-track.sync') + this.updateBackingTrackState($(track)) for streamMix in @list.find('.stream-mix.sync') this.updateStreamMixState($(streamMix)) @@ -726,6 +978,18 @@ context.JK.SyncViewer = class SyncViewer deferred.resolve(matchingTrack.data('server-info')) return deferred + resolveBackingTrack: (commandMetadata) => + recordingId = commandMetadata['recording_id'] + clientTrackId = commandMetadata['track_id'] + + matchingTrack = @list.find(".recorded-backing-track[data-recording-id='#{recordingId}'][data-client-track-id='#{clientTrackId}']") + if matchingTrack.length == 0 + return @rest.getRecordedBackingTrack({recording_id: recordingId, track_id: clientTrackId}) + else + deferred = $.Deferred(); + deferred.resolve(matchingTrack.data('server-info')) + return deferred + renderFullUploadRecordedTrack: (serverInfo) => $track = $(context._.template(@templateRecordedTrackCommand.html(), $.extend(serverInfo, {action:'UPLOADING'}), {variable: 'data'})) $busy = @uploadProgress.find('.busy') @@ -738,6 +1002,18 @@ context.JK.SyncViewer = class SyncViewer $busy.empty().append($track) @downloadProgress.find('.progress').css('width', '0%') + renderFullUploadRecordedBackingTrack: (serverInfo) => + $track = $(context._.template(@templateRecordedBackingTrackCommand.html(), $.extend(serverInfo, {action:'UPLOADING'}), {variable: 'data'})) + $busy = @uploadProgress.find('.busy') + $busy.empty().append($track) + @uploadProgress.find('.progress').css('width', '0%') + + renderFullDownloadRecordedBackingTrack: (serverInfo) => + $track = $(context._.template(@templateRecordedBackingTrackCommand.html(), $.extend(serverInfo, {action:'DOWNLOADING'}), {variable: 'data'})) + $busy = @downloadProgress.find('.busy') + $busy.empty().append($track) + @downloadProgress.find('.progress').css('width', '0%') + # this will either show a generic placeholder, or immediately show the whole track renderDownloadRecordedTrack: (commandId, commandMetadata) => # try to find the info in the list; if we can't find it, then resolve it @@ -756,6 +1032,23 @@ context.JK.SyncViewer = class SyncViewer deferred.done(this.renderFullUploadRecordedTrack).fail(()=> @logger.error("unable to fetch recorded_track info") ) + # this will either show a generic placeholder, or immediately show the whole track + renderDownloadRecordedBackingTrack: (commandId, commandMetadata) => + # try to find the info in the list; if we can't find it, then resolve it + deferred = this.resolveBackingTrack(commandMetadata) + if deferred.state() == 'pending' + this.renderGeneric(commandId, 'download', commandMetadata) + + deferred.done(this.renderFullDownloadRecordedBackingTrack).fail(()=> @logger.error("unable to fetch recorded_backing_track info") ) + + renderUploadRecordedBackingTrack: (commandId, commandMetadata) => + # try to find the info in the list; if we can't find it, then resolve it + deferred = this.resolveBackingTrack(commandMetadata) + if deferred.state() == 'pending' + this.renderGeneric(commandId, 'upload', commandMetadata) + + deferred.done(this.renderFullUploadRecordedBackingTrack).fail(()=> @logger.error("unable to fetch recorded_backing_track info") ) + renderGeneric: (commandId, category, commandMetadata) => commandMetadata.displayType = this.displayName(commandMetadata) @@ -794,6 +1087,8 @@ context.JK.SyncViewer = class SyncViewer @downloadProgress.addClass('busy') if commandMetadata.type == 'recorded_track' and commandMetadata.action == 'download' this.renderDownloadRecordedTrack(commandId, commandMetadata) + else if commandMetadata.type == 'recorded_backing_track' and commandMetadata.action == 'download' + this.renderDownloadRecordedBackingTrack(commandId, commandMetadata) else this.renderGeneric(commandId, 'download', commandMetadata) else if commandMetadata.queue == 'upload' @@ -803,6 +1098,8 @@ context.JK.SyncViewer = class SyncViewer @uploadProgress.addClass('busy') if commandMetadata.type == 'recorded_track' and commandMetadata.action == 'upload' this.renderUploadRecordedTrack(commandId, commandMetadata) + else if commandMetadata.type == 'recorded_backing_track' and commandMetadata.action == 'upload' + this.renderUploadRecordedBackingTrack(commandId, commandMetadata) else this.renderGeneric(commandId, 'upload', commandMetadata) else if commandMetadata.queue == 'cleanup' @@ -820,6 +1117,12 @@ context.JK.SyncViewer = class SyncViewer $track.data('server-info', userSync) this.associateClientInfo(clientRecordings.recordings[0]) this.updateTrackState($track) + else if userSync.type == 'recorded_backing_track' + $track = @list.find(".sync[data-id='#{userSync.id}']") + continue if $track.length == 0 + $track.data('server-info', userSync) + this.associateClientInfo(clientRecordings.recordings[0]) + this.updateBackingTrackState($track) else if userSync.type == 'mix' # check if there is a virtual mix 1st; if so, update it $mix = @list.find(".mix.virtual[data-recording-id='#{userSync.recording.id}']") @@ -839,20 +1142,6 @@ context.JK.SyncViewer = class SyncViewer updateSingleRecording: (recording_id) => @rest.getUserSyncs({recording_id: recording_id}).done(this.renderSingleRecording) - updateSingleRecordedTrack: ($track) => - serverInfo = $track.data('server-info') - @rest.getUserSync({user_sync_id: serverInfo.id}) - .done((userSync) => - # associate new server-info with this track - $track.data('server-info', userSync) - # associate new client-info with this track - clientRecordings = context.jamClient.GetLocalRecordingState(recordings: [userSync.recording]) - this.associateClientInfo(clientRecordings.recordings[0]) - - this.updateTrackState($track) - ) - .fail(@app.ajaxError) - updateProgressOnSync: ($track, queue, percentage) => state = if queue == 'upload' then '.upload-state' else '.client-state' $progress = $track.find("#{state} .progress") @@ -892,10 +1181,10 @@ context.JK.SyncViewer = class SyncViewer $progress = @downloadProgress.find('.progress') $progress.css('width', percentage + '%') - if @downloadMetadata.type == 'recorded_track' + if @downloadMetadata.type == 'recorded_track' or @downloadMetadata.type == 'recorded_backing_track' clientTrackId = @downloadMetadata['track_id'] recordingId = @downloadMetadata['recording_id'] - $matchingTrack = @list.find(".recorded-track.sync[data-recording-id='#{recordingId}'][data-client-track-id='#{clientTrackId}']") + $matchingTrack = @list.find(".track-item.sync[data-recording-id='#{recordingId}'][data-client-track-id='#{clientTrackId}']") if $matchingTrack.length > 0 this.updateProgressOnSync($matchingTrack, 'download', percentage) @@ -903,10 +1192,10 @@ context.JK.SyncViewer = class SyncViewer $progress = @uploadProgress.find('.progress') $progress.css('width', percentage + '%') - if @uploadMetadata.type == 'recorded_track' and @uploadMetadata.action == 'upload' + if (@uploadMetadata.type == 'recorded_track' or @uploadMetadata.type == 'recorded_backing_track') and @uploadMetadata.action == 'upload' clientTrackId = @uploadMetadata['track_id'] recordingId = @uploadMetadata['recording_id'] - $matchingTrack = @list.find(".recorded-track.sync[data-recording-id='#{recordingId}'][data-client-track-id='#{clientTrackId}']") + $matchingTrack = @list.find(".track-item.sync[data-recording-id='#{recordingId}'][data-client-track-id='#{clientTrackId}']") if $matchingTrack.length > 0 this.updateProgressOnSync($matchingTrack, 'upload', percentage) else if @uploadMetadata.type == 'stream_mix' and @uploadMetadata.action == 'upload' @@ -977,15 +1266,15 @@ context.JK.SyncViewer = class SyncViewer this.logResult(data.commandMetadata, false, data.commandReason, true) displayName: (metadata) => - if metadata.type == 'recorded_track' && metadata.action == 'download' + if (metadata.type == 'recorded_track' || metadata.type == 'recorded_backing_track') && metadata.action == 'download' return 'DOWNLOADING TRACK' - else if metadata.type == 'recorded_track' && metadata.action == 'upload' + else if (metadata.type == 'recorded_track' || metadata.type == 'recorded_backing_track') && metadata.action == 'upload' return 'UPLOADING TRACK' else if metadata.type == 'mix' && metadata.action == 'download' return 'DOWNLOADING MIX' - else if metadata.type == 'recorded_track' && metadata.action == 'convert' + else if (metadata.type == 'recorded_track' || metadata.type == 'recorded_backing_track') && metadata.action == 'convert' return 'COMPRESSING TRACK' - else if metadata.type == 'recorded_track' && metadata.action == 'delete' + else if (metadata.type == 'recorded_track' || metadata.type == 'recorded_backing_track') && metadata.action == 'delete' return 'CLEANUP TRACK' else if metadata.type == 'stream_mix' && metadata.action == 'upload' return 'UPLOADING STREAM MIX' diff --git a/web/app/assets/javascripts/trackHelpers.js b/web/app/assets/javascripts/trackHelpers.js index 6b30035e0..a08885af3 100644 --- a/web/app/assets/javascripts/trackHelpers.js +++ b/web/app/assets/javascripts/trackHelpers.js @@ -30,6 +30,28 @@ return tracks; }, + getBackingTracks: function(jamClient) { + var mediaTracks = context.JK.TrackHelpers.getTracks(jamClient, 4); + + console.log("mediaTracks", mediaTracks) + + var backingTracks = [] + context._.each(mediaTracks, function(mediaTrack) { + // the check for 'not managed' means this is not a track opened by a recording, basically + // we do not try and sync these sorts of backing tracks to the server, because they + // are already encompassed by + if(mediaTrack.media_type == "BackingTrack" && !mediaTrack.managed) { + var track = {}; + track.client_track_id = mediaTrack.persisted_track_id; + track.client_resource_id = mediaTrack.rid; + track.filename = mediaTrack.filename; + backingTracks.push(track); + } + }) + + return backingTracks; + }, + /** * This function resolves which tracks to configure for a user * when creating or joining a session. By default, tracks are pulled diff --git a/web/app/assets/javascripts/ui_helper.js b/web/app/assets/javascripts/ui_helper.js index 9213c43cb..01f204bfb 100644 --- a/web/app/assets/javascripts/ui_helper.js +++ b/web/app/assets/javascripts/ui_helper.js @@ -6,6 +6,7 @@ context.JK.UIHelper = function(app) { var logger = context.JK.logger; var rest = new context.JK.Rest(); + var sessionUtils = context.JK.SessionUtils; function addSessionLike(sessionId, userId, $likeCountSelector, $likeButtonSelector) { rest.addSessionLike(sessionId, userId) @@ -54,9 +55,11 @@ } function launchSessionStartDialog(session) { - var sessionStartDialog = new JK.SessionStartDialog(JK.app, session); - sessionStartDialog.initialize(); - return sessionStartDialog.showDialog(); + sessionUtils.ensureValidClient(app, context.JK.GearUtils, function() { + var sessionStartDialog = new JK.SessionStartDialog(JK.app, session); + sessionStartDialog.initialize(); + return sessionStartDialog.showDialog(); + }); } this.addSessionLike = addSessionLike; diff --git a/web/app/assets/javascripts/utils.js b/web/app/assets/javascripts/utils.js index f4ac9cee0..fef57ccf5 100644 --- a/web/app/assets/javascripts/utils.js +++ b/web/app/assets/javascripts/utils.js @@ -991,6 +991,15 @@ return hasFlash; } + context.JK.getNameOfFile = function(filename) { + + var index = filename.lastIndexOf('/'); + if(index == -1) { + index = filename.lastIndexOf('\\'); + } + return index == -1 ? filename : filename.substring(index + 1, filename.length) + } + context.JK.hasOneConfiguredDevice = function () { var result = context.jamClient.FTUEGetGoodConfigurationList(); logger.debug("hasOneConfiguredDevice: ", result); diff --git a/web/app/assets/stylesheets/client/common.css.scss b/web/app/assets/stylesheets/client/common.css.scss index e2f174fe0..a349729f5 100644 --- a/web/app/assets/stylesheets/client/common.css.scss +++ b/web/app/assets/stylesheets/client/common.css.scss @@ -208,6 +208,10 @@ $fair: #cc9900; background-color: $error; } + &.not_mine { + background-color: $good; + } + &.discarded { background-color: $unknown; } @@ -252,6 +256,10 @@ $fair: #cc9900; background-color: $good; } + &.not_mine { + background-color: $good; + } + .retry { display:none; position:absolute; diff --git a/web/app/assets/stylesheets/client/help.css.scss b/web/app/assets/stylesheets/client/help.css.scss index 4755d711f..512988936 100644 --- a/web/app/assets/stylesheets/client/help.css.scss +++ b/web/app/assets/stylesheets/client/help.css.scss @@ -45,7 +45,7 @@ body.jam, body.web, .dialog{ } } - .help-hover-recorded-tracks, .help-hover-stream-mix { + .help-hover-recorded-tracks, .help-hover-stream-mix, .help-hover-recorded-backing-tracks { font-size:12px; padding:5px; diff --git a/web/app/assets/stylesheets/client/jamkazam.css.scss b/web/app/assets/stylesheets/client/jamkazam.css.scss index 8ccd7a022..ab1444769 100644 --- a/web/app/assets/stylesheets/client/jamkazam.css.scss +++ b/web/app/assets/stylesheets/client/jamkazam.css.scss @@ -593,5 +593,16 @@ body.jam .icheckbox_minimal { display:inline-block; } +} +#jamblaster-notice { + position:absolute; + width:100%; + bottom:105%; + border-color:#ED3618; + border-style:solid; + border-width:1px; + padding:10px; + text-align:center; + @include border_box_sizing; } \ No newline at end of file diff --git a/web/app/assets/stylesheets/client/session.css.scss b/web/app/assets/stylesheets/client/session.css.scss index 53cbfd929..7f38661c6 100644 --- a/web/app/assets/stylesheets/client/session.css.scss +++ b/web/app/assets/stylesheets/client/session.css.scss @@ -18,7 +18,7 @@ .track { width:70px; - height:290px; + height:300px; display:inline-block; margin-right:8px; position:relative; @@ -50,6 +50,9 @@ vertical-align:top; } + .session-recordedtracks-container { + //display: block; + } .recording-controls { display:none; @@ -74,17 +77,23 @@ left:5px; } - .open-media-file-header { + .open-media-file-header, .use-metronome-header { font-size:16px; line-height:100%; margin:0; - float:left; img { position:relative; top:3px; } } + .open-media-file-header { + float: left; + } + + .use-metronome-header { + clear: both; + } .open-media-file-options { font-size:16px; @@ -110,8 +119,21 @@ .session-recording-name-wrapper{ position:relative; - white-space:nowrap; - display:none; + white-space:normal; + display:none; + + .session-recording-name { + position:relative; + margin-top:9px; + margin-bottom:8px; + font-size:16px; + height: 22px; + min-height: 22px; + max-height: 22px; + display: inline-block; + width:60%; + text-overflow:ellipsis; + } .session-add { margin-top:9px; @@ -205,6 +227,9 @@ table.vu td { position: absolute; text-align:center; width: 55px; + height: 15px; + min-height: 11px; + max-height: 33px; max-width: 55px; white-space:normal; top: 3px; @@ -212,6 +237,7 @@ table.vu td { font-family: Arial, Helvetica, sans-serif; font-size: 11px; font-weight: bold; + text-overflow:ellipsis; } .track-close { @@ -323,8 +349,6 @@ table.vu td { color: inherit; } - - .session-add { margin-top:9px; margin-bottom:8px; @@ -349,7 +373,7 @@ table.vu td { overflow-x:auto; overflow-y:hidden; width:100%; - height:340px; + height:370px; float:left; white-space:nowrap; } @@ -486,12 +510,9 @@ table.vu td { .track-gain { position:absolute; width:28px; - height:83px; + height:63px; top:138px; left:23px; - background-image:url('/assets/content/bkg_gain_slider.png'); - background-repeat:repeat-y; - background-position:bottom; } .track-gain-wrapper { @@ -518,6 +539,45 @@ table.vu td { height: 18px; background-image:url('/assets/content/icon_mute.png'); background-repeat:no-repeat; + text-align: center; +} + +.track-icon-loop { + cursor: pointer; + position:absolute; + top:250px; + left:11px; + width: 20px; + height: 18px; + text-align: center; + font-size: 8pt; + font-weight: bold; + + .icheckbox_minimal { + top:5px; + margin-right:5px; + } +} + +.metronome-selects { + position: absolute; + width: 52px; + top:252px; + left: 10px; + + height: 18px; + text-align: center; + //display: block; + //padding: 4px; + + select.metronome-select { + position: relative; + padding: 4px 0px 4px 0px; + margin: 0; + width: 100% !important; + font-size: 10px; + font-weight: normal; + } } .track-icon-mute.muted { @@ -528,12 +588,12 @@ table.vu td { } .session-livetracks .track-icon-mute, .session-recordings .track-icon-mute { - top:245px; + top:225px; } .track-icon-settings { position:absolute; - top:255px; + top:235px; left:28px; } diff --git a/web/app/assets/stylesheets/client/sessionList.css.scss b/web/app/assets/stylesheets/client/sessionList.css.scss index 16d208a59..b0df3def5 100644 --- a/web/app/assets/stylesheets/client/sessionList.css.scss +++ b/web/app/assets/stylesheets/client/sessionList.css.scss @@ -1,7 +1,7 @@ @import "client/common"; -table.findsession-table, table.local-recordings, table.open-jam-tracks, #account-session-detail { +table.findsession-table, table.local-recordings, table.open-jam-tracks, table.open-backing-tracks, #account-session-detail { .latency-unacceptable { width: 50px; @@ -64,7 +64,7 @@ table.findsession-table, table.local-recordings, table.open-jam-tracks, #account text-align:center; } } -table.findsession-table, table.local-recordings, table.open-jam-tracks { +table.findsession-table, table.local-recordings, table.open-jam-tracks, table.open-backing-tracks { width:98%; height:10%; font-size:11px; diff --git a/web/app/assets/stylesheets/client/site_validator.css.scss b/web/app/assets/stylesheets/client/site_validator.css.scss new file mode 100644 index 000000000..4534ebd8c --- /dev/null +++ b/web/app/assets/stylesheets/client/site_validator.css.scss @@ -0,0 +1,38 @@ +@import "client/common"; + +.site_validator { + + .validator-input { + float: left; + } + .validator-add-rec { + float: left; + } + + input { + width: 100%; + padding: 5px 5px 5px 30px; + float: left; + } + .validate-checkmark { + background-image: url('/assets/content/icon_checkmark_circle.png'); + background-repeat:no-repeat; + background-position:center; + width:32px; + height:32px; + background-size: 50% 50%; + display:inline-block; + vertical-align: middle; + position: relative; + margin-top: -40px; + left: 0px; + } + .error { + } + span.spinner-small { + display:inline-block; + vertical-align: middle; + position: relative; + margin-top: -40px; + } +} diff --git a/web/app/assets/stylesheets/dialogs/openBackingTrackDialog.css.scss b/web/app/assets/stylesheets/dialogs/openBackingTrackDialog.css.scss new file mode 100644 index 000000000..06b4ffb2e --- /dev/null +++ b/web/app/assets/stylesheets/dialogs/openBackingTrackDialog.css.scss @@ -0,0 +1,40 @@ +@import "client/common"; + +#open-backing-track-dialog { + table.open-backing-tracks { + tbody { + tr:hover { + background-color: #777; + cursor:pointer; + } + + tr[data-local-state=MISSING], tr[data-local-state=PARTIALLY_MISSING] { + background-color:#777; + color:#aaa; + } + } + } + + .right { + margin-right:10px; + } + + .help-links { + text-align: left; + position: absolute; + margin: 0 auto; + width: 70%; + //left: 15%; + font-size: 12px; + padding-top:5px; + + a { + margin:0 10px; + } + } + + .paginator-holder { + padding-top:3px; + } +} + diff --git a/web/app/assets/stylesheets/landings/landing_page.css.scss b/web/app/assets/stylesheets/landings/landing_page.css.scss index 51491d12a..ce9f7496b 100644 --- a/web/app/assets/stylesheets/landings/landing_page.css.scss +++ b/web/app/assets/stylesheets/landings/landing_page.css.scss @@ -9,10 +9,412 @@ body.web.landing_page { display:none; } - &.wo_1 { - .landing-content h1 { - margin-left:45px; + .badge-number { + font-size:125%; + color:white; + background-color:$ColorScreenPrimary; + width:30px; + height:30px; + //position:absolute; + //left:-10px; + //top:4px; + margin-right:10px; + -webkit-border-radius:50%; + -moz-border-radius:50%; + border-radius:50%; + text-align:center; + display:inline-block; + border:2px solid white; + } + + &.kick { + + h1 { + } + p { + margin-bottom:15px; + line-height:120%; + } + .signup-wrapper { + width:75%; + text-align:center; + } + .landing-tag { + left: 50%; + } + + .cta-container { + width:100%; + text-align:left; + margin-left:0% !important; + h1 { + margin: 0 0 10px 0; + } + a {margin-bottom:0 !important} + } + + .column:nth-child(1) { + width:50% !important; + + .cta-container { + margin-top:20px; + } + } + + .column:nth-child(2) { + width:50% !important; + + h2 { + margin-bottom:30px; + } + + .cta-container a { + + margin-bottom:8px; + } + } + + } + + &.kick_4 { + + h1 { + + } + + .column h2 { + position:absolute; + font-size:12px; + margin-left:45px; + top:45px; + } + p { + margin-bottom:15px; + line-height:120%; + } + .signup-wrapper { + width:75%; + text-align:center; + } + .landing-tag { + left: 50%; + } + + .cta-container { + width:100%; + text-align:left; + margin-left:0% !important; + h1 { + margin: 0 0 10px 0; + } + a {margin-bottom:0 !important} + } + + .column:nth-child(1) { + width:50% !important; + + .cta-container { + margin-top:20px; + } + } + + .column:nth-child(2) { + width:50% !important; + + h2 { + margin-bottom:30px; + } + + .cta-container { + + width:80%; + + a { + width:90%; + text-align:center; + + img { + width:65%; + margin-top:20px; + } + } + } + } + + } + + + &.kick_2 { + + .linker { + -webkit-border-radius:6px; + -moz-border-radius:6px; + border-radius:6px; + border-width:1px; + border-style:solid; + border-color:$ColorScreenPrimary; + position:absolute; + width:92%; + left:-1%; + top:60px; + height:270px; + } + h1 { + //padding-left:20px; + } + + .youtube-time-tip { + font-style:italic; + } + + p { + line-height:120%; + } + .signup-wrapper { + width:75%; + text-align:center; + } + .landing-tag { + left: 50%; + } + + .cta-container { + width:85%; + text-align:left; + margin-left:0% !important; + h1 { + margin: 0 0 10px 0; + } + a {margin-bottom:0 !important} + } + + .back-us { + margin-top:15px; + width:75%; + margin-left:30px; + img { + width:100%; + } + } + .column.one { + width:50% !important; + + .cta-container { + margin-top:20px; + } + } + + .column.two { + width:45% !important; + + h2 { + margin-bottom:30px; + } + + .cta-container a { + + margin-bottom:8px; + } + } + + .testimonial { + margin-top:70px; + p { + font-size:18px !important; + } + } + + .signature { + margin-top:15px; + margin-left:30%; + font-size:18px; + position:relative; + + .dash { + position:absolute; + width:20px; + left:-10px; + top:0; + } + } + + .signature-details { + margin-top:5px; + font-style:italic; + margin-left:30%; + } + + .signup-holder { + position:absolute; + top:350px; + width:45%; + } + .signup-info { + + } + + .signup-wrapper { + width:90%; + } + + } + + &.kick_3 { + + .linker { + -webkit-border-radius:6px; + -moz-border-radius:6px; + border-radius:6px; + border-width:1px; + border-style:solid; + border-color:$ColorScreenPrimary; + position:absolute; + width:92%; + left:-1%; + top:56px; + height:270px; + } + + .column h1 { + font-size:16px !important; + } + + .youtube-time-tip { + font-style:italic; + } + + p { + line-height:120%; + } + .signup-wrapper { + width:75%; + text-align:center; + } + .landing-tag { + left: 50%; + } + + .cta-container { + width:85%; + text-align:left; + margin-left:0% !important; + h1 { + margin: 0 0 10px 0; + } + a {margin-bottom:0 !important} + } + + .back-us { + margin-top:15px; + width:75%; + margin-left:30px; + + img { + width:100%; + } + } + .column.one { + width:50% !important; + + .cta-container { + margin-top:20px; + } + } + + .column.two { + width:45% !important; + + h2 { + margin-bottom:30px; + } + + .cta-container a { + + margin-bottom:8px; + } + } + + .testimonial { + margin-top:70px; + p { + font-size:18px !important; + } + } + + .signature { + margin-top:15px; + margin-left:30%; + font-size:18px; + position:relative; + + .dash { + position:absolute; + width:20px; + left:-10px; + top:0; + } + } + + .signature-details { + margin-top:5px; + font-style:italic; + margin-left:30%; + } + + .signup-holder { + position:absolute; + top:350px; + width:45%; + } + .signup-info { + + } + + .signup-wrapper { + width:90%; + } + + } + + + &.wo_1 { + + .landing-tag { + left: 50%; + } + + .cta-container { + width:75%; + text-align:center; + margin-left:0% !important; + + h2 { + margin-left:0px !important; + } + } + + .column:nth-child(1) { + width:50% !important; + } + + .column:nth-child(2) { + width:50% !important; + h1 { + + } + + h2 { + margin-bottom:30px; + } + + .cta-container a { + + margin-bottom:8px; + } + } + + } &.wo_3 { .landing-content h1 { diff --git a/web/app/assets/stylesheets/web/main.css.scss b/web/app/assets/stylesheets/web/main.css.scss index 291f61967..82c5160fd 100644 --- a/web/app/assets/stylesheets/web/main.css.scss +++ b/web/app/assets/stylesheets/web/main.css.scss @@ -64,10 +64,16 @@ body.web { } } + &.register { + .landing-content { + min-height:460px; + } + } + .landing-content { background-color:black; width:100%; - min-height: 460px; + min-height: 366px; position:relative; padding-bottom:30px; diff --git a/web/app/assets/stylesheets/web/welcome.css.scss b/web/app/assets/stylesheets/web/welcome.css.scss index c4bd44786..2171789eb 100644 --- a/web/app/assets/stylesheets/web/welcome.css.scss +++ b/web/app/assets/stylesheets/web/welcome.css.scss @@ -19,6 +19,15 @@ body.web { } } + .jamfest { + top:-70px; + position:relative; + + .jamblaster { + font-weight:bold; + } + } + .follow-links { position: absolute; right: 0; diff --git a/web/app/controllers/api_backing_tracks_controller.rb b/web/app/controllers/api_backing_tracks_controller.rb new file mode 100644 index 000000000..9ed591422 --- /dev/null +++ b/web/app/controllers/api_backing_tracks_controller.rb @@ -0,0 +1,32 @@ +class ApiBackingTracksController < ApiController + + # have to be signed in currently to see this screen + before_filter :api_signed_in_user + + before_filter :lookup_recorded_backing_track, :only => [ :backing_track_silent ] + + respond_to :json + + def index + tracks = [ + {:name=>'foo',:path=>"foobar.mp3", :length=>4283}, + {:name=>'bar',:path=>"foo.mp3",:length=>3257} + ] + @backing_tracks, @next = tracks, nil + render "api_backing_tracks/index", :layout => nil + end + + def backing_track_silent + @recorded_backing_track.mark_silent + + render :json => {}, :status => 200 + end + + private + + def lookup_recorded_backing_track + @recorded_backing_track = RecordedBackingTrack.find_by_recording_id_and_client_track_id!(params[:id], params[:track_id]) + raise PermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR unless @recorded_backing_track.recording.has_access?(current_user) + end + +end # class ApiBackingTracksController diff --git a/web/app/controllers/api_music_sessions_controller.rb b/web/app/controllers/api_music_sessions_controller.rb index b85f265d0..e5f6a7bd4 100644 --- a/web/app/controllers/api_music_sessions_controller.rb +++ b/web/app/controllers/api_music_sessions_controller.rb @@ -4,7 +4,7 @@ class ApiMusicSessionsController < ApiController # have to be signed in currently to see this screen before_filter :api_signed_in_user, :except => [ :add_like, :show, :show_history, :add_session_info_comment ] - before_filter :lookup_session, only: [:show, :update, :delete, :claimed_recording_start, :claimed_recording_stop, :track_sync, :jam_track_open, :jam_track_close] + before_filter :lookup_session, only: [:show, :update, :delete, :claimed_recording_start, :claimed_recording_stop, :track_sync, :jam_track_open, :jam_track_close, :backing_track_open, :backing_track_close, :metronome_open, :metronome_close] skip_before_filter :api_signed_in_user, only: [:perf_upload] respond_to :json @@ -357,7 +357,7 @@ class ApiMusicSessionsController < ApiController end def track_sync - @tracks = MusicSessionManager.new.sync_tracks(@music_session, params[:client_id], params[:tracks]) + @tracks = MusicSessionManager.new.sync_tracks(@music_session, params[:client_id], params[:tracks], params[:backing_tracks]) unless @tracks.kind_of? Array # we have to do this because api_session_detail_url will fail with a bad @tracks @@ -597,8 +597,44 @@ class ApiMusicSessionsController < ApiController respond_with_model(@music_session) end + def backing_track_open + unless @music_session.users.exists?(current_user) + raise PermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR + end - private + @backing_track_path = params[:backing_track_path] + @music_session.open_backing_track(current_user, @backing_track_path) + respond_with_model(@music_session) + end + + def backing_track_close + unless @music_session.users.exists?(current_user) + raise PermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR + end + + @music_session.close_backing_track() + respond_with_model(@music_session) + end + + def metronome_open + unless @music_session.users.exists?(current_user) + raise PermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR + end + + @music_session.open_metronome(current_user) + respond_with_model(@music_session) + end + + def metronome_close + unless @music_session.users.exists?(current_user) + raise PermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR + end + + @music_session.close_metronome() + respond_with_model(@music_session) + end + +private def lookup_session @music_session = ActiveMusicSession.find(params[:id]) diff --git a/web/app/controllers/api_recordings_controller.rb b/web/app/controllers/api_recordings_controller.rb index 6244e1e17..5caa975a8 100644 --- a/web/app/controllers/api_recordings_controller.rb +++ b/web/app/controllers/api_recordings_controller.rb @@ -3,6 +3,7 @@ class ApiRecordingsController < ApiController before_filter :lookup_recording, :only => [ :show, :stop, :claim, :discard, :keep, :delete_claim ] before_filter :lookup_recorded_track, :only => [ :download, :upload_next_part, :upload_sign, :upload_part_complete, :upload_complete ] + before_filter :lookup_recorded_backing_track, :only => [ :backing_track_download, :backing_track_upload_next_part, :backing_track_upload_sign, :backing_track_upload_part_complete, :backing_track_upload_complete ] before_filter :lookup_recorded_video, :only => [ :video_upload_sign, :video_upload_start, :video_upload_complete ] before_filter :lookup_stream_mix, :only => [ :upload_next_part_stream_mix, :upload_sign_stream_mix, :upload_part_complete_stream_mix, :upload_complete_stream_mix ] @@ -43,7 +44,11 @@ class ApiRecordingsController < ApiController @recorded_track = RecordedTrack.find_by_recording_id_and_client_track_id(params[:id], params[:track_id]) end - def download + def show_recorded_backing_track + @recorded_backing_track = RecordedBackingTrack.find_by_recording_id_and_client_track_id(params[:id], params[:track_id]) + end + + def download # track raise PermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR unless @recorded_track.can_download?(current_user) @recorded_track.current_user = current_user @@ -58,6 +63,21 @@ class ApiRecordingsController < ApiController end end + def backing_track_download + raise PermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR unless @recorded_backing_track.can_download?(current_user) + + @recorded_backing_track.current_user = current_user + @recorded_backing_track.update_download_count + + @recorded_backing_track.valid? + if !@recorded_backing_track.errors.any? + @recorded_backing_track.save! + redirect_to @recorded_backing_track.sign_url + else + render :json => { :message => "download limit surpassed" }, :status => 404 + end + end + def start music_session = ActiveMusicSession.find(params[:music_session_id]) @@ -227,6 +247,61 @@ class ApiRecordingsController < ApiController end end + def backing_track_upload_next_part + length = params[:length] + md5 = params[:md5] + + @recorded_backing_track.upload_next_part(length, md5) + + if @recorded_backing_track.errors.any? + + response.status = :unprocessable_entity + # this is not typical, but please don't change this line unless you are sure it won't break anything + # this is needed because after_rollback in the RecordedTrackObserver touches the model and something about it's + # state doesn't cause errors to shoot out like normal. + render :json => { :errors => @recorded_backing_track.errors }, :status => 422 + else + result = { + :part => @recorded_backing_track.next_part_to_upload, + :offset => @recorded_backing_track.file_offset.to_s + } + + render :json => result, :status => 200 + end + + end + + def backing_track_upload_sign + render :json => @recorded_backing_track.upload_sign(params[:md5]), :status => 200 + end + + def backing_track_upload_part_complete + part = params[:part] + offset = params[:offset] + + @recorded_backing_track.upload_part_complete(part, offset) + + if @recorded_backing_track.errors.any? + response.status = :unprocessable_entity + respond_with @recorded_backing_track + else + render :json => {}, :status => 200 + end + end + + def backing_track_upload_complete + @recorded_backing_track.upload_complete + @recorded_backing_track.recording.upload_complete + + if @recorded_backing_track.errors.any? + response.status = :unprocessable_entity + respond_with @recorded_backing_track + return + else + render :json => {}, :status => 200 + end + end + # POST /api/recordings/:id/videos/:video_id/upload_sign def video_upload_sign length = params[:length] @@ -314,6 +389,11 @@ class ApiRecordingsController < ApiController raise PermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR unless @recorded_track.recording.has_access?(current_user) end + def lookup_recorded_backing_track + @recorded_backing_track = RecordedBackingTrack.find_by_recording_id_and_client_track_id!(params[:id], params[:track_id]) + raise PermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR unless @recorded_backing_track.recording.has_access?(current_user) + end + def lookup_stream_mix @quick_mix = QuickMix.find_by_recording_id_and_user_id!(params[:id], current_user.id) raise PermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR unless @quick_mix.recording.has_access?(current_user) diff --git a/web/app/controllers/api_rsvp_requests_controller.rb b/web/app/controllers/api_rsvp_requests_controller.rb index 82c9c3466..b4fd641cb 100644 --- a/web/app/controllers/api_rsvp_requests_controller.rb +++ b/web/app/controllers/api_rsvp_requests_controller.rb @@ -12,7 +12,8 @@ class ApiRsvpRequestsController < ApiController music_session = MusicSession.find(params[:session_id]) # retrieve all requests for this session - if music_session.creator.id == current_user.id + creator = music_session.creator + if creator && creator.id == current_user.id @rsvp_requests = RsvpRequest.index(music_session, nil, params) # scope the response to the current user diff --git a/web/app/controllers/api_users_controller.rb b/web/app/controllers/api_users_controller.rb index 7d8d40f94..c9598a543 100644 --- a/web/app/controllers/api_users_controller.rb +++ b/web/app/controllers/api_users_controller.rb @@ -1,7 +1,7 @@ require 'sanitize' class ApiUsersController < ApiController - before_filter :api_signed_in_user, :except => [:create, :show, :signup_confirm, :auth_session_create, :complete, :finalize_update_email, :isp_scoring, :add_play, :crash_dump] + before_filter :api_signed_in_user, :except => [:create, :show, :signup_confirm, :auth_session_create, :complete, :finalize_update_email, :isp_scoring, :add_play, :crash_dump, :validate_data] before_filter :auth_user, :only => [:session_settings_show, :session_history_index, :session_user_history_index, :update, :delete, :liking_create, :liking_destroy, # likes :following_create, :following_show, :following_destroy, # followings @@ -699,6 +699,37 @@ class ApiUsersController < ApiController end end + def validate_data + unless (data = params[:data]).present? + render(json: { message: "blank data #{data}" }, status: :unprocessable_entity) && return + end + url = nil + site = params[:sitetype] + if site.blank? || 'url'==site + url = data + elsif Utils.recording_source?(site) + rec_id = Utils.extract_recording_id(site, data) + if rec_id + render json: { message: 'Valid Site', recording_id: rec_id, data: data }, status: 200 + return + else + render json: { message: 'Invalid Site', data: data, errors: { site: ["Could not detect recording identifier"] } }, status: 200 + return + end + else + url = Utils.username_url(data, site) + end + unless url.blank? + if errmsg = Utils.site_validator(url, site) + render json: { message: 'Invalid Site', data: data, errors: { site: [errmsg] } }, status: 200 + else + render json: { message: 'Valid Site', data: data }, status: 200 + end + else + render json: { message: "unknown validation for data '#{params[:data]}', site '#{params[:site]}'" }, status: :unprocessable_entity + end + end + ###################### RECORDINGS ####################### # def recording_index # @recordings = User.recording_index(current_user, params[:id]) diff --git a/web/app/controllers/landings_controller.rb b/web/app/controllers/landings_controller.rb index 0249897d7..1268f730b 100644 --- a/web/app/controllers/landings_controller.rb +++ b/web/app/controllers/landings_controller.rb @@ -22,6 +22,22 @@ class LandingsController < ApplicationController end end + def watch_overview_kick + render 'watch_kick', layout: 'web' + end + + def watch_overview_kick2 + render 'watch_kick_2', layout: 'web' + end + + def watch_overview_kick3 + render 'watch_kick_3', layout: 'web' + end + + def watch_overview_kick4 + render 'watch_kick_4', layout: 'web' + end + def watch_overview @promo_buzz = PromoBuzz.active diff --git a/web/app/controllers/spikes_controller.rb b/web/app/controllers/spikes_controller.rb index 36b0596a4..ab6da676d 100644 --- a/web/app/controllers/spikes_controller.rb +++ b/web/app/controllers/spikes_controller.rb @@ -52,4 +52,12 @@ class SpikesController < ApplicationController render :layout => 'web' end + + def site_validate + render :layout => 'web' + end + + def recording_source + render :layout => 'web' + end end diff --git a/web/app/controllers/users_controller.rb b/web/app/controllers/users_controller.rb index 6cd34cabd..61c42fe28 100644 --- a/web/app/controllers/users_controller.rb +++ b/web/app/controllers/users_controller.rb @@ -190,12 +190,12 @@ class UsersController < ApplicationController def welcome @slides = [ - Slide.new("JamKazam Overview", "web/carousel_musicians.jpg", "http://www.youtube.com/embed/ylYcvTY9CVo?autoplay=1"), - Slide.new("Getting Started", "web/carousel_fans.jpg", "http://www.youtube.com/embed/DBo--aj_P1w?autoplay=1"), - Slide.new("Playing in a Session", "web/carousel_bands.jpg", "http://www.youtube.com/embed/zJ68hA8-fLA?autoplay=1"), - Slide.new("JamKazam Overview", "web/carousel_musicians.jpg", "http://www.youtube.com/embed/ylYcvTY9CVo?autoplay=1"), - Slide.new("Getting Started", "web/carousel_fans.jpg", "http://www.youtube.com/embed/DBo--aj_P1w?autoplay=1"), - Slide.new("Playing in a Session", "web/carousel_bands.jpg", "http://www.youtube.com/embed/zJ68hA8-fLA?autoplay=1") + Slide.new("JamKazam Overview", "web/carousel_overview.png", "http://www.youtube.com/embed/ylYcvTY9CVo?autoplay=1"), + Slide.new("The Revolutionary New JamBlaster!", "web/carousel_jamblaster.png", "http://www.youtube.com/embed/gAJAIHMyois?autoplay=1"), + Slide.new("Kudos From Our Community", "web/carousel_community.png", "http://www.youtube.com/embed/_7qj5RXyHCo?autoplay=1"), + Slide.new("JamKazam Overview", "web/carousel_overview.png", "http://www.youtube.com/embed/ylYcvTY9CVo?autoplay=1"), + Slide.new("The Revolutionary New JamBlaster!", "web/carousel_fans.jpg", "http://www.youtube.com/embed/gAJAIHMyois?autoplay=1"), + Slide.new("Kudos From Our Community", "web/carousel_community.png", "http://www.youtube.com/embed/_7qj5RXyHCo?autoplay=1") ] @promo_buzz = PromoBuzz.active @@ -207,8 +207,8 @@ class UsersController < ApplicationController end # temporary--will go away soon - @jamfest_2014 = Event.find_by_id('80bb6acf-3ddc-4305-9442-75e6ec047c27') # production ID - @jamfest_2014 = Event.find_by_id('a2dfbd26-9b17-4446-8c61-b67a542ea6ee') unless @jamfest_2014 # development ID + #@jamfest_2014 = Event.find_by_id('80bb6acf-3ddc-4305-9442-75e6ec047c27') # production ID + #@jamfest_2014 = Event.find_by_id('a2dfbd26-9b17-4446-8c61-b67a542ea6ee') unless @jamfest_2014 # development ID # temporary--end @welcome_page = true diff --git a/web/app/helpers/sessions_helper.rb b/web/app/helpers/sessions_helper.rb index 98b0c9774..3545c3d05 100644 --- a/web/app/helpers/sessions_helper.rb +++ b/web/app/helpers/sessions_helper.rb @@ -91,4 +91,10 @@ module SessionsHelper current_user.musician? ? 'Musician' : 'Fan' end end + + def metronome_tempos + [ + 40, 42, 44, 46, 48, 50, 52, 54, 56, 58, 60, 63, 66, 69, 72, 76, 80, 84, 88, 92, 96, 100, 104, 108, 112, 116, 120, 126, 132, 138, 144, 152, 160, 168, 176, 184, 192, 200, 208 + ] + end end diff --git a/web/app/views/api_backing_tracks/index.rabl b/web/app/views/api_backing_tracks/index.rabl new file mode 100644 index 000000000..a5b9b4b9b --- /dev/null +++ b/web/app/views/api_backing_tracks/index.rabl @@ -0,0 +1,7 @@ +node :next do |page| + @next +end + +node :backing_tracks do |page| + @backing_tracks +end diff --git a/web/app/views/api_claimed_recordings/show.rabl b/web/app/views/api_claimed_recordings/show.rabl index dbc1c7fd9..aa7de2609 100644 --- a/web/app/views/api_claimed_recordings/show.rabl +++ b/web/app/views/api_claimed_recordings/show.rabl @@ -2,6 +2,8 @@ # I don't think I need to include URLs since that's handled by syncing. This is just to make the metadata # depictable. +# THIS IS USED DIRECTLY BY THE CLIENT. DO NOT CHANGE FORMAT UNLESS YOU VERIFY CLIENT FIRST. IN PARTICULAR RecordingFileStorage#getLocalRecordingState + object @claimed_recording attributes :id, :name, :description, :is_public, :genre_id, :discarded @@ -36,6 +38,18 @@ child(:recording => :recording) { } } + child(:recorded_backing_tracks => :recorded_backing_tracks) { + attributes :id, :fully_uploaded, :client_track_id, :client_id, :filename + + child(:user => :user) { + attributes :id, :first_name, :last_name, :name, :city, :state, :country, :location, :photo_url + } + + node :mine do |recorded_backing_track| + recorded_backing_track.user == current_user + end + } + child(:comments => :comments) { attributes :comment, :created_at diff --git a/web/app/views/api_music_sessions/show.rabl b/web/app/views/api_music_sessions/show.rabl index 124f9ba56..cd277c3fe 100644 --- a/web/app/views/api_music_sessions/show.rabl +++ b/web/app/views/api_music_sessions/show.rabl @@ -13,7 +13,7 @@ if !current_user } else - attributes :id, :name, :description, :musician_access, :approval_required, :fan_access, :fan_chat, :band_id, :user_id, :claimed_recording_initiator_id, :track_changes_counter, :max_score + attributes :id, :name, :description, :musician_access, :approval_required, :fan_access, :fan_chat, :band_id, :user_id, :claimed_recording_initiator_id, :track_changes_counter, :max_score, :backing_track_path, :metronome_active node :can_join do |session| session.can_join?(current_user, true) @@ -54,6 +54,10 @@ else child(:tracks => :tracks) { attributes :id, :connection_id, :instrument_id, :sound, :client_track_id, :client_resource_id, :updated_at } + + child(:backing_tracks => :backing_tracks) { + attributes :id, :connection_id, :filename, :client_track_id, :client_resource_id, :updated_at + } } child({:invitations => :invitations}) { @@ -114,6 +118,14 @@ else attributes :id, :first_name, :last_name, :city, :state, :country, :photo_url } } + + child(:recorded_backing_tracks => :recorded_backing_tracks) { + attributes :id, :fully_uploaded, :client_track_id, :client_id, :filename + + child(:user => :user) { + attributes :id, :first_name, :last_name, :city, :state, :country, :photo_url + } + } } } diff --git a/web/app/views/api_recordings/show.rabl b/web/app/views/api_recordings/show.rabl index 1fe574026..f3c817b07 100644 --- a/web/app/views/api_recordings/show.rabl +++ b/web/app/views/api_recordings/show.rabl @@ -27,6 +27,12 @@ child(:recorded_tracks => :recorded_tracks) { end } +child(:recorded_backing_tracks => :recorded_backing_tracks) { + node do |recorded_backing_track| + partial("api_recordings/show_recorded_backing_track", :object => recorded_backing_track) + end +} + child(:comments => :comments) { attributes :comment, :created_at diff --git a/web/app/views/api_recordings/show_recorded_backing_track.rabl b/web/app/views/api_recordings/show_recorded_backing_track.rabl new file mode 100644 index 000000000..6487b9db6 --- /dev/null +++ b/web/app/views/api_recordings/show_recorded_backing_track.rabl @@ -0,0 +1,11 @@ +object @recorded_backing_track + +attributes :id, :fully_uploaded, :client_track_id, :client_id, :recording_id, :filename + +node :mine do |recorded_backing_track| + recorded_backing_track.user == current_user + end + +child(:user => :user) { + attributes :id, :first_name, :last_name, :city, :state, :country, :location, :photo_url +} \ No newline at end of file diff --git a/web/app/views/api_user_syncs/show.rabl b/web/app/views/api_user_syncs/show.rabl index 0c63ba010..730120276 100644 --- a/web/app/views/api_user_syncs/show.rabl +++ b/web/app/views/api_user_syncs/show.rabl @@ -18,7 +18,6 @@ glue :recorded_track do partial("api_recordings/show", :object => recorded_track.recording) end - node :upload do |recorded_track| { should_upload: true, @@ -35,6 +34,45 @@ glue :recorded_track do end + +glue :recorded_backing_track do + + @object.current_user = current_user + + node :type do |i| + 'recorded_backing_track' + end + + attributes :id, :recording_id, :client_id, :track_id, :client_track_id, :md5, :length, :download_count, :fully_uploaded, :upload_failures, :part_failures, :created_at, :filename + + node :user do |recorded_backing_track| + partial("api_users/show_minimal", :object => recorded_backing_track.user) + end + + node :recording do |recorded_backing_track| + partial("api_recordings/show", :object => recorded_backing_track.recording) + end + + node :mine do |recorded_backing_track| + recorded_backing_track.user == current_user + end + + node :upload do |recorded_backing_track| + { + should_upload: true, + too_many_upload_failures: recorded_backing_track.too_many_upload_failures? + } + end + + node :download do |recorded_backing_track| + { + should_download: recorded_backing_track.can_download?(current_user), + too_many_downloads: recorded_backing_track.too_many_downloads? + } + end + +end + glue :mix do @object.current_user = current_user diff --git a/web/app/views/clients/_help.html.slim b/web/app/views/clients/_help.html.slim index 1f0efda08..4fda8bda3 100644 --- a/web/app/views/clients/_help.html.slim +++ b/web/app/views/clients/_help.html.slim @@ -199,6 +199,10 @@ script type="text/template" id="template-help-media-controls-disabled" | Only the person who opened the recording can control the volume levels. | {% } %} + +script type="text/template" id="template-help-volume-media-mixers" + | Audio files only expose master mix controls, so any change here will also affect everyone in the session. + script type="text/template" id="template-help-downloaded-jamtrack" .downloaded-jamtrack p When a JamTrack is first purchased, a user-specific version of it is created on the server. Once it's ready, it's then downloaded to the client. diff --git a/web/app/views/clients/_home.html.slim b/web/app/views/clients/_home.html.slim index 5492f904c..8130e82cc 100644 --- a/web/app/views/clients/_home.html.slim +++ b/web/app/views/clients/_home.html.slim @@ -1,4 +1,8 @@ -.screen layout="screen" layout-id="home" +.screen layout="screen" layout-id="home" + -if Rails.configuration.show_jamblaster_notice + #jamblaster-notice + a href='https://www.youtube.com/watch?v=gAJAIHMyois' rel="external" + span Check out the amazing new JamBlaster, and learn how it can improve your sessions! / Layout is different if jam_tracks tile available: -jamtracks=Rails.configuration.jam_tracks_available -if (jamtracks) @@ -19,7 +23,7 @@ h2 find session .homebox-info /! 1 session invitation, 19 public sessions active - .homecard.feed layout-grid-columns="4" layout-grid-position="8,0" layout-grid-rows="1" layout-link="feed" + .homecard.feed layout-grid-columns="4" layout-grid-position="8,0" layout-grid-rows="1" layout-link="feed" h2 feed .homebox-info /! 4 friends online, 2 currently in sessions diff --git a/web/app/views/clients/_session.html.erb b/web/app/views/clients/_session.html.erb deleted file mode 100644 index 45403f7c9..000000000 --- a/web/app/views/clients/_session.html.erb +++ /dev/null @@ -1,199 +0,0 @@ - -
-
-
- <%= image_tag "shared/icon_session.png", {:height => 19, :width => 19} %> -
-

session

-
- -
- - - - - -
-
- - -
- - -
-

my tracks

-
- <%= image_tag "content/icon_settings_lg.png", {:width => 18, :height => 18} %> - Settings -
- -
-
- -
-
- - - - - -
-

other audio

- -
-
-
- <%= image_tag "content/icon_folder.png", {width:22, height:20} %> Open: -
    -
  • Recording
  • - <% if Rails.application.config.jam_tracks_available %> -
  • JamTrack
  • - <% end %> - -
-
-
-
- - <%= render "play_controls" %> - -
- - - -
- -
-
-
-
-
- - -<%= render "configureTrack" %> -<%= render "addTrack" %> -<%= render "addNewGear" %> -<%= render "error" %> -<%= render "sessionSettings" %> - - - - - - - - - diff --git a/web/app/views/clients/_session.html.slim b/web/app/views/clients/_session.html.slim new file mode 100644 index 000000000..49a5b2cb0 --- /dev/null +++ b/web/app/views/clients/_session.html.slim @@ -0,0 +1,145 @@ +#session-screen.screen.secondary[layout="screen" layout-id="session" layout-arg="id"] + .content-head + .content-icon + = image_tag "shared/icon_session.png", {:height => 19, :width => 19} + h1 + | session + .content-body + #session-controls + a#session-resync.button-grey.resync.left + = image_tag "content/icon_resync.png", {:align => "texttop", :height => 14, :width => 12} + | RESYNC + a#session-settings-button.button-grey.left[layout-link="session-settings"] + = image_tag "content/icon_settings_sm.png", {:align => "texttop", :height => 12, :width => 12} + | SETTINGS + a.button-grey.left[layout-link="share-dialog"] + = image_tag "content/icon_share.png", {:align => "texttop", :height => 12, :width => 12} + | SHARE + .block + .label + | VOLUME: + #volume.fader.lohi[mixer-id=""] + .block.monitor-mode-holder + .label + | MIX: + select.monitor-mode.easydropdown + option.label[value="personal"] + | Personal + option[value="master"] + | Master + a#session-leave.button-grey.right.leave[href="/client#/home"] + | X  LEAVE + #tracks + .content-scroller + .content-wrapper + .session-mytracks + h2 + | my tracks + #track-settings.session-add[style="display:block;" layout-link="configure-tracks"] + = image_tag "content/icon_settings_lg.png", {:width => 18, :height => 18} + span + | Settings + .session-tracks-scroller + #session-mytracks-container + #voice-chat.voicechat[style="display:none;" mixer-id=""] + .voicechat-label + | CHAT + .voicechat-gain + .voicechat-mute.enabled[control="mute" mixer-id=""] + .session-livetracks + h2 + | live tracks + .session-add[layout-link="select-invites"] + a#session-invite-musicians[href="#"] + = image_tag "content/icon_add.png", {:width => 19, :height => 19, :align => "texttop"} + |   Invite Musicians + .session-tracks-scroller + #session-livetracks-container + .when-empty.livetracks + | No other musicians + br + | are in your session + br[clear="all"] + #recording-start-stop.recording + a + = image_tag "content/recordbutton-off.png", {:width => 20, :height => 20, :align => "absmiddle"} + |    + span#recording-status + | Make a Recording + .session-recordings + h2 + | other audio + .session-recording-name-wrapper + .session-recording-name.left + | (No audio loaded) + .session-add.right + a#close-playback-recording[href="#"] + = image_tag "content/icon_close.png", {:width => 18, :height => 20, :align => "texttop"} + |   Close + .session-tracks-scroller + #session-recordedtracks-container + .when-empty.recordings + span.open-media-file-header + = image_tag "content/icon_folder.png", {width:22, height:20} + | Open: + ul.open-media-file-options + li + a#open-a-recording[href="#"] + | Recording + - if Rails.application.config.jam_tracks_available + li + a#open-a-jamtrack[href="#"] + | JamTrack + - if Rails.application.config.backing_tracks_available + li + a#open-a-backingtrack[href="#"] + | Audio File + .when-empty.use-metronome-header + - if Rails.application.config.metronome_available + = image_tag "content/icon_metronome.png", {width:22, height:20} + a#open-a-metronome[href="#"] + | Use Metronome + br[clear="all"] + = render "play_controls" += render "configureTrack" += render "addTrack" += render "addNewGear" += render "error" += render "sessionSettings" +script#template-session-track[type="text/template"] + .session-track.track client-id="{clientId}" track-id="{trackId}" + .track-vu-left.mixer-id="{vuMixerId}_vul" + .track-vu-right.mixer-id="{vuMixerId}_vur" + .track-label[title="{name}"] + span.name-text="{name}" + #div-track-close.track-close.op30 track-id="{trackId}" + =image_tag("content/icon_closetrack.png", {width: 12, height: 12}) + div class="{avatarClass}" + img src="{avatar}" + .track-instrument class="{preMasteredClass}" + img height="45" src="{instrumentIcon}" width="45" + .track-gain mixer-id="{mixerId}" + .track-icon-mute class="{muteClass}" control="mute" mixer-id="{muteMixerId}" + .track-icon-loop.hidden control="loop" + input#loop-button type="checkbox" value="loop" Loop + .track-connection.grey mixer-id="{mixerId}_connection" + CONNECTION + .disabled-track-overlay + .metronome-selects.hidden + select.metronome-select.metro-sound title="Metronome Sound" + option.label value="Beep" Bleep + option.label value="Click" Click + option.label value="Snare" Drum + br + select.metronome-select.metro-tempo title="Metronome Tempo" + - metronome_tempos.each do |t| + option.label value=t + =t + +script#template-option type="text/template" + option value="{value}" title="{label}" selected="{selected}" + ="{label}" + +script#template-genre-option type="text/template" + option value="{value}" + ="{label}" \ No newline at end of file diff --git a/web/app/views/clients/_sidebar.html.erb b/web/app/views/clients/_sidebar.html.erb index dbb92c66a..b8d44dc68 100644 --- a/web/app/views/clients/_sidebar.html.erb +++ b/web/app/views/clients/_sidebar.html.erb @@ -210,7 +210,7 @@